Vibe Engines
YouTube
System Design

Design a Flash Sale System

Step 1 / 9

Learn system design by building a flash-sale / limited-inventory system (think a 1000-unit drop with millions of buyers, or Ticketmaster-style scarcity) step by step.

The numbers to beatwaiting roomqueue the herdN/secadmit in batchesedgeshed load early

The whole design, in writing

Learn system design by building a flash-sale / limited-inventory system (think a 1000-unit drop with millions of buyers, or Ticketmaster-style scarcity) step by step. An interactive guide covering why a read-then-decrement oversells, atomic inventory decrement, admission control and a virtual waiting room to survive the thundering herd, reserve-then-confirm with async order processing, fairness and anti-bot, and the unhappy paths (reservation expiry, hot-key, idempotency).

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

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.

Buyersmillions, 1 drop
New in this step: Buyers.

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.

What the new pieces do

Buyersclient
A massive, synchronized spike — millions hitting "buy" at 12:00:00 for a few thousand units. The thundering herd.

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?

Buyersmillions, 1 drop
The system as it stands at this step.

read stock; if > 0, decrement. Why does this oversell under massive concurrency?

  1. Space isn't it. The bug is a race: read and decrement are two steps, so thousands of buyers can all read "5 left" at the same instant and all pass the check, then all decrement — selling far more than 5.

  2. Type isn't the issue. The flaw is that check-then-decrement is non-atomic: many concurrent buyers read the same positive stock before any decrement lands, so they all "win" and oversell.

  3. 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.

Why this piece earns its place

This bug is dangerous because it passes every test you are likely to write. The window between the read and the write is microseconds wide, so on a laptop, in CI, and on a staging box with ten users, read-then-decrement is indistinguishable from correct code — the race only exists in the one second of the year when the whole herd arrives together, which is also the one second you cannot rehearse. Afterwards the logs defend it: every request returned success, because from each buyer’s point of view the check really did pass. You find out from the sold count. So I wouldn’t treat this as a code-review problem. Make the invariant structural — stock cannot go negative anywhere in the system, enforced by the store itself — so the wrong pattern fails loudly in the paths nobody thought to test, instead of quietly on the day of the drop.

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?

Buyersmillions, 1 dropAdmissionwaiting room
New in this step: Admission.

Two million requests in one second for 1000 units. How do you protect the core?

  1. A front gate absorbs the flood: it rate-limits, filters bots, and lets users through in manageable batches (or a "virtual waiting room" that queues everyone and admits N/sec). The core services only ever see a controlled trickle, not the full spike.

  2. 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.

  3. The database will not cope with a 1000× spike; it will fall over, failing everyone including the winners. You must throttle admission before traffic reaches the core.

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.

Why this piece earns its place

Admitting N per second turns a flood into a rate, and that rate is the knob. The mistake is deriving it from the inventory number — a 1000-unit drop does not imply a thousand admissions. Set it from the measured capacity of the narrowest thing behind the gate, which here is the order service turning wins into reservations, and leave headroom, because the line is what you have instead of an outage. Too high and the trickle is still a flood: the core falls over and the waiting room’s promise becomes a lie. Too low and the sale crawls — people who are genuinely in line give up, and the drop can end with units never claimed. The gate itself has to be the cheapest thing in the system, because everybody touches it and almost nobody gets past it: a static page and a signed token, served at the edge, holding no per-user server state.

  • waiting roomqueue the herd
  • N/secadmit in batches
  • edgeshed load early

What the new pieces do

Admissiongateway
The front gate: rate-limits, filters bots, and admits users in controlled batches so the core never sees the full flood.

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?

Admissionwaiting roomInventoryRedis atomic DECR
New in this step: Inventory.

Admitted buyers grab units. How do you let exactly 1000 succeed, fast, with no oversell?

  1. Keep remaining stock in an atomic in-memory counter and decrement it atomically per purchase; the buyer wins only if the post-decrement value is still ≥ 0, otherwise it's sold out (and you can undo). Atomicity guarantees exactly the available count succeed — no oversell — at very high throughput.

  2. 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.

  3. Overselling a scarce item and clawing it back is a terrible, trust-destroying experience (and legally fraught). Prevent it up front with an atomic decrement — never sell the unit that doesn't exist.

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.

Why this piece earns its place

An atomic decrement guarantees you never sell one too many. It does not guarantee you sell them all, and the gap is the acknowledgement. If the counter applies the decrement and the reply never reaches the caller — a timeout, a dropped connection, a process restarting between the decrement and writing the reservation down — the unit is gone from stock and no buyer knows they own it. The user sees sold out and retries, which costs another unit, and a scarce drop can bleed real inventory to nothing but network weather. So the decrement isn’t fire-and-forget arithmetic: it returns a claim, recorded under a key derived from the buyer, so a retry from that same buyer re-reads the claim it already holds instead of taking a second one. Everything downstream keys off that claim — and a claim nobody ever picks up is reclaimable, which is what the expiry path exists for.

  • DECRatomic, no race
  • ≥ 0 winselse sold out
  • exactly Ncan ever succeed

What the new pieces do

Inventorycache
The fast atomic counter of remaining units. An atomic decrement is the gate that makes overselling impossible.

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?

Admissionwaiting roomOrder Servicereserve → confirm
New in this step: Order Service.

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.

Why this piece earns its place

The hold length is the number I’d argue about. Measure it from how long real checkout takes at its slow tail — a card that triggers an extra verification step, a banking app on a phone on mobile data, someone fetching a wallet from another room — not from how fast you can click through it. Too short and you take the unit back from a buyer who won it fairly and is actively paying, which is the worst outcome in the room because they watch it happen. Too long and the drop reads sold out while units sit dead behind abandoned checkouts. The hold also has to outlive the payment attempt it protects, or the two race: the TTL fires, the unit returns to stock and is won again, and then the original charge succeeds. So I’d size the hold as the payment attempt’s own deadline plus a margin, and have the worker give up on a charge it can no longer land rather than let it succeed against a unit somebody else already owns.

  • reservehold with TTL
  • confirmpay → order
  • expirerelease back to stock

What the new pieces do

Order Serviceservice
Turns a successful decrement into a reservation, then an order — quickly handing slow work to the queue.

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?

Order Servicereserve → confirmOrder Queueasync processDurable Storeorders + truth
New in this step: Order Queue, Durable Store.

Payment/fulfillment are slow. How do you keep the fast "grab" from being blocked by them?

  1. Blocking the fast reservation on slow payment/fulfillment jams the whole flow under load and couples the quick gate to slow, fallible steps. Hand the slow work to a queue and process it asynchronously.

  2. You can't skip payment or fulfillment. You decouple them: the reservation is instant, and the slow work happens off the hot path via a queue, holding the unit until it completes.

  3. 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.

Why this piece earns its place

Handing the slow work to a queue quietly assumes one thing: that the queue drains faster than the hold lasts. While that holds, a backlog is just latency and the reservation covers it. The day it stops — workers crashlooping, the payment provider degraded, consumers falling behind producers — reservations start expiring while their own orders are still in the queue waiting to be paid for. A worker then picks up an order whose unit is already back on sale, and if it charges anyway the same unit has been sold twice. So consumer lag isn’t an ops metric here, it’s a correctness signal: compare it against the hold budget continuously, have the worker re-check that the reservation is still live immediately before it charges rather than trusting the message, and throttle admission at the gate when lag approaches the hold. Better to slow the line than to sell units the system has already given away.

  • enqueueconfirmed reservation
  • asyncpayment + fulfillment
  • retryabledurable queue + workers

What the new pieces do

Order Queuebus
Buffers confirmed reservations so payment and fulfillment happen asynchronously, not on the hot path.
Durable Storedata
The durable record of orders and the ultimate inventory truth — reconciled with the fast counter, never oversold.

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.

InventoryRedis atomic DECROrder Servicereserve → confirmPaymentcharge · confirmDurable Storeorders + truth
New in this step: Payment.

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.

Why this piece earns its place

An idempotency key only works if two attempts at the same purchase produce the same key. Minted fresh per attempt — a new identifier generated when the worker retries — it’s decoration, and the second attempt charges again. It has to derive from something stable that both attempts share, and the reservation is exactly that: one reservation, one key, one charge, however many times the queue redelivers it. The release path needs the same suspicion, because the increment that returns a unit to stock is atomic but not idempotent. Run it twice for one reservation — an expiry sweep and a failed-payment handler both deciding this unit is free — and stock is now larger than the drop, which is overselling arriving through the back door after all the work spent shutting the front. So the reservation row is the only place a release is allowed to be recorded. The expiry sweep and the failed-payment handler both compare-and-set held → released on that row, and only whoever wins that write runs the increment; the loser reads a row that is already released and does nothing at all, quietly, exactly as if it had never fired.

  • idempotency keyno double-charge
  • confirmreservation → order
  • fail → releaseunit back to stock

What the new pieces do

Paymentservice
Charges the winner. On success the reservation becomes a paid order; on timeout the held unit is released.

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.

InventoryRedis atomic DECROrder Servicereserve → confirmPaymentcharge · confirmDurable Storeorders + truth
New in this step: Inventory → Durable Store.

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.

Why this piece earns its place

Sharding the counter buys throughput and sells you a tail. Split the stock into buckets and remaining stock is no longer one number anyone can read — a buyer routed to a bucket that happens to be empty is told sold out while units sit in the bucket next door. So a miss can’t be an answer: it becomes a walk across the remaining buckets, and sold out only means anything once every one of them is drained. That makes the last units far more expensive to sell than the first, exactly when the herd is loudest, and it turns bucket sizing into a fairness question, because an unlucky assignment is the difference between winning and not. Which is why I’d shard only when one key is measurably saturating, and start from the smallest number of buckets that clears it. Every extra bucket is more scanning at the tail and more chance a buyer with a unit in front of them is turned away.

  • shard stockspread the hot key
  • per-user limitfairness + anti-bot
  • reconcilecounter ↔ durable truth

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").

BuyersAdmissionInventoryOrder ServiceOrder QueuePaymentDurable Store
The system as it stands at this step. · swipe to pan the diagram

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.

Why this piece earns its place

Reconcile against the durable store is the sentence everyone says, and it’s the one operation that can undo everything above it. A decrement is safe under concurrency because it’s relative. A reconciliation writes an absolute number computed from a snapshot, and during a drop that snapshot is stale before it lands — units claimed while it was being taken get handed back out and the counter goes up. That is the single path left in this design that can oversell after the atomic gate did its job. So reconciliation detects and alarms; it doesn’t correct. Units return to stock only through the same guarded increment a release uses, one per reservation. And when reclaimed units do reopen the sale, the reopen goes back through admission rather than straight to the buyers refreshing a sold-out page — otherwise the small second drop arrives with the same herd as the first and none of the pacing.

You did it

You just designed a flash sale system.

BuyersAdmissionInventoryOrder ServiceOrder QueuePaymentDurable Store
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • Read-then-decrement oversells under concurrency — thousands read the same stock before any write.
  • Admission control / a virtual waiting room paces the herd so the core sees a survivable trickle.
  • An atomic decrement (Redis DECR / conditional UPDATE) guarantees exactly N buyers win — never item 1001.
  • A win is a reservation with a hold TTL, not a final sale — pay to confirm, or it's released.
  • Enqueue confirmed reservations and do payment/fulfillment async off the hot path.
  • Payment is idempotent (no double-charge) and reversible (failure returns the unit to stock).
  • Tame the single hot inventory key (shard it), enforce fairness/anti-bot, and reconcile to the durable store.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Why does a read-then-decrement oversell, and what exactly fixes it?

    It oversells because checking stock and decrementing it are two separate operations with a gap between them, and under extreme concurrency that gap is fatal. When millions of buyers hit "buy" in the same instant, thousands of them read the stock value (say "5 left") before any of them has written a decrement, so they all pass the "is it > 0?" check and all proceed to decrement, driving the count well below zero — you've sold far more units than exist. This is a classic lost-update race, amplified enormously by the flash-sale traffic shape. The fix is to collapse check-and-decrement into a single atomic operation that the store serializes: an atomic Redis DECR (with a Lua script to check the result) or a conditional database update like UPDATE ... SET stock = stock - 1 WHERE stock > 0. Because the store executes this indivisibly, there is no window in which two buyers can both observe the same positive value and both succeed — exactly the available number of decrements can return a non-negative result, and every buyer after that gets "sold out." Atomicity, not locking or scaling, is what makes overselling structurally impossible.

  2. Why separate admission control from the inventory decrement — aren't they solving the same thing?

    They solve two genuinely different problems that a flash sale forces you to address at once: correctness and scale. The atomic inventory decrement solves correctness — it guarantees you never sell more units than exist, no matter how many buyers race. But correctness alone doesn't keep the system up: two million requests arriving in a single second for a thousand units would overwhelm your core services (the inventory store, order service, database) and crash them, failing everyone including the eventual winners, long before the atomic decrement's guarantee even matters. Admission control solves scale by shedding and pacing that flood at the edge — a virtual waiting room queues everyone and admits users in controlled batches (say N per second), rate-limits, and filters bots, so the core services only ever see a manageable trickle instead of the full spike. You need both because they're orthogonal: you can be perfectly correct and still fall over from load, or perfectly scalable and still oversell. Admission control protects availability; the atomic decrement protects correctness.

  3. Why model a win as a reservation with a TTL instead of an immediate sale?

    Because a winning decrement means the user has earned the right to buy, but buying isn't instantaneous — payment takes seconds, involves external systems, and can fail — and both naive alternatives are bad. If you only decrement stock upon successful payment, then slow or hesitating buyers effectively hold the item open while everyone waits, and concurrent buyers race on stock that hasn't been claimed yet, reintroducing overselling. If you treat the decrement as a final sale before payment, then anyone who abandons checkout or fails payment has permanently consumed a unit that no one gets, under-selling the drop. The reservation model resolves this: a successful decrement immediately reserves the unit for that user with a short hold TTL (a few minutes) to complete checkout, so the unit is neither freely available to others nor permanently sold. If they pay within the window, the reservation becomes a confirmed order; if the TTL expires, the reservation is cancelled and the unit is atomically returned to stock for someone else. The TTL is the crucial safety valve that reclaims units abandoned at checkout — it's the same reserve-then-confirm pattern used for booking seats or tickets.

  4. Why push payment and fulfillment onto an async queue instead of doing them inline?

    Because those steps are slow and fallible relative to the instant atomic decrement, and doing them synchronously would couple the fast, correctness-critical path to the slowest, least reliable parts of the system exactly when it's under peak load. Payment involves external processors and can take seconds or time out; persistence, fraud checks, fulfillment, and notifications add more latency; and if the reservation path blocked on all of that, each admitted buyer would occupy resources for seconds, throughput would collapse, and a payment-provider slowdown could jam the entire sale. Instead, once a unit is reserved, you enqueue the confirmed reservation on a durable queue and let a pool of workers handle payment, persistence, and fulfillment asynchronously at their own pace, while the reservation's hold TTL protects the unit in the meantime. This keeps the reservation gate responsive (the user instantly sees "you're in, completing your order"), lets the queue absorb bursts, and makes the slow steps durable and retryable rather than fragile and blocking. The fast grab and the slow confirm are bridged by the reservation hold, so the unit is safe while the queue drains.

  5. A flash sale is the ultimate hot-key problem — how do you handle one number under millions of writes?

    The inventory counter is a single cell that every single buyer must contend on, which is the definition of a hot key and would crush a normal database. The first move is to keep that counter in a fast in-memory store (like Redis) whose atomic operations can sustain millions of decrements per second, rather than a disk-based row with locks. When even one key is too hot, you shard the stock into buckets — representing 1000 units as, say, ten counters of 100 — so decrements spread across multiple keys (and potentially multiple nodes), reducing contention on any single one, with a buyer trying buckets until one succeeds and the sale ending when all are drained. You also pre-warm and replicate the counter so it's ready for the instantaneous spike. Around the counter you enforce fairness and anti-abuse — per-user purchase limits, rate limiting and bot filtering at admission, CAPTCHAs, and pre-issued queue tokens — so a single script can't sweep the entire drop and the sale feels fair to real users. Finally, you persist and reconcile the fast counter's state against the durable store, which remains the ultimate system of record and final backstop: the in-memory gate provides throughput and the no-oversell guarantee in real time, while the durable store ensures the books are ultimately correct and that neither overselling nor lost (stuck-in-expired-hold) units can persist.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. A read-then-decrement oversells because…

    Under concurrency, buyers observe the same positive stock before any write — a lost-update race.

  2. Admission control / a virtual waiting room exists to…

    You can't serve millions instantly for a few thousand units; shed and pace load at the edge.

  3. Item 1001 is never sold because…

    The store serializes decrement-if-positive, so no read-then-write gap and no oversell.

  4. A winning grab is modeled as a reservation with a TTL so that…

    Hold the unit briefly; pay to confirm, or the TTL expires and it returns to stock.

  5. Payment and fulfillment run async on a queue because…

    Fast grab, async confirm: the hold TTL protects the unit while durable workers process it.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Never oversell: sell exactly the available units — item 1001 can never be sold — via a single atomic DECR the store serializes.
  • Survive the herd: absorb a 1000× spike with admission control — a virtual waiting room that rate-limits, filters bots and admits users in controlled batches.
  • Reserve, then confirm: a winning decrement holds the unit with a short TTL; pay to turn it into an order, or the hold expires and the unit returns to stock.
  • Process orders async: enqueue each confirmed reservation and run payment and fulfillment off the hot path, with an idempotency key so retries never double-charge.
  • Keep it fair: per-user purchase limits, rate limiting, CAPTCHAs and queue tokens so one bot script can’t sweep the whole drop.

The qualities that shape everything

Each one names the mechanism that buys it.

Stay up under a 1000× spike
Admission control and a virtual waiting room shed and pace the flood at the edge, admitting N/sec so the core only ever sees a survivable trickle.
No oversell under massive concurrency
A single atomic decrement (Redis DECR / conditional UPDATE) the store serializes — exactly the available buyers get a non-negative result, so item 1001 is never sold.
A win doesn’t block on slow payment
Model the win as a reservation with a short hold TTL, not a final sale, so a slow or failed payment neither blocks the unit forever nor sells it to a non-payer.
The fast path never waits on slow steps
Enqueue confirmed reservations on a durable queue and process payment, persistence and fulfillment asynchronously off the hot path, protected by the hold TTL.
Retries never double-charge
Payment runs with an idempotency key and the confirm step is reversible — a failed payment releases the unit back to stock via an atomic increment.
One blazing-hot key survives millions of writes
Keep the counter in a fast in-memory store, optionally shard the stock into buckets to spread contention, and reconcile against the durable store.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

An atomic counter over a DB transaction with a table lock per purchase

A fast atomic Redis DECR (or conditional UPDATE) lets exactly the available buyers win at in-memory throughput with no read-then-write gap. A table lock serializes every purchase through one lock — correct, but far too slow for the throughput and it builds a huge queue.

Admission control at the edge over autoscaling the core to serve everyone

A virtual waiting room rate-limits and admits N/sec so the core only sees a survivable trickle. Autoscaling to serve two million in one second for a few thousand units is wildly expensive, often impossible fast enough, and pointless since almost everyone fails to get a unit.

A reservation with a hold TTL over an immediate final sale on decrement

Holding the unit briefly means slow or failed payment neither blocks it forever nor sells it to a non-payer; if the TTL expires the unit returns to stock. Selling before payment lets non-payers permanently consume units, under-selling the drop.

Enqueue and confirm async over payment and fulfillment synchronously before confirming

The reservation is instant while slow, fallible work runs off the hot path on a durable queue, protected by the hold TTL. Doing payment and fulfillment synchronously jams the whole flow under load and couples the quick gate to slow, fallible steps.

An in-memory counter, sharded into buckets over a single disk-based inventory row under locks

A fast in-memory store sustains millions of atomic decrements a second, and sharding 1000 units into ten counters of 100 spreads the contention across keys. A single hot row under millions of writes would crush a normal database.

The answer, out loud

What a strong answer to “Design a Flash Sale System” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    What’s actually being asked

    I want to pin down the shape before I draw anything. There are a few thousand units, a fixed start time, and effectively everyone who wants one arriving in the same second, so I’m really being asked two separate questions at once. One is correctness: sell exactly the units that exist and not one more. The other is availability: stay up through a spike far beyond normal traffic, for a sale where almost everyone loses. Those need different machinery, and a perfectly correct system that’s down has still failed everybody, including the winners.

  2. 3–8 min

    Why the obvious purchase is wrong

    The naive purchase is: read the stock, check it’s positive, decrement, hand over a unit. That’s two operations with a gap, and at this traffic shape the gap is where the whole sale goes wrong. Thousands of buyers read the same positive number before any of them has written anything back, they all pass the check, and they all decrement — the count goes negative and I’ve sold units that don’t exist. Nothing in that flow looks broken; it just can’t survive everyone arriving at once. So I want taking a unit to be one indivisible operation instead of two.

    Built in step 1: Read-then-decrement races
  3. 8–14 min

    One atomic decrement

    I’d keep remaining stock in a fast in-memory counter and make the purchase a single atomic decrement — a Redis DECR, or a conditional update that only subtracts while stock is above zero. You win the unit if your decrement leaves the number at or above zero; if it doesn’t, you’re sold out and I put the unit back. Because the store serializes that operation, exactly as many buyers as there are units can ever get a winning result. That’s the entire no-oversell guarantee, however many people arrive. A database transaction taking a lock per purchase is also correct and far too slow — it funnels the whole sale through one lock.

    Built in step 3: Atomic inventory
  4. 14–20 min

    Keeping the site up is a separate job

    Correctness doesn’t keep the site up, though. Two million requests in a second aimed at the order path will take the core down long before the counter’s guarantee matters, and I can’t scale to serve everyone instantly for a thousand units — it’s enormously expensive and mostly pointless, since nearly all of that traffic is doomed. So I put admission control in front: a virtual waiting room that holds everyone, filters obvious bots, and admits a fixed number per second to attempt a purchase. The core sees a trickle instead of a wall, and users get a place in line rather than an error page.

    Built in step 2: Admission control & the waiting room
  5. 20–27 min

    A win is a hold, not a sale

    A winning decrement isn’t a sale yet, because paying takes time and can fail. If I only decrement once payment succeeds, buyers race on stock nobody has claimed and I’m back to overselling. If I treat the decrement as final, anyone who abandons checkout permanently eats a unit and the drop under-sells. So the win is a reservation: the unit is held for that buyer for a few minutes to finish checkout. Pay, and it becomes a confirmed order. Let the hold expire, and the reservation is cancelled and the unit goes back into stock atomically for someone else.

    Built in step 4: Reserve, don't sell yet
  6. 27–33 min

    Getting the slow work off the path

    Then I move the slow work off the hot path. Payment, fraud checks, writing the order down, notifications, fulfilment — all of it is slow and fallible next to the decrement, and doing it inline means every admitted buyer holds resources for seconds while one slow payment provider stalls the entire sale. So a confirmed reservation goes onto a durable queue and workers handle the rest at their own pace. The buyer immediately sees that they’re in and their order is completing, the queue absorbs the burst, and the hold is what keeps their unit safe while it drains.

    Built in step 5: Async order processing
  7. 33–38 min

    Charging exactly once

    The worker charges the buyer with an idempotency key, so a retry can’t double-charge — and at this volume retries and redeliveries are certain, not hypothetical. On success it confirms the order in the durable store and the reservation becomes a real, paid order. On failure or timeout it cancels the reservation and returns the unit to stock, so a failed payment costs me nothing but a few minutes of that unit’s time. And I tell the buyer what happened either way, because silence is what makes people refresh.

    Built in step 6: Payment and confirmation
  8. 38–43 min

    One very hot number, and fairness

    Two things I’d raise before time runs out. The counter is a single blazing-hot key that every buyer touches, so it lives in memory, it’s pre-warmed and replicated, and if one key saturates I can split the stock across several counters to spread the writes. And the sale has to be fair: per-user purchase limits, rate limiting and bot filtering at the gate, CAPTCHAs, and tokens issued in the waiting room, so one script can’t sweep the drop before real people are through the door.

    Built in step 7: The hot-key & fairness
  9. 43–45 min

    The trade, and what’s left

    The trade I’ve made throughout is a fast, correctness-critical front and everything slow or fallible pushed behind a hold. That’s what makes the sale survivable, and it’s also what leaves work on the table: units can sit in dead holds while the page says sold out. With more time I’d spend it on the durable store as the final arbiter — reconciling it against the fast counter, and deciding whether a handful of units trickling back is worth reopening the sale for at all — and on the live status the buyer sees, because much of a drop’s load is people refreshing to see whether they’re still in it.

What this teaches

Learn system design by building a flash-sale / limited-inventory system (think a 1000-unit drop with millions of buyers, or Ticketmaster-style scarcity) step by step. An interactive guide covering why a read-then-decrement oversells, atomic inventory decrement, admission control and a virtual waiting room to survive the thundering herd, reserve-then-confirm with async order processing, fairness and anti-bot, and the unhappy paths (reservation expiry, hot-key, idempotency).

Key takeaways

  • Read-then-decrement oversells under concurrency — thousands read the same stock before any write.
  • Admission control / a virtual waiting room paces the herd so the core sees a survivable trickle.
  • An atomic decrement (Redis DECR / conditional UPDATE) guarantees exactly N buyers win — never item 1001.
  • A win is a reservation with a hold TTL, not a final sale — pay to confirm, or it's released.
  • Enqueue confirmed reservations and do payment/fulfillment async off the hot path.
  • Payment is idempotent (no double-charge) and reversible (failure returns the unit to stock).
  • Tame the single hot inventory key (shard it), enforce fairness/anti-bot, and reconcile to the durable store.

Concepts covered

  • What makes a flash sale hard?
  • Read-then-decrement races
  • Admission control & the waiting room
  • Atomic inventory
  • Reserve, don't sell yet
  • Async order processing
  • Payment and confirmation
  • The hot-key & fairness
  • Expiry, idempotency & under-sell
built to sell exactly 1000 units to a million people without overselling one — make the calls, drop the counter, run the gauntlet.
Finished this one? 0 / 65 System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs