Design problems
E-commerce & marketplacesMediumSDE-2 30 min

Design a coupon and offers engine

Asked at
CREDFlipkartMyntraPaytmSwiggyZomato

The interviewer asks

“Design the coupons and offers engine for an e-commerce or food delivery app. Marketing creates offers like "FLAT100 on orders above Rs 499 for new users" or "10% off with HDFC cards, max Rs 150"; at checkout the system must apply the best valid offers correctly and enforce usage limits and budgets.”

What a strong answer looks like

A strong answer separates three concerns. First, offer definition: a rules model (conditions on user, cart, items, payment method, time, city) plus actions (flat, percent with cap, free delivery, buy-X-get-Y), stored in a database and compiled into an in-memory rule index on every evaluation node. Second, evaluation: a stateless, fast service that takes a cart context and returns applicable offers and the best combination under stacking rules, computing the discount with integer paise and per-line apportioning. Third, redemption accounting: per-user and global usage limits and campaign budgets, which need atomic counters with reservation at order creation and confirm/release on payment, idempotent on order_id. Strong candidates handle the race on 'first 10,000 users', budget burn during sales, fraud (multi-accounting, coupon leaks), and keep the price shown on the cart page consistent with what checkout charges.

Concepts to mention

Offer = conditions + benefit (typed rule tree)In-memory compiled rule index on stateless evaluatorsAtomic check-and-increment for limits and budgetsReserve, confirm, release redemption lifecycleIdempotency on order_idInteger paise and line-level apportioningStacking groups and best-combination selectionFraud controls: multi-account, code leaks, burn-rate alertsRedemptions ledger as source of truth

1 Requirements

Functional

  • Marketing creates offers with eligibility rules (new user, city, min cart value, categories, payment method, time window) and benefits (flat, percent with cap, free delivery, BOGO, cashback)
  • Cart page shows applicable offers and auto-applies the best one; users can also type a code
  • Enforce limits: per-user uses, total redemptions, campaign budget in rupees
  • Stacking rules: e.g. one coupon + one bank offer, never two coupons
  • Unique single-use codes (e.g. 1M codes for a partner campaign) and public codes
  • Reverse redemptions on cancellation or refund

Non-functional

  • Evaluation p99 under 50 ms (it sits in cart and checkout paths)
  • ~20k evaluations/s at peak (every cart view and change), ~2k redemptions/s
  • Usage limits and budgets never exceeded by more than a tiny tolerance
  • Discount shown in cart equals discount charged at checkout, or the user is told why
  • Offer changes go live within ~1 minute; full audit of who changed what

Out of scope (say it out loud)

  • Dynamic pricing of products
  • Loyalty points programme
  • Settlement of bank-funded offers with the bank (only record funding source)

2 Estimates

Active offers~5-20kPlatform-wide, city-level, restaurant/brand-funded and bank offers; plus millions of unique codes stored separately
Evaluation QPS~3-5k/s avg, ~20k/s peak~3M orders/day x ~10 cart views or changes per order ~ 30M/day ~ 350/s from carts; offer badges on menus and listing pages add ~10x -> ~3.5k/s; dinner and sale peaks ~5x
Redemptions~17/s avg, ~2k/s peak~3M orders/day, ~50% with an offer ~ 1.5M/day ~ 17/s; a sale start concentrates many in a minute
Rule index size~20-50 MB in memory20k offers x ~2 KB compiled rules = 40 MB; replicated to every evaluator pod
Usage counters~50M keysPer-user-per-offer counters only for users who used an offer: 1.5M/day x 30-day retention window
Unique codes~50M rowsPartner campaigns issue 1-10M single-use codes each; ~50 bytes per row ~ 2.5 GB

3 API

EndpointWhat it does
POST/v1/offers/evaluateInput: user, cart lines, address, payment method (optional), entered code. Output: applicable offers, best combination, per-line discounts, reasons for ineligible ones
POST/v1/redemptionsAt order creation: reserve the chosen offers for order_id (idempotent); checks limits and budgets atomically
POST/v1/redemptions/{order_id}/confirmPayment success: make redemption final
POST/v1/redemptions/{order_id}/releasePayment failed, order cancelled or fully refunded: return the use and budget
POST/admin/v1/offersCreate or edit an offer (draft -> approved -> live), with validation and audit
POST/admin/v1/offers/{id}/codesGenerate a batch of unique single-use codes

4 Data model

offersPostgres
offer_idcode (nullable for auto-apply)typeconditions JSON (rule tree)benefit JSONprioritystack_groupper_user_limittotal_limitbudget_paisefunded_by (platform/brand/bank)start_atend_atstatusversion

Source of truth, low write rate. Changes publish an event so evaluators reload. Versioned; redemptions record the offer version used.

offer_index (compiled)In-memory on each evaluator, built from offers
maps: city -> offerscategory -> offerspayment_method -> offerscode -> offer

Pre-filtering by indexed dimensions avoids evaluating all 20k rules per request.

usage_countersRedis (cluster) backed by the redemptions table
offer:{id}:usedoffer:{id}:budget_spent_paiseuser:{uid}:offer:{id}:used

Atomic Lua check-and-increment for limits; rebuilt from the redemptions table if lost.

redemptionsPostgres (sharded by user_id at scale)
redemption_idorder_id + offer_id (unique)user_iddiscount_paisestatus (RESERVED, CONFIRMED, RELEASED)offer_versioncreated_at

Durable ledger of every use; source for reconciliation, finance reports and brand/bank billing.

unique_codesPostgres or DynamoDB
code (PK, hashed)offer_idstatus (UNUSED, RESERVED, USED)user_idorder_id

Single-use claim via conditional update WHERE status = 'UNUSED'.

user_segmentsFeature store / Redis
user_id -> {is_new_userorder_countsegments[]fraud_risk}

Precomputed segments for targeting; evaluator reads one key per request.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAcart, checkoutevaluate cartuser segmentsremaining limitsreserve on orderatomic check+incrredemption rowoffer changedreload rulespaid / cancelledCustomer appMarketing consoleAPI gatewayOffer evaluatorCart / checkoutRedemption serviceOffer admin serviceKafka (offer + orderevents)Budget alerts +reconcilerUser segments storeRedis usage countersOffers + redemptionsDB
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Evaluate offers on the cart page

  1. 1Cart service calls POST /v1/offers/evaluate with user_id, lines (sku, qty, price in paise, category, brand), city, delivery fee and any typed code.
  2. 2Evaluator loads the user's segments (new user, order count, risk score) from one key-value read.
  3. 3It narrows candidates using its in-memory index (offers live now, for this city, matching categories or payment methods): ~20-50 of 20k.
  4. 4Each candidate's rule tree is evaluated against the cart context; ineligible offers keep a reason ('Add Rs 120 more to unlock').
  5. 5It reads remaining global and per-user usage from Redis to hide exhausted offers (advisory only, not a reservation).
  6. 6It computes discounts in integer paise, applies caps, apportions the discount to lines (needed for GST, partial refunds and brand funding), and picks the best valid combination under stacking rules.
  7. 7Response returns the auto-applied best combination plus the list of other offers; total time ~10-20 ms.

Redeem at order creation

  1. 1Checkout re-evaluates the cart with the chosen offers (prices and rules may have changed) and then calls POST /v1/redemptions with order_id.
  2. 2Redemption service runs a Redis Lua script per offer: check per-user used < limit, global used < total_limit, budget_spent + discount <= budget; if all pass, increment all three atomically.
  3. 3It inserts redemption rows (RESERVED) with unique (order_id, offer_id); a retry with the same order_id returns the existing result without touching counters again.
  4. 4If any offer fails its check, it rolls back the counters it already incremented for this order and returns which offer is no longer valid; checkout shows the new price before charging.
  5. 5Payment success event -> CONFIRMED. Payment failure, timeout (sweeper after 30 min) or cancellation -> RELEASED and counters decremented.
  6. 6Reconciler compares Redis counters with the redemptions table every few minutes and fixes drift; budget alerts notify marketing at 80% and auto-pause at 100%.

7 Deep dives

Rules model and fast evaluation

Avoid hard-coding offers in if-else code; marketing creates new ones daily. Model an offer as conditions plus benefit. Conditions are a small tree of typed predicates: user (is_new, segment in [...], order_count < 3), cart (subtotal >= 49900 paise, contains category 'pizza', brand = X), context (city in [...], time between 22:00-02:00, platform = android), payment (method = card, bin range in HDFC list). Benefits: flat, percent with max cap, free delivery, BOGO on the cheapest item, cashback credited later. Keep the predicate language limited and typed rather than letting marketing write arbitrary scripts; it is safer, can be validated at creation and can be compiled. Evaluation speed comes from indexing: build inverted maps from dimensions (city, category, payment method, code) to offers so each request evaluates only tens of rules. Evaluators are stateless and hold the compiled index in memory; on an OfferChanged event they rebuild and swap atomically. Store the offer version in each redemption so later disputes see the exact rule that was applied.

Usage limits and budgets under concurrency

'First 10,000 orders' or 'Rs 50 lakh budget' are shared counters hit by many checkouts at once. Read-check-write in the application races and overshoots. Use an atomic check-and-increment: a Redis Lua script that checks per-user count, global count and budget, then increments all, or nothing. Redis gives ~100k ops/s per shard; put all keys for one offer in one hash slot using a hash tag ({offer:123}) so the script can touch them, and keep per-user keys separate or in the same slot with the offer tag. Reserve at order creation, confirm on payment, release on failure, like inventory. Because Redis can lose recent writes on failover, the redemptions table is the durable truth: on recovery, rebuild counters as the sum of RESERVED + CONFIRMED rows. For very hot offers, you can pre-allocate budget chunks to evaluator pods (e.g. 1,000 uses at a time) to reduce contention, accepting a small overshoot or undershoot at the end. Tell the interviewer what tolerance the business accepts: overspending 0.1% of a budget is usually fine, double-using a single-use code is not.

Choosing the best combination and splitting the discount

Users expect the app to pick the best deal. With stacking groups (one coupon, one bank offer, one free-delivery offer), the search space is small: for each group pick at most one offer, so brute force over a few candidates per group is fine. Order matters: a percent offer applied after a flat one gives a different total, so define a fixed order (item-level offers, then cart-level coupons, then payment offers on the remaining amount) and stick to it. Always use integer paise and round in one direction at a defined step to avoid one-paisa mismatches between cart, invoice and refund. Apportion each discount across eligible lines in proportion to their value, with the remainder on the largest line; this matters for GST invoices (tax is on the discounted value), partial cancellations (refund the line's net price) and billing brands for brand-funded offers. Cashback offers are not a price reduction; they create a wallet credit after delivery, so they belong in a separate post-order flow.

Abuse and leaks

Coupons are money, so they attract fraud. Common patterns: one person creating many accounts for 'new user' offers (detect with device ID, phone, payment instrument and address fingerprints; tie 'new user' to device and instrument, not just account); public leaks of codes meant for a partner (use unique single-use codes or bind codes to user IDs); bots brute-forcing short codes (rate limit code attempts per user and IP, use long random codes); order-and-cancel loops to farm cashback (release only after delivery, cap per device); and internal mistakes such as a missing max cap on a 50% offer during a sale. Guardrails: mandatory max cap and budget for percent offers, a second approver for offers above a value threshold, budget burn-rate alerts (spending 30% of a day's budget in 10 minutes means something is wrong), and a kill switch that pauses an offer within seconds through the event pipeline.

8 Trade-offs

Typed rule tree vs general rules engine (Drools) vs scripts

A typed predicate tree is fast, safe and easy to validate and explain. A general rules engine is flexible but heavier and harder to reason about. Arbitrary scripts are dangerous. Start with a typed tree and add predicate types as needed.

Reserve at order creation vs at payment

Reserving at order creation guarantees the user gets the discount they saw when paying, at the cost of holding uses for unpaid orders (released after a timeout). Reserving after payment risks charging the full price or honouring an exhausted offer.

Redis counters vs database counters

DB counters with conditional updates are durable but hot rows limit throughput. Redis is fast but needs rebuild-from-ledger on failover. Use Redis for hot campaigns, with the redemptions table as truth.

Auto-apply best offer vs user choice

Auto-applying improves conversion and fairness; letting users choose matters when offers differ in type (cashback vs instant). Auto-apply the best instant discount and show others.

9 Bottlenecks

Sale-start burst on one offer

Thousands of redemptions per second on one offer's counters. Hash-tag keys to one slot and keep the Lua script tiny; if needed, pre-allocate chunks to pods; move evaluation-time limit checks to cached values to avoid read load on the same keys.

Evaluation on browse surfaces

Showing offer badges on every listing card multiplies calls. Precompute non-personal badges ('20% off') into the catalogue/search index and only evaluate personal offers in the cart.

10 Failure handling

Redis down

Evaluation still works with rules only (limits treated as available). Redemption checks fall back to conditional updates on the database for limited offers, at lower throughput, or pause limited offers while unlimited ones keep working.

Payment event lost

Reserved redemptions without confirm or release are swept after 30 minutes by checking the order state with the order service, so uses are not leaked forever.

Misconfigured offer goes live

Burn-rate alerts fire within minutes; the kill switch pauses the offer through an event to all evaluators. Orders already placed are honoured or handled per policy; the audit log shows who approved it.

11 What impresses

  • Separating evaluation (fast, stateless) from redemption (stateful, atomic)
  • Explaining why the cart and checkout totals can differ and how the user is told
  • Apportioning discounts per line for GST and partial refunds
  • Rebuilding Redis counters from the ledger after failover
  • Budget burn-rate alerts and an instant kill switch
  • Defining 'new user' by device and payment instrument, not account

12 Mistakes

  • Hard-coding offers in application code
  • Checking limits with read-then-write and overshooting the budget
  • Using floating point for money
  • Incrementing usage on cart view or never releasing it on failure
  • Evaluating all offers for every request with no index
  • Ignoring fraud and multi-accounting

13 Follow-ups

Q1How would you A/B test two offers against each other?
Q2How do you support 'Rs 100 off on your 3rd order this month'?
Q3How do you handle a partial cancellation of one item from a discounted order?
Q4How do bank offers that depend on the card BIN work before the user enters the card?
Q5How would you let restaurants or brands create and fund their own offers safely?
Q6How do you make offer badges on listing pages consistent with checkout?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free