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):
- Look up coupon by code (Redis cache: key=coupon:{code}, TTL=1min)
- Check status=ACTIVE and valid_from <= NOW() <= valid_until
- Check cart total >= min_order_amount
- Check uses_count < max_uses (global limit not exhausted)
- Check user redemption count < max_uses_per_user (query CouponRedemption)
- 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
- Validate coupon (all checks above)
- Decrement Redis remaining counter
- Place order with discount applied
- INSERT CouponRedemption record
- 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.