Promotions Engine Low-Level Design: Coupon Stacking, Eligibility Rules, and Budget Caps

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.

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

Scroll to Top