Courses › System Design › Question Breakdowns
Design a Flash Sale
In-memory stock gates, waiting rooms with admission tokens, no overselling under failover, bots and isolation.
Design a flash sale system — a limited drop like a sneaker release, a Shopify merchant's 10,000-unit launch, or a Prime Day lightning deal: millions of shoppers hit "Buy" at the same second for a small fixed stock. It's a question about extreme write contention on a few inventory rows, absorbing a traffic spike far beyond normal capacity, never overselling, fairness, and bots.
Requirements
Functional
- A sale has a start time, a product with fixed stock (e.g. 10,000 units) and a per-customer limit
- Shoppers reserve a unit, then pay within a time limit; unpaid reservations return to stock
- Shoppers see whether they got one — quickly and honestly — and the remaining availability
- The rest of the store keeps working during the sale
Out of scope: the normal catalog and checkout (see Design an Online Bookstore), shipping, payment internals.
Non-functional
- No overselling, ever: sold units ≤ stock
- Spike tolerance: ~1M users arriving within seconds, ~100k+ "Buy" requests/second at the start — roughly 100× normal traffic
- Fairness: first come, first served (or a fair lottery); bots shouldn't win everything
- Fast, clear answers: users learn "you got it" / "sold out" within seconds
- Isolation: the sale can't take down the rest of the platform
[!TIP] Most requests must fail fast and cheaply With 10,000 units and 1M shoppers, 99% of requests will be told "sold out". The design goal is to reject them as early and cheaply as possible — at the edge or in memory — so only a trickle of real orders reaches the database.
Capacity estimates
Core Entities
- Sale — product, start time, stock, per-user limit, status
- Stock counter — remaining units (the hot spot)
- Reservation — sale, user, status (reserved / paid / expired), expires at
- Order — created on payment, as in normal checkout
API
GET /v1/sales/{saleId} → { "startsAt": "...", "status": "live", "available": true } (CDN-cached, short TTL)
POST /v1/sales/{saleId}/reserve Idempotency-Key: ... (with a sale token from the waiting room)
→ 201 { "reservationId": "r_1", "expiresAt": "..." } | 409 { "error": "sold_out" }
POST /v1/reservations/{id}/checkout { "paymentMethod": "..." }High-Level Design
user = client "Shoppers" @0,1
cdn = cache "CDN / Edge\n(static page, bot checks)" @1,0
wait = service "Waiting Room\n(queue + tokens)" @1,1
gate = service "Reservation Service" @2,1
stock = cache "Stock Counter\n(Redis, atomic)" @3,0
q = queue "Reservation Queue" @3,1
orders = service "Order Workers" @4,1
db = store "Orders / Inventory DB" @4,2
pay = service "Checkout + Payment" @3,2
user -> cdn : sale page
user -> wait : join
wait -> gate : admitted with token
gate -> stock : DECR if > 0
gate -> q : reserved
q -> orders
orders -> db
user -> pay : pay within 10 min
pay -> db- The sale page is static and served from the CDN; bots are filtered at the edge.
- At start time, shoppers enter a waiting room that admits them at a rate the backend can handle, issuing signed admission tokens.
- The reservation service decrements an in-memory stock counter atomically; success means a unit is held for that user, failure means sold out.
- Successful reservations go through a queue to workers that write them durably; the user then pays within the time limit.
- Expired reservations put units back into the counter.
Deep Dives
1. Why the database can't be the gate
UPDATE inventory SET stock = stock - 1 WHERE product_id = :p AND stock > 0;Correct, but every request locks the same row: a database handles a few thousand such updates per second, with requests piling up behind the lock and timing out. Move the gate to an atomic in-memory counter:
local left = tonumber(redis.call('GET', KEYS[1]))
if left and left > 0 then
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1]) -- record the buyer (enforces one per user)
return 1
end
return 0A single Redis key handles ~100k ops/s. For more, split stock into buckets (10 counters × 1,000 units) and route users to a bucket by hash, falling back to other buckets when theirs is empty.
2. Keeping the counter and the database in agreement
The counter is fast but volatile; the database is durable. Treat the counter as the admission gate and the database as the record:
- A successful decrement enqueues a reservation (durable queue) before responding; workers insert it with a unique
(sale, user)constraint. - If Redis fails over and loses recent decrements, the database still enforces the hard cap (a conditional insert that counts reservations ≤ stock), so overselling is impossible even then — at worst a few users are told "sold out" wrongly.
- Expired reservations: a sweeper marks them expired in the DB and
INCRs the counter.
[!WARNING] Return units exactly once If both a timeout sweeper and a late payment-failure handler return the same unit, stock grows past the real amount and you oversell. Make the return a conditional state change (reserved → expired) and increment the counter only when that update succeeds.
3. The waiting room
A virtual queue turns a 1M-user stampede into a steady flow:
- Users who arrive before or at the start get a random or arrival-ordered position; the page polls cheaply (from the edge) for its turn.
- Admission rate is set to what the reservation service can handle; admitted users receive a short-lived signed token required by
/reserve. - When the counter hits zero, the waiting room tells everyone "sold out" immediately — no one needs to reach the backend.
4. Fairness and bots
- Per-user and per-payment-method limits enforced in the Lua script and the DB.
- Bot defenses at the edge: rate limits per IP/device, challenges, purchase history signals; tokens bound to the session so they can't be shared.
- Some drops use a lottery: collect entries during a window, then randomly pick winners — fairer than milliseconds-matter racing and much easier on the system.
5. Isolation
Run the sale on dedicated capacity (separate reservation service instances, Redis, queues) with its own rate limits, so a sale spike can't exhaust the shared checkout or catalog services. Pre-scale before the start time; autoscaling is too slow for a spike that peaks in seconds.
What's expected at each level
- Mid-level: an atomic stock decrement, reservations with expiry, a CDN for the sale page.
- Senior: moving the gate to an in-memory counter, durable reservation queue with DB uniqueness, waiting room with admission tokens, returning expired units safely.
- Staff: counter/DB consistency under failover, bucketed counters, bot defense and lottery alternatives, isolation and pre-scaling, and honest UX when stock runs out.