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)
| Question | Why it matters |
|---|---|
| Validate-only at cart preview, or reserve redemption at apply time? |
|
| 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.
| Metric | Calculation | Value |
|---|---|---|
| Active coupons | Given | 100K |
| Coupon validation / sec | Derived from daily volume ÷ 86400 (+ peak factor) | 5K |
| Redemptions / day | Given (assumption documented in value) | 10M |
| Bulk code generation | Given | Up 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.
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_costStacking 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 customerRace 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.
-- 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.
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.
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 86400Event 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-dlqFault Tolerance
| Concern | Solution |
|---|---|
| Over-redemption |
|
| Coupon cache stale | TTL + invalidate on coupon update |
| Bulk generation failure |
|
| Abuse (shared codes) |
|
| Redis/PG divergence |
|
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
How helpful was this walkthrough?
Click a star to rate. We actively use this feedback to refine and update our system design content.
Discussion
Share your thoughts, ask questions, or help others.