Promotions Engine Low-Level Design
A promotions engine governs discount codes, automatic promotions, stackability rules, customer eligibility, budget caps, and redemption tracking. Interviewers use this design to test rule evaluation, concurrency control for budget enforcement, and abuse prevention.
Requirements
Functional
- Define promotions with eligibility criteria (customer segment, minimum order value, specific products or categories).
- Support discount types: percentage off, fixed amount off, free shipping, buy-X-get-Y.
- Allow stackable and non-stackable promotions with explicit precedence rules.
- Enforce per-promotion budget caps (maximum total discount dollars) and per-customer redemption limits.
- Track every redemption with a link to the order for analytics and fraud detection.
Non-Functional
- Promotion evaluation during checkout under 50 ms.
- Budget cap enforcement: zero over-budget redemptions under concurrent load.
- Idempotent application: applying the same coupon twice to the same order must have no additional effect.
Data Model
- promotion: id (UUID), name, discount_type (PERCENT | FIXED | FREE_SHIPPING | BXGY), discount_value, stackable (BOOLEAN), stack_priority (INT), max_budget_cents (nullable), redeemed_budget_cents (running total), max_redemptions_per_customer (nullable), starts_at, ends_at, status (DRAFT | ACTIVE | PAUSED | EXHAUSTED | EXPIRED).
- promotion_eligibility: id, promotion_id, criterion_type (CUSTOMER_SEGMENT | MIN_ORDER | PRODUCT | CATEGORY | FIRST_ORDER), criterion_value (JSONB).
- coupon_code: code (VARCHAR, PK), promotion_id, max_uses (nullable), use_count, expires_at, created_at.
- redemption: id (UUID), promotion_id, coupon_code (nullable), order_id (UNIQUE per promotion to enforce idempotency), customer_id, discount_applied_cents, redeemed_at.
- budget_lock: promotion_id (PK), locked_cents, lock_version — used for optimistic budget reservation.
Core Algorithms and Flows
Eligibility Evaluation
At checkout the engine fetches all ACTIVE promotions whose time window includes the current timestamp. For each promotion it evaluates eligibility criteria in order. Criteria are evaluated with short-circuit AND logic: if any criterion fails the promotion is skipped immediately. Criteria checked: customer segment membership (from customer profile service), minimum order subtotal, presence of qualifying product or category in the cart, and first-order flag from the order history service. Eligible promotions are collected into a candidate set for stackability resolution.
Stackable Discount Resolution
Non-stackable promotions are sorted by stack_priority (ascending — lower wins). The highest-priority non-stackable promotion is selected and all other non-stackable promotions are excluded. Stackable promotions are then applied on top of the winning non-stackable discount. Percentage discounts are applied to the post-previous-discount subtotal (multiplicative stacking), not to the original subtotal. Fixed discounts are applied additively after all percentage discounts. Free shipping is applied independently regardless of stack order. The final discount cannot exceed the order subtotal.
Budget Cap Enforcement
Before committing a redemption the engine reserves budget using optimistic locking on budget_lock. It reads (locked_cents, lock_version), computes the new total after adding the proposed discount, checks it against max_budget_cents, and attempts an UPDATE WHERE lock_version = current_version. If the update affects zero rows a concurrent reservation won the race; the engine retries up to three times then rejects the promotion if the budget is now exhausted. On successful reservation the redemption row is inserted and the promotion.redeemed_budget_cents is updated asynchronously by a background reconciler.
Per-Customer Redemption Limit
Before applying a promotion the engine queries the redemption table for COUNT(*) WHERE promotion_id = X AND customer_id = Y. If the count meets or exceeds max_redemptions_per_customer the promotion is excluded from the candidate set. This query is served from a read replica with a short TTL and is bounded by the low cardinality of per-customer redemption history.
Idempotent Redemption
The redemption table has a UNIQUE constraint on (promotion_id, order_id). A second attempt to apply the same promotion to the same order triggers an ON CONFLICT DO NOTHING and returns the existing discount amount without incrementing counters or consuming additional budget. This covers retry scenarios where the checkout saga is re-executed after a transient failure.
API Design
POST /v1/promotions/evaluate— evaluates all eligible promotions for a cart and customer; returns ranked discount list with amounts.POST /v1/promotions/apply— commits the selected promotions to the order; enforces budget caps and per-customer limits atomically.POST /v1/coupon-codes/validate— validates a coupon code and returns the associated promotion if eligible.POST /v1/promotions— merchant creates a new promotion with eligibility criteria and budget.PATCH /v1/promotions/{id}— updates promotion parameters; pauses or activates a promotion.GET /v1/promotions/{id}/redemptions?cursor={c}— paginated redemption history for analytics.
Scalability Considerations
- Active promotion sets are small (typically under 50 at any time); the full set is cached in Redis and refreshed on any promotion status change, making evaluation an in-memory operation after the initial cache warm.
- Budget lock contention is highest during flash sales with tight caps; the optimistic retry loop handles bursts without blocking threads, and promotions nearing exhaustion are flagged to reduce unnecessary evaluation in the candidate set.
- Redemption count queries are served from a read replica with a 30-second cache; brief over-limit redemptions are acceptable at the margin and are caught by a post-processing fraud review job.
- The evaluate endpoint is read-only and stateless; it scales horizontally without coordination, as no state is written until the apply call commits the selected promotions.
- Budget reconciliation runs as a low-priority background job comparing redeemed_budget_cents against the sum of redemption.discount_applied_cents, correcting drift caused by failed write paths without affecting the hot redemption path.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does stackable discount resolution work in a promotions engine?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Each promotion is tagged as stackable or exclusive. The engine collects all promotions the cart qualifies for, separates them into exclusive and stackable pools, selects the single best exclusive promotion by discount value, and then applies all stackable promotions on top. Stacking order matters: percentage discounts are applied sequentially on the running subtotal (compounding), while absolute discounts are summed and deducted once. The engine enforces a floor price so stacking cannot produce a negative line-item price.”
}
},
{
“@type”: “Question”,
“name”: “How is eligibility rule evaluation implemented in a promotions engine?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Eligibility rules are expressed as composable predicates: customer segment membership, minimum cart value, qualifying SKU or category presence, coupon code match, and date/time window. Rules are stored as JSON condition trees and evaluated against the cart context at runtime using a lightweight rules interpreter. Short-circuit evaluation stops as soon as a required condition fails. This design lets merchandisers configure complex eligibility without code deploys, and the interpreter is unit-tested against a suite of fixture carts.”
}
},
{
“@type”: “Question”,
“name”: “How is budget cap enforcement implemented in a promotions engine?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Each promotion has an optional total-budget cap stored in the promotions table. A Redis counter keyed by promotion ID tracks cumulative discount granted. Before applying a promotion, the engine atomically increments the counter by the discount amount using INCRBY and checks if the result exceeds the budget. If it does, the engine decrements (rolls back) and skips the promotion. This optimistic approach is correct under concurrent load because Redis commands are atomic. A reconciliation job periodically syncs the Redis counter with DB-summed redemption amounts.”
}
},
{
“@type”: “Question”,
“name”: “How is idempotent redemption ensured in a promotions engine?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Each redemption attempt is keyed by (promotionId, orderId). Before applying a promotion, the engine checks a redemptions table for this composite key. If a row exists, the promotion is considered already applied and the stored discount value is returned without reprocessing. If no row exists, the engine applies the discount and inserts the redemption row in the same DB transaction as the order update. This guarantees that retried checkout requests — due to network errors or user double-submission — never double-count a promotion.”
}
}
]
}
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering
See also: Shopify Interview Guide
See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems