Design problems
E-commerce & marketplacesHardSDE-2 35 min

Design a flash sale (Big Billion Days)

Asked at
AmazonBookMyShowFlipkartMeeshoMyntraNykaa

The interviewer asks

“Design a flash sale: at 12:00 noon, 10,000 units of a new phone go on sale at a big discount. Millions of users will try to buy in the first few seconds. Make sure we never sell more than 10,000, the site stays up, and the sale is fair.”

What a strong answer looks like

A strong answer recognises that this is an admission-control problem, not a database-scaling problem: millions of requests chase 10,000 units, so 99% of users must be turned away cheaply and fairly before they touch the order system. Static content is pre-pushed to the CDN; a virtual waiting room issues signed, rate-limited tokens; stock is held in Redis as an atomic counter (a Lua script or DECR guarded against going negative) so each successful decrement grants a time-limited claim; only claim holders enter the normal checkout, which creates orders asynchronously through a queue at a rate the order DB can handle. Unpaid claims expire and return units to the pool. Bots are fought with per-user limits, device fingerprinting and pre-registration. The candidate should give numbers, pre-scale everything before noon, and show how the system keeps the 10,000 limit exact even when components fail.

Concepts to mention

Admission control and virtual waiting roomCDN for static sale pages and sold-out flagAtomic Redis decrement via LuaStock buckets to spread hot keysClaims with TTL and release of unpaid unitsQueue-based order creation (load levelling)Idempotency on claim_idBot defence and per-user limitsPre-scaling and isolation from normal traffic

1 Requirements

Functional

  • Sale starts at an exact time; before that, the 'Buy' button is disabled for everyone
  • Sell at most N units (e.g. 10,000) of the sale SKU; never oversell
  • Limit each user to 1 unit
  • Users who get a unit have ~10 minutes to pay; unpaid units go back to the pool
  • Users who miss out see 'sold out' quickly instead of hanging
  • Normal browsing and other orders keep working during the sale

Non-functional

  • Absorb ~5M users and ~1M requests/s in the first minute at the edge
  • Purchase-attempt API p99 under 500 ms; no timeouts piling up
  • Exact inventory: sold <= N under all failures
  • Fairness: roughly first-come-first-served; bots should not win most units
  • Order system load stays within its normal capacity (~2-5k orders/s)

Out of scope (say it out loud)

  • Payment gateway internals
  • Warehouse picking and shipping
  • Pricing and catalogue tools for setting up the sale

2 Estimates

Users at noon~5M concurrentPre-registrations and push notifications bring millions to the app at the same second for a hyped phone launch
Request rate at edge~1M req/s peak5M users each tapping/refreshing ~1 time in the first 5 seconds plus app polling; most are static or cached
Buy attempts reaching origin~200k/s for ~10 sWaiting room admits only users with valid tokens; still a large burst on the purchase API
Successful claims10,000 total, sold out in secondsWith 200k attempts/s, the counter reaches zero in well under a second of admitted traffic; after that every request is a cheap 'sold out'
Redis counter throughput~100k+ ops/s per shardA single Redis primary handles ~100k-200k simple ops/s; a Lua decrement is a few µs. Split stock into ~10 buckets across shards for headroom
Orders to create10,000 over ~1-2 minutesDrained from a queue at ~200/s, well within the order DB's capacity

3 API

EndpointWhat it does
GET/v1/sales/{sale_id}Sale page data: start time, server time, status (upcoming / live / sold out); cached at CDN for 1-2 s
POST/v1/sales/{sale_id}/queueEnter the waiting room; returns a signed queue token with position or an admission token
POST/v1/sales/{sale_id}/claimWith an admission token, try to claim one unit; returns claim_id with expiry or SOLD_OUT
POST/v1/checkoutNormal checkout with claim_id attached; creates the order and payment session
GET/v1/sales/{sale_id}/claim/{claim_id}Poll claim and order status

4 Data model

sale_configPostgres (source) + pushed to Redis and CDN
sale_idsku_idstart_atend_attotal_unitsper_user_limitprice

Loaded into every edge/service cache before the sale; read-only during the sale.

stock_counterRedis (replicated, AOF every second)
sale:{id}:stock:{bucket} -> remaining integer

Split into buckets (e.g. 10 x 1,000) on different shards to spread load. Decrement with a Lua script that checks > 0.

user_claimsRedis set + durable log
sale:{id}:users -> set of user_id; claim:{claim_id} -> {user_idbucketexpires_atstatus}

SADD returning 0 means the user already claimed; enforced in the same Lua script as the decrement.

claim_eventsKafka
claim_iduser_idsale_idbucketts

Durable record of every grant; consumers create PENDING orders and are the audit trail for reconciliation.

ordersOrders DB (sharded SQL)
order_iduser_idclaim_id (unique)status...

Unique claim_id prevents two orders for one claim.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALsale page, timerjoin queueadmitted tokensPOST /claimLua: decr + user checkclaim grantedPENDING orderpay with claimreturn unpaid unitscancel expiredMillions of clientsCDN (static sale page)Waiting room + botfilterAPI gateway (ratelimit)Claim serviceCheckout serviceKafka: claimsClaim expiry sweeperOrder creator workersRedis stock bucketsOrders DBPayment gateway
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Claim a unit at 12:00:00

  1. 1Minutes before noon, the app loads the sale page from the CDN; the countdown uses server time from the sale endpoint, not the phone clock.
  2. 2At noon the app calls the waiting room. It checks the user's login token, device fingerprint and per-user/IP rate limits, then either admits the user (signed admission token valid for 2 minutes) or returns a queue position. Admission rate is set to what the claim service can handle (~50k/s).
  3. 3Admitted client calls POST /claim with the token. The gateway verifies the signature locally (no DB call).
  4. 4Claim service picks a stock bucket (hash of user_id, falling through to other buckets when empty) and runs a Lua script atomically: if the user is already in the claimed set return DUPLICATE; if bucket stock <= 0 return EMPTY; else decrement stock, add the user, and store the claim with expires_at = now + 10 min.
  5. 5On success it publishes ClaimGranted to Kafka (with acks=all) and returns claim_id to the user. If all buckets are empty it sets a global sold_out flag that the CDN and waiting room read, so further users are rejected at the edge.
  6. 6Order creator workers consume claims at a steady rate and create PENDING orders with claim_id unique, so replays do not duplicate.
  7. 7User pays through normal checkout; on payment success the order is CONFIRMED and the claim is marked USED.

Unpaid claim expires

  1. 1The sweeper scans claims whose expires_at has passed and whose status is not USED.
  2. 2In one Lua script it marks the claim EXPIRED and increments the bucket's stock, so the unit returns exactly once.
  3. 3It cancels the PENDING order and clears the sold_out flag if stock is now above zero, admitting more users from the waiting room.
  4. 4If a payment success arrives after expiry, checkout re-tries a claim; if no unit is free, the payment is refunded automatically.

7 Deep dives

Layered admission control: reject cheaply, early

The core idea is that 5M people want 10,000 units, so at least 99.8% of them must get 'no' and that 'no' must be as cheap as possible. Build layers like a funnel. Layer 1, CDN: the sale page, images and countdown are static and cached; even a 'sold out' status is a tiny JSON with a 1-2 second TTL served from the edge. Layer 2, client: disable the button until server time says the sale started, add jitter of a few hundred milliseconds, and do not retry automatically more than once. Layer 3, waiting room: a queue service (like the ones ticketing sites use) admits users at a fixed rate and hands out signed tokens; everyone else sees their position. Layer 4, gateway rate limits per user, device and IP. Layer 5, the claim service does one atomic Redis operation. Only 10,000 requests ever reach the order database, through a queue at a controlled rate. Compare this with the naive design where 1M requests/s hit SELECT ... FOR UPDATE on one inventory row: the database melts and nobody gets anything.

Exact inventory with Redis and how to keep it safe

A single Redis key decremented atomically is the simplest correct counter: Redis runs commands one at a time, so a Lua script that checks stock > 0, decrements and records the user cannot interleave with another. DECR alone is not enough because it goes negative; you either check in Lua or INCR back after a negative result. To spread load, split 10,000 units into 10 buckets of 1,000 on different Redis shards; a user tries a bucket by hash and moves to the next if empty. The risk is Redis failover: replication is asynchronous, so a primary crash can lose the last few decrements and the replica would show more stock, causing overselling. Mitigations: use WAIT to require one replica acknowledgment for each grant (adds ~1 ms), keep AOF fsync, and most importantly treat Kafka ClaimGranted events as the durable truth. If Redis fails over, pause claims, rebuild each bucket as initial units minus granted claims (from Kafka) plus expired returns, then resume. A small buffer (e.g. hold back 0.5% of units) also absorbs edge cases without overselling.

Fairness and bots

In a hyped sale, bots and resellers try to grab most units with scripts that call the API directly at 11:59:59.900. Defences: require login and a verified phone number, ideally pre-registration a day before (which also tells you demand for capacity planning); limit one unit per user, per phone, per address and per payment instrument; check device attestation (Play Integrity, App Attest) and device fingerprints; validate that the call carries a waiting-room token that cannot be minted early because the waiting room only starts issuing at noon; reject requests that arrive before the start time at the gateway; and add a randomised admission window, e.g. everyone who arrives in the first 10 seconds is shuffled, which removes the advantage of millisecond timing. After the sale, cancel orders that share suspicious signals. A lottery for pre-registered users is the fairest option when demand is 500x supply; mention it as a product choice.

Protecting the rest of the platform

A flash sale must not take down normal shopping. Isolate it: separate claim service deployment, separate Redis cluster, separate Kafka topic, and its own rate limits at the gateway so a flood on /sales does not starve /checkout for other users. Pre-scale everything 30-60 minutes before noon, because autoscaling reacts in minutes and the spike lasts seconds. Warm caches with the sale page and product data. Use feature flags to switch off expensive extras (recommendations, 'people also bought', review loading) on the sale page. Run a load test at 2x expected peak in the week before with the real infrastructure, and have a runbook: if the payment gateway slows down, extend claim expiry; if Redis fails, pause and rebuild as above. Monitor a single dashboard: admitted rate, claims granted, units remaining, order creation lag and payment success rate.

8 Trade-offs

Redis counter vs database row lock

A database row with SELECT FOR UPDATE is simple and durable but serialises every attempt on one row at ~1k/s at best. Redis does 100k+/s per shard but needs a durability plan (Kafka as the source of truth, rebuild on failover). For a flash sale Redis wins; normal products keep the DB path.

Waiting room vs letting everyone race

A waiting room adds a step and some complexity but turns an unbounded spike into a controlled flow and gives users honest feedback. Without it, the system relies on rejecting at the gateway, which feels random and still costs connections.

Synchronous vs queued order creation

Creating the order synchronously gives the user an immediate order ID but couples the claim rate to DB capacity. Queuing claims and creating orders at a steady rate protects the DB; the user gets a claim ID instantly and the order a second later.

First-come-first-served vs lottery

FCFS rewards speed, which bots win. A lottery among pre-registered users is fairer and spreads load, but feels less exciting. Many launches use registration plus FCFS with randomised admission inside short windows.

9 Bottlenecks

Single hot counter

One Redis key can be a hot spot at extreme rates. Split into buckets across shards; once a bucket is empty, mark it so clients skip it.

Payment gateway at the end

10,000 payments in a few minutes is fine, but many sales run together on sale days. Spread across multiple gateways with routing, and extend claim TTL if success rates drop.

Connection storms

Millions of TLS handshakes at noon can overload load balancers. Keep the app connection warm before noon, use HTTP/2 or HTTP/3, and serve sold-out status from the CDN.

10 Failure handling

Redis primary fails mid-sale

Pause claims (return 'please retry'), promote the replica, rebuild bucket counts from Kafka claim events and expired claims, then resume. Users wait a few seconds; no overselling.

Claim granted but Kafka publish fails

The Lua script records the claim in Redis first; if the publish fails, the claim service retries, and a reconciler compares Redis claims with Kafka events every few seconds. Order workers dedupe by claim_id.

Order workers fall behind

Kafka buffers claims; users see 'confirming your order'. Claim expiry is measured from order creation, not from claim time, so slow workers do not eat users' payment window.

11 What impresses

  • Framing it as rejecting 99.8% of users cheaply, with numbers
  • Handling Redis async replication risk and rebuilding counts from Kafka
  • Using server time for the countdown
  • Randomised admission windows or lottery for fairness
  • Isolating the sale stack so normal shopping is unaffected
  • A pre-sale load test and runbook

12 Mistakes

  • SELECT ... FOR UPDATE on one inventory row for every attempt
  • Relying on autoscaling to handle a spike that lasts seconds
  • Using DECR without guarding against negative stock
  • Forgetting to return unpaid units
  • No per-user limit, so bots take most of the stock
  • Letting every request reach the order database

13 Follow-ups

Q1How do you show users their real position in the queue?
Q2What if the sale has 50 different SKUs with different stock?
Q3How would you run this across two data centres?
Q4What if the price is wrong and you must stop the sale immediately?
Q5How do you prove after the sale that you did not oversell?
Q6How would you handle a sale where each user can buy up to 3 units?

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