Coupon and Discount System Low-Level Design

Requirements

  • Create and manage discount coupons (percentage, fixed amount, free shipping)
  • Apply coupon to a cart: validate, compute discount, prevent reuse
  • Support: single-use codes, multi-use codes with a global limit, user-specific codes
  • Prevent race conditions: same coupon applied concurrently by many users
  • 100M users, 10K coupon redemptions/second during flash sales

Data Model

Coupon(coupon_id, code VARCHAR UNIQUE, type ENUM(PERCENT,FIXED,FREESHIP),
       value DECIMAL, min_order_amount, max_uses INT, uses_count INT,
       max_uses_per_user INT, valid_from, valid_until, status ENUM(ACTIVE,EXPIRED,DISABLED),
       applicable_product_ids[], applicable_category_ids[])

CouponRedemption(redemption_id, coupon_id, user_id, order_id,
                 discount_amount, redeemed_at)

Coupon Validation

On apply coupon (before order placement):

  1. Look up coupon by code (Redis cache: key=coupon:{code}, TTL=1min)
  2. Check status=ACTIVE and valid_from <= NOW() <= valid_until
  3. Check cart total >= min_order_amount
  4. Check uses_count < max_uses (global limit not exhausted)
  5. Check user redemption count < max_uses_per_user (query CouponRedemption)
  6. Check applicable products/categories match cart items

Return: discount amount (if PERCENT: order_total * value/100, if FIXED: min(value, order_total)).

Preventing Race Conditions on Global Limit

Problem: 10K users try to redeem the last spot of a coupon simultaneously. Without locking, uses_count could exceed max_uses.

Solution: Atomic Redis decrement. Before DB insert:

# Initialize Redis counter on coupon creation:
SET coupon_remaining:{coupon_id} {max_uses - current_uses}

# On redemption attempt:
remaining = DECR coupon_remaining:{coupon_id}
if remaining < 0:
    INCR coupon_remaining:{coupon_id}  # undo
    return CouponExhausted
# Proceed with DB insert

Redis DECR is atomic — no two requests can simultaneously see remaining >= 1 and both decrement to 0. After the Redis check, insert CouponRedemption and increment uses_count in DB. If the DB insert fails (duplicate key on user+coupon), INCR to undo the Redis decrement.

Per-User Rate Limiting

Check max_uses_per_user: SELECT COUNT(*) FROM CouponRedemption WHERE coupon_id=X AND user_id=Y. Cache result (key=user_coupon_uses:{user_id}:{coupon_id}, TTL=60s). For max_uses_per_user=1 (single-use per user): use a UNIQUE constraint on (coupon_id, user_id) in CouponRedemption. Concurrent redemptions from the same user: the second INSERT fails with a duplicate key error, safely returning AlreadyUsed.

Coupon Redemption Flow

  1. Validate coupon (all checks above)
  2. Decrement Redis remaining counter
  3. Place order with discount applied
  4. INSERT CouponRedemption record
  5. UPDATE Coupon SET uses_count = uses_count + 1 (async, for reporting)

Step 3 and 4 are in a database transaction. If the transaction fails, undo step 2 (INCR Redis counter). For steps 2-4, use a saga pattern with compensation.

Bulk Coupon Generation

For marketing campaigns: generate 1M unique single-use codes. Use a CSPRNG (cryptographically secure random) to generate 8-12 character alphanumeric codes. Check for collisions in bulk: INSERT … ON CONFLICT DO NOTHING, retry for conflicts. Store in DB with status=ACTIVE; import to Redis in batch for fast lookup. Coupon codes are case-insensitive: normalize to uppercase on input.

Key Design Decisions

  • Redis atomic DECR prevents over-redemption without DB-level locking
  • UNIQUE constraint on (coupon_id, user_id) handles concurrent per-user limit checks
  • Cache coupon details in Redis (TTL=1min) to handle high lookup volume during flash sales
  • Soft delete coupon codes (status=DISABLED) — never hard delete for audit trail


{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you prevent a coupon from being redeemed more than its global limit?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use a Redis atomic decrement as the authoritative gate. On coupon creation: SET coupon_remaining:{coupon_id} {max_uses}. On redemption attempt: remaining = DECR coupon_remaining:{coupon_id}. If remaining < 0: INCR to undo and return CouponExhausted. If remaining >= 0: proceed with the DB insert. Redis DECR is single-threaded and atomic — no two concurrent requests can both see remaining >= 1 and both succeed when only 1 spot is left. After the Redis gate, insert the CouponRedemption record in DB. If the DB insert fails for any reason: INCR coupon_remaining to return the slot. Periodic reconciliation: compare Redis counter with COUNT(*) in CouponRedemption table hourly; reset if they diverge.”}},{“@type”:”Question”,”name”:”How do you enforce per-user coupon limits (max 1 use per user)?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Add a UNIQUE constraint on (coupon_id, user_id) in the CouponRedemption table. When the user redeems: INSERT INTO CouponRedemption (coupon_id, user_id, …) values … ON CONFLICT DO NOTHING. Check rows_affected: if 0, the user already redeemed this coupon. For concurrent requests from the same user (double-click, retry): the UNIQUE constraint makes one succeed and one get rows_affected=0. For max_uses_per_user > 1: SELECT COUNT(*) FROM CouponRedemption WHERE coupon_id=X AND user_id=Y before inserting. Cache this count in Redis (key=user_coupon:{user_id}:{coupon_id}, TTL=60s) to avoid per-request DB queries during flash sales when many users check the same code.”}},{“@type”:”Question”,”name”:”How do you handle coupon lookup performance under flash sale traffic?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Cache coupon details in Redis on first lookup: key=coupon:{code}, value=JSON of coupon fields (type, value, max_uses, min_order_amount, valid_until, status), TTL=60s. On cache miss: fetch from DB and populate. Under flash sale traffic (thousands of users checking the same coupon code per second): the DB is hit only once per 60 seconds (one cache miss), then all subsequent requests are served from Redis (~1ms). Invalidate the cache on coupon status change (disable, update). For the remaining counter: Redis DECR is the authoritative source — the DB uses_count is updated asynchronously via batch job for reporting.”}},{“@type”:”Question”,”name”:”How do you generate millions of unique coupon codes for a marketing campaign?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use a cryptographically secure random number generator (os.urandom, secrets module) to generate 8-12 character alphanumeric codes. Encode as uppercase to avoid ambiguity (remove I, O, 0, 1). Each 8-character code from a 30-character alphabet has 30^8 = 656 billion possible values — collision probability is negligible for 1M codes. Bulk insert: generate codes in batches of 10,000, INSERT INTO coupons … ON CONFLICT (code) DO NOTHING, check rows_affected for collisions (rare), regenerate collisions. Populate Redis: after DB insert, warm the cache. Rate limit activation: set valid_from to the campaign start date so codes can be pre-generated but not used early. Normalize codes to uppercase on input to avoid case sensitivity issues.”}},{“@type”:”Question”,”name”:”How do you apply multiple coupons or stacking discounts?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Most systems allow only one coupon per order (simplest). For stacking: define a CouponStack policy on each coupon: EXCLUSIVE (cannot combine with others), STACKABLE (can combine). Validation: if user applies two coupons, check that both are STACKABLE. Apply discounts in a defined order: (1) percentage discounts first, (2) fixed discounts on the reduced amount. Cap: total discount cannot exceed order total. Implementation: store an array of applied_coupon_ids on the Order. Apply discounts sequentially in the defined priority order. For cart-level vs item-level coupons: item-level discounts are applied to specific items before cart-level discounts. Store the discount breakdown on each OrderItem for receipt clarity.”}}]}

Shopify system design interviews cover coupon and discount systems. See common questions for Shopify interview: coupon and discount system design.

Amazon system design covers coupon and promotions at scale. Review design patterns for Amazon interview: coupon and promotions system design.

Stripe system design covers discount codes and payment flows. See design patterns for Stripe interview: discount and payment system design.

Scroll to Top