SYSTEM_DESIGN
System Design: Coupon & Discount System
System design of a coupon and discount system supporting percentage-off, fixed-amount, BOGO, and tiered discounts with real-time validation and abuse prevention.
Requirements
Functional Requirements:
- Create and manage coupons: percentage-off, fixed-amount, BOGO, free shipping, tiered discounts
- Coupon constraints: minimum order value, applicable categories/products, user eligibility, date range
- Real-time coupon validation and discount calculation at cart/checkout
- Usage limits: per-coupon total limit and per-user limit
- Stackable vs. non-stackable coupon rules
- Coupon code generation (unique codes, bulk generation for campaigns)
Non-Functional Requirements:
- Validate coupons in under 10ms (inline with cart pricing)
- Support 1M active coupons with 100M redemptions/month
- Zero over-redemption: a coupon limited to 1,000 uses must not be redeemed 1,001 times
- 99.99% availability for validation; coupon application must never block checkout
- Audit trail for all coupon creations, modifications, and redemptions
Scale Estimation
1M active coupons × 1KB metadata = 1GB — fits entirely in Redis. Validation requests: 20M cart views/day with coupon applied = 231 QPS average, spiking to 5,000 QPS during promotional events. Redemption writes: 3M redemptions/day = 35 writes/sec. Coupon code lookups: hash-based O(1) in Redis. Total redemption history: 100M records/month × 200 bytes = 20GB/month in PostgreSQL.
High-Level Architecture
The Coupon Service uses Redis for real-time validation and PostgreSQL for durable storage and analytics. The validation flow: Cart Service calls Coupon Service with cart contents + coupon code → Coupon Service looks up coupon rules from Redis → evaluates eligibility (user limits, date range, minimum order, product applicability) → calculates discount amount → returns discount breakdown. All validation logic runs in-memory against cached coupon rules — no database calls in the hot path.
Redemption (when an order is confirmed) follows a different path: Order Service emits order.confirmed event → Coupon Redemption Consumer increments the usage counter in Redis atomically (INCR with Lua script that checks limit) → writes redemption record to PostgreSQL. If the Redis counter exceeds the coupon's limit, the Lua script returns failure and the redemption is rejected — the order proceeds without the discount (a compensating action notifies the customer).
Coupon management (CRUD operations by marketing teams) writes to PostgreSQL first, then updates the Redis cache via a CDC pipeline. A Coupon Admin UI allows creating campaigns with bulk code generation (e.g., 100,000 unique codes for an email campaign).
Core Components
Coupon Rule Engine
The rule engine evaluates coupon eligibility using a chain-of-responsibility pattern. Each rule is a predicate: DateRangeRule (is current time within validity period?), MinOrderRule (does cart total exceed minimum?), ProductApplicabilityRule (are eligible products in the cart?), UserEligibilityRule (is the user in the target segment? first-time buyer, loyalty tier, etc.), UsageLimitRule (has the per-user or per-coupon limit been reached?). Rules are evaluated in order of cheapest-first to fail fast. The discount calculation supports: percentage_off (capped at max_discount_amount), fixed_amount, buy_x_get_y, and tiered (spend $100 get 10%, spend $200 get 15%).
Atomic Usage Counter
Preventing over-redemption uses a Redis Lua script for atomic check-and-increment: local current = tonumber(redis.call('get', counter_key)) or 0; if current >= limit then return -1 end; redis.call('incr', counter_key); return current + 1. The per-user counter uses a separate key: coupon:{code}:user:{user_id} with the same Lua script. Both counters are checked during validation (to show the user if the coupon is still valid) and again during redemption (to prevent races between validation and order confirmation).
Bulk Code Generator
Marketing campaigns require generating 100K+ unique coupon codes. The generator creates codes using a prefix + random alphanumeric suffix (e.g., SUMMER-A3B7K9). Codes are generated in batches of 10,000, checked for uniqueness against a Bloom filter (1M capacity, 0.01% false positive rate), and inserted into PostgreSQL in bulk. The Bloom filter avoids expensive uniqueness queries during generation. Generated codes are loaded into Redis as a hash set for O(1) lookup during validation.
Database Design
PostgreSQL schema: coupons table (coupon_id UUID, code VARCHAR UNIQUE, type ENUM('percentage', 'fixed', 'bogo', 'tiered', 'free_shipping'), value DECIMAL, max_discount DECIMAL, min_order_value DECIMAL, applicable_products JSONB, applicable_categories JSONB, user_segment JSONB, usage_limit INT, per_user_limit INT, start_date TIMESTAMP, end_date TIMESTAMP, stackable BOOLEAN, status ENUM('active', 'paused', 'expired'), created_by, created_at). redemptions table (redemption_id, coupon_id FK, user_id, order_id, discount_amount, redeemed_at).
Redis data model: Hash coupon:{code} → full coupon rule document (JSON). String coupon_count:{code} → total redemption count. String coupon_count:{code}:user:{user_id} → per-user redemption count. Set coupon_codes → all active coupon codes (for existence check).
API Design
POST /api/v1/coupons/validate— Validate a coupon against cart; body contains coupon_code, cart_items, user_id; returns eligibility status and discount breakdownPOST /api/v1/coupons— Create a coupon; body contains type, value, constraints, limits; returns coupon_idPOST /api/v1/coupons/bulk-generate— Generate batch of unique codes; body contains prefix, count, campaign_id; returns job_idGET /api/v1/coupons/{coupon_id}/analytics— Redemption analytics: total uses, revenue impact, top redeemers
Scaling & Bottlenecks
During large promotional events (site-wide 20% off), the validation endpoint sees 5,000 QPS — all hitting the same coupon record in Redis. Since Redis hash reads are O(1) and the entire coupon catalog fits in memory, a single Redis instance handles this load comfortably. The bottleneck shifts to the redemption counter: 5,000 concurrent INCR operations on the same key. Redis serializes these at ~100K ops/sec, well within capacity. For extreme cases (single-code coupons with millions of applicants), the counter is sharded across 10 keys with random distribution.
The dual-check pattern (validate at cart view + re-validate at redemption) introduces a race condition window: a coupon may be valid during cart view but exhausted by the time the order is confirmed. The system handles this gracefully: if redemption fails, the order still succeeds but without the discount, and the customer is notified with an apology credit. This prevents coupon exhaustion from blocking revenue-generating orders.
Key Trade-offs
- Redis for validation over database queries: Sub-millisecond validation at the cost of maintaining a synchronized cache — CDC pipeline handles the sync with eventual consistency (coupon creation visible in <2 seconds)
- Lua-scripted atomic counters over database transactions: Prevents over-redemption without the overhead of distributed locks, but counter state is in Redis (volatile) — the PostgreSQL redemption log serves as the source of truth for reconciliation
- Graceful degradation on coupon exhaustion: Allowing the order to proceed without the discount preserves revenue, but may frustrate customers — the apology credit mechanism maintains goodwill
- Bloom filter for code uniqueness: Eliminates expensive DB queries during bulk generation, but the 0.01% false positive rate means ~10 codes per 100K batch may be unnecessarily regenerated
GO DEEPER
Master this topic in our 12-week cohort
Our Advanced System Design cohort covers this and 11 other deep-dive topics with live sessions, assignments, and expert feedback.