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
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.