System Design Problem

Design a Coupon and Discount Engine

Commonly Asked By:AmazonWalmartPayPalGroupon

Interview Setup

Interview Prompt

Design a coupon and discount engine that validates promo codes at checkout, enforces stacking rules and usage limits, and supports bulk campaign code generation.

Clarifying Questions (ask before designing)

QuestionWhy it matters
Validate-only at cart preview, or reserve redemption at apply time?
  • Preview can be eventually consistent
  • apply must be atomic to prevent over-redemption at 10M redemptions/day.
Can multiple coupons stack on one order?Stack groups and exclusive flags alter evaluation ordering, where an incorrect policy erodes profit margins.
For global limits vs per-user limits, what happens during concurrent checkouts?Two users finishing checkout simultaneously for the last redemption slot needs Redis INCR + DB constraint.
Bulk unique codes or single shared code per campaign?1M unique codes per campaign changes storage and lookup (prefix index vs single hot key).

Scope

In scope

  • Rule engine for eligibility
  • Stacking policies
  • Atomic redemption limits
  • Bulk coupon generation
  • Validate and apply APIs

Out of scope (state explicitly)

  • Payment capture and settlement
  • Full catalog management
  • Fraud ML beyond basic abuse rules

Functional Requirements

Start by clarifying which coupon categories and stacking policies are in scope. Confirm total usage limits per coupon and per customer, because financial liability from over-redemption is the primary failure mode interviewers probe.

  • Create coupons: Percentage off, fixed amount, BOGO, free shipping, tiered discounts
  • Apply coupons: Validate and apply coupon code at checkout
  • Auto-apply promotions: Automatic discounts (site-wide sales, category discounts) without code
  • Stacking rules: Define which coupons can combine (stackable vs exclusive)
  • Eligibility rules: Target by user segment, purchase history, cart value, product category, first-time buyer
  • Usage limits: Per-coupon limit (10,000 total), per-user limit (1 per customer), time-bound
  • Coupon generation: Bulk generate unique codes for campaigns (100K codes)
  • Analytics: Track redemption rates, revenue impact, popular coupons

Non-Functional Requirements

Interviewers focus primarily on checkout-path latency and atomic usage counters. Because coupon validation sits on the critical checkout path, highlight atomic Redis operations with Lua scripting before race conditions are raised.

  • Low Latency: Coupon validation in < 50 ms (in checkout critical path)
  • Strong Consistency: Usage count must be accurate: no over-redemption
  • Scale: 100K+ active coupons, 10M+ redemptions/day
  • Availability: 99.99%: coupon failure blocks checkout
  • Fraud Resistant: Prevent coupon abuse (sharing, scripted redemption)

Capacity Estimations

Run this math before you size the rule cache. Active coupons and redemptions per day tell you Redis key count; checkout QPS drives validation latency budget.

MetricCalculationValue
Active couponsGiven100K
Coupon validation / secDerived from daily volume ÷ 86400 (+ peak factor)5K
Redemptions / dayGiven (assumption documented in value)10M
Bulk code generationGivenUp to 1M codes per campaign

Architecture Diagram

In the interview: decouple validation (read-only and low-latency) from redemption (atomic transactional write), ensuring shoppers receive instant feedback before confirming their purchase.

Coupon validation runs directly on the checkout critical path, executing rule lookup, cart eligibility evaluation, and atomic usage checks in under 20 milliseconds. The architecture decouples synchronous validation from asynchronous redemption analytics and campaign code generation. For related transactional checkout patterns, see the Shopping Cart System and the Flash Sale System. For distributed atomicity guarantees, see Distributed Transactions: 2PC vs Saga.

Loading...

Component Deep Dives

Coupon Rule Engine

Coupon Rule Engine and Eligibility Flow

Coupons require flexible, multi-attribute eligibility rules evaluated against real-time cart contents. Because validation executes on every cart update, the engine reads pre-cached rule JSON from Redis, verifies active validity windows, checks usage thresholds, and evaluates line-item constraints.

Coupon definition:
{
  "coupon_id": "SUMMER25",
  "type": "percentage",
  "value": 25,
  "conditions": {
    "min_cart_value": 100.00,
    "eligible_categories": ["electronics", "clothing"],
    "eligible_user_segments": ["premium_members"],
    "first_purchase_only": false,
    "max_uses_total": 10000,
    "max_uses_per_user": 1,
    "valid_from": "2026-03-01",
    "valid_to": "2026-03-31",
    "stackable": false,
    "excluded_skus": ["SKU-GIFT-CARD"]
  },
  "discount_cap": 50.00
}

Validation flow:
  1. Lookup coupon: Redis cache or PostgreSQL
  2. Check validity period: valid_from <= NOW <= valid_to
  3. Check total usage: Redis INCR coupon_usage:{coupon_id}
  4. Check per-user usage: Redis GET user_coupon:{user_id}:{coupon_id}
  5. Evaluate conditions against cart
  6. If all pass -> calculate discount and return

Discount calculation:
  percentage: discount = min(cart_subtotal * value/100, discount_cap)
  fixed: discount = min(value, cart_subtotal)
  bogo: discount = price of cheapest qualifying item
  tiered: spend $100 get $10 off, spend $200 get $30 off
  free_shipping: discount = shipping_cost

Stacking Rules

Coupon Stacking and Exclusivity Policies

Stacking policies balance promotional appeal against margin protection. The engine enforces stack groups and exclusivity flags to determine whether multiple discounts can combine on a single order.

User applies two coupons: "SUMMER25" (25% off) + "FREESHIP" (free shipping)

Stacking policies:
  1. No stacking: only one coupon per order (simplest, Amazon's approach)
  2. Category stacking: one coupon per category (percentage + shipping OK)
  3. Full stacking: any coupons can combine (rare, complex)

Implementation:
  Each coupon has: stackable = true/false, stack_group = "percentage" | "shipping" | "fixed"
  
  Rule: max one coupon per stack_group.
  "SUMMER25" (stack_group: "percentage") + "FREESHIP" (stack_group: "shipping") -> OK
  "SUMMER25" (percentage) + "FALL20" (percentage) -> REJECTED (same group)
  
  Application order matters:
    Option A: Apply percentage THEN fixed: $100 - 25% = $75 - $10 = $65
    Option B: Apply fixed THEN percentage: $100 - $10 = $90 - 25% = $67.50
    Standard: apply most beneficial order for the customer

Race Condition: Coupon Usage Limit

Atomic Usage Counters and Race Conditions

High-concurrency promotions face severe race conditions where hundreds of shoppers attempt to redeem the final remaining coupon slots simultaneously. Redis Lua scripts ensure atomic increment and rollback operations.

LUA
-- Coupon "FLASH50" has max_uses = 1000. 1,500 users try to use it simultaneously.
-- Without atomicity: 1,500 users all read count=999, all increment, all succeed, causing over-redemption.

local current = redis.call('INCR', 'coupon_usage:FLASH50')
if current > 1000 then
  redis.call('DECR', 'coupon_usage:FLASH50')
  return 0  -- exceeded limit
end
return 1  -- success reservation

-- If payment subsequently fails, DECR releases the reserved usage slot.
-- Per-user enforcement utilizes SETNX user_coupon:{user_id}:{coupon_id} with TTL.

Auto-Apply Promotions

Auto-Apply Promotion Evaluation

Auto-apply discounts evaluate on cart page rendering rather than manual coupon entry, selecting the optimal combination of site-wide, category, or order-tier offers without slowing down page load times.

Site-wide: "10% off everything this weekend"
Category: "Free shipping on electronics"
Cart-value: "$15 off orders over $100"

These promotions apply automatically without user code entry.

Implementation:
  1. On cart page load / checkout initiation:
     Fetch all active auto-apply promotions from Redis cache:
       auto_promos:{current_date} --> List of auto-apply coupon definitions
       TTL: 300 (refreshed from PostgreSQL)
  
  2. Evaluate each promotion against current cart:
     For each promo in auto_promos:
       if evaluate_conditions(promo.conditions, cart, user):
         applicable_promos.append(promo)
  
  3. Apply the BEST applicable promotion (or stack if policy allows):
     Sort by discount_amount DESC -> pick highest
     Or: apply all stackable auto-promos

  4. Show on cart page: "Promotion applied: $15 off orders over $100 ✓"

Edge case: auto-promo + manual coupon
  Policy options:
  a. Manual coupon replaces auto-promo
  b. Both stack (more customer-friendly)
  c. Apply whichever gives bigger discount (best of both)
  Standard: option (c) selecting the superior discount for the shopper.

API Design

Coupon Validation and Administration APIs

The platform exposes RESTful endpoints to validate discounts against cart line items, apply confirmed promotions at checkout, and initiate bulk code generation batches.

HTTP
POST /api/v1/coupons/validate
{ "coupon_code": "SUMMER25", "cart": { "items": [...], "subtotal": 150.00 }, "user_id": "..." }
--> 200 { "valid": true, "discount": 37.50, "type": "percentage", "message": "25% off (max $50)" }
 OR 200 { "valid": false, "reason": "Coupon expired" }

POST /api/v1/coupons/apply  (at checkout confirmation)
{ "coupon_code": "SUMMER25", "order_id": "order-uuid" }
--> 200 { "applied": true, "discount": 37.50 }

POST /api/v1/admin/coupons  (create coupon)
{ "code": "SUMMER25", "type": "percentage", "value": 25, "conditions": {...} }

POST /api/v1/admin/coupons/bulk-generate
{ "campaign_id": "camp-uuid", "prefix": "SUMMER", "count": 100000, "template": {...} }
--> 202 { "batch_id": "batch-uuid", "status": "generating" }

Common Error Responses

400 Bad Request: invalid input, missing required fields, or malformed JSON payload
401 Unauthorized: missing or invalid authentication token or API key
403 Forbidden: authenticated caller lacks required permissions for this resource
404 Not Found: requested resource ID does not exist
409 Conflict: duplicate write or version conflict, retry with a unique idempotency key
422 Unprocessable Entity: syntactically valid request failed semantic business validation
429 Too Many Requests: rate limit quota exceeded, client should honor Retry-After header
500 Internal Error: unexpected server failure, retry safely with an idempotency key
503 Service Unavailable: downstream dependency is unavailable or overloaded, retry with exponential backoff
402 Payment Required: account balance or payment method has insufficient funds
502 Bad Gateway: payment gateway provider timeout, poll transaction status endpoint

Data Model

PostgreSQL Schema: Coupons and Redemptions

PostgreSQL stores canonical coupon definitions, validity periods, stacking metadata, and individual customer redemption ledger records.

SQL
CREATE TABLE coupons (
    coupon_id VARCHAR(50) PRIMARY KEY, campaign_id UUID,
    type ENUM('percentage','fixed','bogo','free_shipping','tiered'),
    value DECIMAL(10,2), discount_cap DECIMAL(10,2),
    conditions JSONB, stackable BOOLEAN DEFAULT FALSE, stack_group VARCHAR(20),
    max_uses_total INT, max_uses_per_user INT DEFAULT 1,
    current_uses INT DEFAULT 0,
    valid_from TIMESTAMPTZ, valid_to TIMESTAMPTZ,
    active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE coupon_redemptions (
    redemption_id UUID PRIMARY KEY, coupon_id VARCHAR(50),
    user_id UUID, order_id UUID, discount_amount DECIMAL(10,2),
    redeemed_at TIMESTAMPTZ DEFAULT NOW(),
    INDEX idx_coupon (coupon_id), INDEX idx_user_coupon (user_id, coupon_id)
);

Redis: High-Velocity Caching and Atomic Counters

Redis caches hot coupon definitions to prevent database bottlenecks and enforces atomic usage counters and user redemption keys.

coupon:{code}                    --> JSON (coupon definition), TTL 3600
coupon_usage:{coupon_id}         --> INT (atomic INCR/DECR)
user_coupon:{user_id}:{coupon_id} --> "1", TTL 86400

Event Bus Design (Kafka)

Topic: coupon-events
  Partitions: 32
  Partition key: coupon_id
  Retention: 30 days

Producer: Coupon Service on validate/apply/redemption
  Event: { event_id, coupon_id, user_id, action: "validated"|"applied"|"redeemed", discount_amount, cart_value }

Consumer groups:
  1. analytics: ClickHouse redemption rates, revenue impact, and A/B test metrics
  2. fraud-detection: multi-account detection from identical IPs, velocity checks, and aggregator patterns
  3. bulk-generator: asynchronous 1M code generation jobs dispatched from admin campaigns

Sync path: validate/apply in Redis + PostgreSQL < 10ms; publish event fire-and-forget
DLQ: coupon-events-dlq

Fault Tolerance

ConcernSolution
Over-redemption
  • Redis atomic INCR
  • rollback on payment failure
Coupon cache staleTTL + invalidate on coupon update
Bulk generation failure
  • Batch insert
  • resume from last generated
  • idempotent
Abuse (shared codes)
  • Per-user limits
  • device fingerprinting
  • account age checks
Redis/PG divergence
  • Periodic reconciliation
  • PG is source of truth

Additional Considerations

Interview Walkthrough

  • 25-minute cut

    Skip arch50/arch75 depth unless staff.

    • Separate validate (read-only) from redeem (atomic write) (5 min)
    • Rule engine: min cart, category, expiry, usage limits (6 min)
    • Atomic redemption via Redis Lua or PostgreSQL txn (5 min)
    • Stacking rules: which coupons combine vs exclusive (5 min)
    • Per-user unique codes for targeted campaigns (4 min)
  • Separate validation (read-only and fast) from redemption (atomic write), because users require instant validation feedback before committing checkout.
  • Walk through the rule engine: eligibility checks (min cart, category, user segment, expiry, usage limits) evaluated in deterministic order.
  • Explain atomic redemption in Redis via Lua scripts or PostgreSQL transactions, incrementing used_count strictly when below max_uses.
  • Cover stacking rules: which coupons combine, which are mutually exclusive, and how to apply best-discount-first automatically.
  • Mention per-user unique codes for targeted marketing campaigns, preventing viral leaks across coupon aggregator sites.
  • Discuss abuse detection: velocity limits on invalid attempts, device fingerprint for multi-account farming, auto-disable on Reddit leaks.
  • Highlight the common pitfall of validating coupons solely during cart addition without re-verifying during final payment capture, which allows expired or exhausted promotions to slip through after checkout delays.

Engineering Trade-offs

Personalized Coupons vs Universal Codes

Promotional systems balance viral distribution velocity against margin loss from unauthorized code leaks, weighing universal shared codes against per-user unique keys.

Universal: "SAVE20" is usable by any shopper, facilitating viral sharing but proving difficult to control.
Unique per-user: "USR-A3F7K2" is uniquely generated for individual recipients, preventing unauthorized redemption leaks.
  Generation: prefix + cryptographically random characters. Storage: coupon_id to user_id mapping.
  Validation: verify that the redemption request originates from the assigned user ID.

Campaign deployment: generate 100K unique single-use codes distributed via email or SMS, eliminating scraping by coupon aggregator extensions.

Coupon Abuse Detection

Monitoring multi-account creation, abnormal redemption velocity, and excessive return patterns protects promotional budgets against automated exploitation.

Common abuse patterns:

1. Code sharing on social media and coupon aggregation sites:
   Detection: more than 1000 unique users redeem the same code within 1 hour
   Action: automatically disable the coupon and alert the marketing operations team

2. Multi-account abuse:
   Same person registers multiple accounts to redeem a first-purchase coupon repeatedly
   Detection: correlate device fingerprints, IP subnets, and payment tokens across accounts
   Action: flag coordinated accounts and revoke applied discount benefits

3. Automated redemption bots:
   Scripts guess variations of promotional codes
   Detection: more than 10 invalid attempts per minute from a single IP or session
   Action: trigger CAPTCHA challenges and rate limit to 5 coupon attempts per session

4. Post-coupon returns:
   Purchasing items with large discounts followed by returning items for full-value store credit
   Detection: track return rates per coupon; flag campaigns with return rates exceeding 30%

Coupon Analytics

Aggregating redemption time-series in ClickHouse measures incremental revenue, customer acquisition cost, and average order value lift.

Key metrics per coupon (ClickHouse queries):

1. Redemption rate = redemptions / unique_views_of_coupon_field
2. Revenue impact = total_order_value_with_coupon - total_discount_given
3. Incremental revenue = orders_with_coupon - estimated_orders_without_coupon
4. Average order value with coupon vs without
5. Customer acquisition cost (for first-purchase coupons):
   CAC = total_discount / new_customers_acquired

💬Review

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...