Knowledge Guide
HomeHands-On BuildsSD Design Labs

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)

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 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):

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.

Flash-sale inventory architecture: 50K buyers for 10K units hit a waiting room that admits only ~N/s and sheds the rest; admitted requests reach the inventory service (reserve/confirm), which decrements an atomic counter (Redis DECR floored at 0, or UPDATE ... WHERE stock>0) that is sharded into K buckets to spread contention; reservations carry a TTL that returns stock on expiry, and payment runs async, confirming on success. The goal is to sell exactly 10,000 units — never 9,999 or 10,001.
Flash-sale inventory architecture: 50K buyers for 10K units hit a waiting room that admits only ~N/s and sheds the rest; admitted requests reach the inventory service (reserve/confirm), which decrements an atomic counter (Redis DECR floored at 0, or UPDATE ... WHERE stock>0) that is sharded into K buckets to spread contention; reservations carry a TTL that returns stock on expiry, and payment runs async, confirming on success. The goal is to sell exactly 10,000 units — never 9,999 or 10,001.

Break it — the naive design's three concrete failures

1. Check-then-act oversell (Attempt 1). Interleaving with stock=1: T1 read 1T2 read 1T1 stock>0? yes, write 0T2 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

DecisionOption AOption BWhen to pick which
Atomicity primitiveDB 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 layoutSingle counterK 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 modelReserve-then-pay (TTL)Decrement-on-payReserve-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 handlingVirtual waiting room (admit rate)Reject / rate-limit at edgeWaiting 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 displayEventual / approximateStrongly consistent readApproximate ("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

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.

🎨 Explain it visually

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.
🤔 Walk me through it (interactive)

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.
🧪 Quiz me & fix my gaps

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.
🧠 Make it stick

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.

📝 My notes