Design a flash sale (Big Billion Days)
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
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
3 API
| Endpoint | What 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}/queue | Enter the waiting room; returns a signed queue token with position or an admission token |
POST/v1/sales/{sale_id}/claim | With an admission token, try to claim one unit; returns claim_id with expiry or SOLD_OUT |
POST/v1/checkout | Normal 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 CDNsale_idsku_idstart_atend_attotal_unitsper_user_limitpriceLoaded into every edge/service cache before the sale; read-only during the sale.
stock_counterRedis (replicated, AOF every second)sale:{id}:stock:{bucket} -> remaining integerSplit 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 logsale:{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_eventsKafkaclaim_iduser_idsale_idbuckettsDurable 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
Hover a box to highlight its connections.
6 Request flows
Claim a unit at 12:00:00
- 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.
- 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).
- 3Admitted client calls POST /claim with the token. The gateway verifies the signature locally (no DB call).
- 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.
- 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.
- 6Order creator workers consume claims at a steady rate and create PENDING orders with claim_id unique, so replays do not duplicate.
- 7User pays through normal checkout; on payment success the order is CONFIRMED and the claim is marked USED.
Unpaid claim expires
- 1The sweeper scans claims whose expires_at has passed and whose status is not USED.
- 2In one Lua script it marks the claim EXPIRED and increments the bucket's stock, so the unit returns exactly once.
- 3It cancels the PENDING order and clears the sold_out flag if stock is now above zero, admitting more users from the waiting room.
- 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
Lessons this design uses
Practice questions
- Your Big Billion Days style sale starts at midnight. 10 million users will open the app in the first minute. How do you use the CDN and caches to survive it?
- For a flash sale of 1,000 phones with 2 million buyers, someone suggests keeping the stock count in Redis. Is that safe? How would you design it?
- In a flash sale, 1,000 phones are on offer and 2 lakh users click 'Buy' in the first minute. How do you make sure you never sell more than 1,000?
- During a flash sale, one large merchant receives 8,000 payments per second. Every payment updates the merchant's balance row in your ledger, and the database is choking on lock contention. How do you fix it?
- A sale starts at exactly 12:00 midnight and traffic jumps 20x in under a minute. Reactive autoscaling did not keep up last year. How do you prepare this time?
- During a flash sale for a limited phone launch, bots buy most of the 10,000 units within seconds and resell them. How would you design the sale to be fair to real users?
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.