Design a Flash Sale System — the walkthrough in full
A written version of the interactive walkthrough above — the same steps, decisions and trade-offs, laid out for reading, reference and search.
The big idea
What makes a flash sale hard?
At 12:00:00 sharp, a 1000-unit drop opens and two million people slam "buy" in the same second. Two things must both hold: you sell exactly 1000 units — never a single one more (overselling a sold-out item is a real, costly failure) — and the site stays up under a spike 1000× its normal load. Scarcity plus a thundering herd.
A flash sale system guarantees no oversell under extreme concurrency while surviving the traffic spike. Two independent problems: correctness (an atomic inventory decrement so item 1001 can never be sold) and scale (admission control + a queue so the herd never crushes the core). Reserve fast, confirm async.
How to read this: Each step opens with a real design decision — make the call before I show you what ships. Watch the design grow, and at the end drop the atomic counter and kill the queue to see correctness vs the herd. Hit Begin.
Step 1 · The oversell bug
Read-then-decrement races
The naive purchase: read stock, if it's > 0 decrement it and give the user a unit. Under millions of concurrent buyers, why does this sell more units than exist?
Design decision: read stock; if > 0, decrement. Why does this oversell under massive concurrency?
The call: Read and decrement aren't atomic — thousands read the same positive stock and all pass, overselling. — With a gap between reading stock and decrementing it, massive concurrency means many buyers observe the same "in stock" value before any of them writes, so they all proceed and the count goes negative. You need the check-and-decrement to be one atomic step.
Read-then-decrement is two steps, so under massive concurrency thousands of buyers all read the same "in stock" value before any decrement lands — they all pass the check and all decrement, driving stock negative. That's overselling. The fix is to make "if available, take one" a single atomic operation that the store serializes.
The race is the enemy: Overselling is a classic lost-update race amplified by extreme concurrency. The only robust fix is atomicity: decrement-if-positive as one indivisible operation, so exactly the available number of buyers can ever succeed. Everything else builds on that guarantee.
Step 2 · Survive the flood
Admission control & the waiting room
Even with correct inventory, two million requests hitting your core services in one second will melt them. You can't scale to serve everyone instantly for a few thousand units. How do you protect the system from the herd itself?
Design decision: Two million requests in one second for 1000 units. How do you protect the core?
The call: Autoscale the core to handle two million concurrent requests. — Scaling to serve two million in one second for a thousand units is wildly expensive and often impossible fast enough — and pointless, since almost everyone will fail to get a unit. Shed and pace the load at the edge instead.
Put admission control in front: a gate (often a virtual waiting room) that rate-limits, filters bots, and admits users in controlled batches — e.g. queue everyone and let N per second through to actually attempt a purchase. The core services only ever see a manageable trickle, so a 1000× spike becomes a steady, survivable stream. Most users wait in line; the system stays up.
Shed and pace at the edge: You can't (and shouldn't) serve two million buyers instantly for a thousand units. Admission control turns an unservable instantaneous flood into a paced queue the core can handle, protecting the system and giving users a fair, orderly "you're in line" experience instead of errors.
Step 3 · Never sell 1001
Atomic inventory
Admitted users now attempt to grab a unit. This is the correctness core: how do you let exactly 1000 succeed and everyone after them get "sold out", under heavy concurrency, fast?
Design decision: Admitted buyers grab units. How do you let exactly 1000 succeed, fast, with no oversell?
The call: A database transaction with a table lock per purchase. — A table lock serializes every purchase through one lock — correct but far too slow for the throughput, creating a huge queue. An atomic counter (or a conditional decrement) achieves correctness with far higher throughput.
Hold remaining stock in a fast atomic counter (Redis DECR / a Lua script, or a DB UPDATE … SET stock=stock-1 WHERE stock>0). Each purchase does one atomic decrement; the buyer wins only if the result stays ≥ 0, otherwise it's sold out (release the decrement). Atomicity guarantees exactly the available number of buyers can ever succeed — item 1001 is never sold — at in-memory throughput.
Atomic decrement = the gate: The single atomic decrement is the whole correctness story: because the store serializes it, there's no read-then-write gap, so no oversell is possible regardless of concurrency. Redis gives the throughput to run it millions of times a second; the durable store stays the ultimate truth.
Step 4 · You won — briefly
Reserve, don't sell yet
A winning decrement means the user gets to buy — but they still have to pay, which takes seconds and can fail. If you only decrement on successful payment, slow buyers block the unit; if you sell before payment, non-payers steal units. What state is the unit in right after the grab?
Model the win as a reservation, not a final sale. A successful decrement reserves the unit for that user with a short hold TTL (e.g. 5–10 minutes) to complete checkout. The unit is neither freely available nor permanently sold — it's held. If they pay, it becomes a confirmed order; if the hold expires, the reservation is cancelled and the unit is returned to stock (an atomic increment) for someone else.
Reserve → confirm → (or release): Separating "grabbed a unit" from "paid for it" is essential: payment is slow and fallible, so you hold the unit briefly rather than block it forever or sell it to a non-payer. The TTL is the safety valve that reclaims units abandoned at checkout — the same reservation pattern as booking a seat.
Step 5 · Decouple the slow parts
Async order processing
Payment, fraud checks, inventory persistence, notifications and fulfillment are all slow relative to the instant decrement. Doing them synchronously while the user (and thousands of admitted others) wait would jam the whole flow. How do you keep the grab fast?
Design decision: Payment/fulfillment are slow. How do you keep the fast "grab" from being blocked by them?
The call: On a successful reservation, enqueue an order and process payment/fulfillment asynchronously off the hot path. — The reservation is instant; the confirmed grab is put on a durable queue, and workers handle payment, persistence and fulfillment at their own pace while the hold TTL protects the unit. The fast path stays fast; slow, fallible work is isolated and retryable.
Once a unit is reserved, enqueue the order on a durable queue and process the slow work — payment, persistence to the durable store, fraud checks, fulfillment, notifications — asynchronously off the hot path. The user gets an instant "you're in, completing your order" while workers handle it at their own pace, protected by the reservation hold. The fast reservation path never waits on slow, fallible steps.
Fast grab, async confirm: Split the flow into a fast, correctness-critical reservation (atomic decrement) and a slow, decoupled fulfillment (queue + workers). The queue absorbs bursts, makes the slow steps retryable and durable, and keeps the reservation gate responsive. The hold TTL bridges the two — the unit is safe while the queue drains.
Step 6 · Money & completion
Payment and confirmation
The async worker now has to actually charge the winner and turn the reservation into a real order — and payments are slow, can fail, and must never double-charge.
The worker takes the reserved order and runs payment with an idempotency key (so a retry never double-charges), then, on success, confirms the order in the durable store — the reservation becomes a paid, fulfilled order. On payment failure or timeout, cancel the reservation and release the unit back to stock (atomic increment) so it isn't lost. Notify the user of the outcome. Payment is just a reliable, idempotent step in the async pipeline, with the hold TTL as its deadline.
Idempotent, reversible completion: The confirm step must be idempotent (retries are inevitable under load) and reversible (a failed payment returns the unit to inventory, not into a void). Tie it to the reservation TTL so an abandoned checkout automatically frees its unit — no unit is ever stuck "held" forever or sold to a non-payer.
Step 7 · The hot single item
The hot-key & fairness
Everyone is contending on one inventory counter — a single, blazing-hot key that every purchase hits, plus determined bots trying to grab everything and the need for the sale to feel fair.
Tame the hot key: the atomic counter is one cell every buyer touches, so keep it in a fast in-memory store, optionally shard the stock into buckets (1000 = 10×100) so decrements spread across keys, and pre-warm/replicate it. Enforce fairness & anti-abuse: per-user purchase limits, rate limiting and bot filtering at admission, CAPTCHAs, and pre-issued queue tokens so one script can't sweep the drop. Persist the fast counter's state to the durable store and reconcile, so the in-memory gate and the system of record never disagree — and the durable store is the final backstop against oversell.
One key, many buyers: A flash sale is the ultimate hot-key problem: a single number under millions of writes. In-memory atomics handle the throughput; sharding the counter spreads the contention; admission + per-user limits + bot defense keep it fair. The durable store reconciles and guarantees the books are right.
Step 8 · The sharp edges
Expiry, idempotency & under-sell
Real drops bring edge cases: units held by users who never pay, duplicate requests from retries and double-clicks, and the opposite risk of under-selling (units stuck in expired holds while the item shows "sold out").
Reclaim abandoned units by expiring reservations and returning them to stock, and re-open the sale if reclaimed units appear — so you don't under-sell (show sold-out while units are stuck in dead holds). Make every step idempotent (idempotency keys, dedupe) so retries, double-clicks and at-least-once queue delivery don't double-reserve or double-charge. Give clear real-time status (in line / you won / sold out) to cut refresh-hammering. And treat the durable store as the final arbiter of inventory, reconciling the fast counter against it so neither oversell nor lost units can persist.
Design for the unhappy path: Abandoned hold → expire + return to stock (avoid under-sell). Retry/double-click → idempotency. Herd refreshing → live status. Counter vs truth → reconcile to the durable store. The atomic-decrement core is correct; these guards make the whole sale correct, fair, and neither over- nor under-sold.
You did it
You just designed a flash sale system.
- R — e
- A — d
- A — n
- A —
- E — n
- P — a
- T — a