Design Flash-Sale Inventory (oversell under contention)
Design Flash-Sale Inventory (oversell under contention)
Why this one is a favourite at Flipkart / Meesho / Swiggy: a flash sale is a controlled stampede — tens of thousands of buyers converging on a few thousand units in the same second. The naive "check if stock is left, then decrement" reads fine and oversells thousands of units under load, which means refunds, cancelled orders, and a trending-for-the-wrong-reason brand. The whole problem is one sentence: sell exactly N, never N−1 (lost revenue) and never N+1 (oversell), while a hot single row is being hammered by 50,000 concurrent writers. It's the fungible-counter cousin of Ticketmaster (which assigns specific seats) and the same race family as dispatch double-booking.
Scope it like a senior (ask before you design)
- Fungible units or specific items? 10,000 identical units of one SKU is a counter problem; assigned seats/serials is a different (allocation) problem. Assume fungible for v1.
- Is any oversell tolerable? For paid inventory, zero. That word forces atomicity — you cannot design "mostly correct" stock.
- Reserve-then-pay, or decrement-on-pay? Almost always reserve a unit with a short TTL at "add to cart / buy now", then confirm on payment — otherwise a card decline after decrement silently loses a unit.
- Peak concurrency and duration? The sale is seconds long. Assume ~50K concurrent buyers, 10K units, a multi-second burst. This sets the contention, which is the real problem — not total throughput.
- Do we shed load, or try to serve everyone? A virtual waiting room that admits a bounded rate is a first-class part of the design, not an afterthought.
Reason to the design (derive it, don't recite it)
Attempt 1 — read stock, decide, write. if (stock > 0) stock = stock - 1;. Classic check-then-act: 50K threads all read stock=1, all pass the check, all decrement → stock goes negative and you've sold the same last unit 50,000 times. Wrong at the first interleaving.
Attempt 2 — wrap it in a lock / SELECT FOR UPDATE. Now it's correct, but every one of 50K buyers serialises on a single row's lock. Throughput collapses to "one decrement per lock round-trip", the queue backs up, and timeouts cascade. Correct but unscalable — the hot single row is the bottleneck.
Attempt 3 — make the decrement itself atomic and conditional, then spread it.
- Atomic conditional decrement — one statement that both checks and decrements:
UPDATE inventory SET stock = stock - 1 WHERE sku = ? AND stock > 0(rows-affected = 1 means you got a unit, 0 means sold out), or a RedisDECRguarded by a floor / a Lua script. No read-modify-write window exists, so no oversell — regardless of concurrency. - Shard the counter — split 10,000 units into K buckets of 10,000/K and route each buyer to one bucket. Contention drops K×; when a bucket hits 0 the request rolls to another non-empty bucket. This turns one white-hot row into K warm ones.
- Shed load first — a virtual waiting room admits a bounded rate so the counter never sees the full 50K at once. You cannot make a single counter serve 50K writes/s; you make sure it never has to.
Atomic-conditional-decrement is what kills oversell; sharding + waiting room are what make it survive the stampede.
Build it — the design worksheet
Functional: reserve a unit (atomic, TTL), confirm on payment, release on expiry/cancel; expose remaining stock (approximate is fine for display). Non-functional: zero oversell; sell-through as close to 100% as possible; survive a seconds-long burst; degrade by shedding, not by overselling.
Back-of-the-envelope (show the arithmetic):
- Contention: 50,000 buyers × 1 reserve attempt in a ~5s window ≈ 10,000 attempts/s against 10,000 units. Without sharding that is 10K writes/s onto one row — far past a single hot row's safe write rate (~a few K/s). Shard into K=10 buckets → ~1,000 writes/s/bucket, comfortably safe.
- Success ratio: only 10,000 of ~50,000 can win a unit → ~80% of admitted buyers must get a clean, fast "sold out" — so the sold-out path has to be as cheap as the success path (a rows-affected=0 is enough; don't queue losers).
- Waiting room: admit ~2,000/s for a few seconds → the counter fleet only ever sees a trickle; the other ~48K sit in a queue with a position, or are told "sold out" early once reserved+remaining ≤ 0.
API sketch: reserve(sku, userId) → {reservationId, ttl} | SOLD_OUT; confirm(reservationId, paymentId); release(reservationId) (explicit or TTL-driven).
Data model / partitioning: inventory as K counter buckets per SKU (Redis keys stock:{sku}:{bucket} or K DB rows), a durable source of truth (row per SKU) reconciled from the confirmed reservations, and a reservations store keyed by reservationId with an expiry index. Route a buyer to a bucket by hash(userId) % K; roll to the next bucket on a 0.
Break it — the naive design's three concrete failures
1. Check-then-act oversell (Attempt 1). Interleaving with stock=1: T1 read 1 → T2 read 1 → T1 stock>0? yes, write 0 → T2 stock>0? yes (stale), write -1. Two buyers, one unit. At 50K concurrency this happens thousands of times. Fix: the atomic conditional UPDATE ... WHERE stock > 0 / floored DECR — the check and the write are one indivisible step, so exactly stock winners exist, no matter the interleaving.
2. Hot-single-row contention (Attempt 2). Correct locking on one row serialises all 50K buyers; effective throughput = 1/(lock round-trip), the reserve latency climbs into seconds, upstream timeouts retry and amplify the load. Traced: at ~2ms per serialized decrement, one row tops out near ~500/s — 20× below the 10K/s burst. Fix: shard the counter into K buckets (contention/K) and admit a bounded rate via the waiting room.
3. Reservation-expiry vs payment race. A buyer reserves a unit, the TTL fires while their card is authorising, the unit is returned to stock and resold — then the original payment succeeds and you've oversold by one. Fix: confirm is itself a conditional transition (reserved → sold only if still reserved); if the reservation already expired, the payment is refunded/void rather than confirmed. The reservation state machine, not the timer, is the source of truth.
Optimise — with trade-offs
| Decision | Option A | Option B | When to pick which |
|---|---|---|---|
| Atomicity primitive | DB conditional UPDATE (WHERE stock>0) | Redis atomic counter (DECR + Lua floor) | DB when stock must be transactional with the order in one system of record; Redis when you need 10× the write rate and can reconcile to a durable store async. Both beat lock-then-read. |
| Counter layout | Single counter | K sharded buckets (roll on empty) | Single for low-contention SKUs; shard the instant a SKU is a flash-sale hero — it trades a tiny "which bucket has the last unit" complexity for K× less contention. |
| Reserve model | Reserve-then-pay (TTL) | Decrement-on-pay | Reserve-then-pay for paid goods (a decline mustn't lose a unit); decrement-on-pay only if payment is instant and always succeeds. TTL adds a release path but prevents silent stock loss. |
| Load handling | Virtual waiting room (admit rate) | Reject / rate-limit at edge | Waiting room gives a fair queue + position and protects the counter; plain rejection is simpler but feels random to users. Either way you must shed — the counter can't take the full burst. |
| Stock display | Eventual / approximate | Strongly consistent read | Approximate ("selling fast") for the browse path — cheap and good enough; the strong guarantee lives only in the atomic decrement, not in the number you render to millions. |
Defend under pushback
"How do you guarantee you never oversell, exactly?" The unit is only ever handed out by an atomic conditional decrement — UPDATE ... SET stock=stock-1 WHERE stock>0 (rows-affected tells you if you won) or a floored Redis DECR in a Lua script. There is no read-modify-write window for two buyers to both see the last unit. The count of winners equals the starting stock by construction, at any concurrency.
"One row at 50K writes/s will melt. Now what?" Two moves. Shard the counter into K buckets so each takes contention/K, rolling to a non-empty bucket on a 0. And shed with a waiting room that admits a bounded rate, so the counter fleet never sees the full burst — you engineer the load down to what atomic decrements can absorb, then scale buckets to meet it.
"A buyer reserved a unit and vanished." Reservations carry a TTL; on expiry a release returns the unit to its bucket. confirm is a guarded state transition (reserved → sold), so a payment that lands after expiry is refunded, not confirmed — the state machine, not the timer, decides.
"Won't sharding strand the last few units?" Slightly — a bucket can be empty while another has stock. Rolling to the next non-empty bucket on a miss recovers almost all of it; a final "drain" pass can merge dregs. You trade a few milliseconds of roll logic for K× less contention — and a 100% sell-through with zero oversell beats a serialized 60% sell-through.
"100× — a national sale, 5M buyers, thousands of SKUs. What breaks first?" Not any single counter (they're sharded and shed-protected) — it's the waiting-room admission and the reconciliation of reserved→confirmed back to the durable store. Scale the counters horizontally per SKU, partition the waiting room by SKU, and make reconciliation an idempotent async pipeline (idempotency keys) so retries never double-confirm.
What you've mastered when this is done
- Killing oversell with an atomic conditional decrement — and explaining why check-then-act and lock-then-read both fail (correctness vs contention).
- Turning one white-hot counter into K sharded buckets + a load-shedding waiting room, and reasoning about the sell-through-vs-contention trade-off.
- Designing a reserve-with-TTL state machine where
confirmis a guarded transition, so an expiry-vs-payment race can never oversell or lose stock. - Naming what actually breaks at 100× (admission + reconciliation, not the counter) and making the reconciliation idempotent.
Related: Ticketmaster (seat reservation) · the dispatch double-booking race · MVCC, locking & row locks · distributed locking · idempotency keys.
🤖 Don't fully get this? Learn it with Claude
Stuck on Design Flash-Sale Inventory (oversell under contention)? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Design Flash-Sale Inventory (oversell under contention)** (Hands-On Builds) and want to truly understand it. Explain Design Flash-Sale Inventory (oversell under contention) from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Design Flash-Sale Inventory (oversell under contention)** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Design Flash-Sale Inventory (oversell under contention)** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Design Flash-Sale Inventory (oversell under contention)** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.