Design a Flash Sale System
Problem
Design the backend for a flash sale on a large e-commerce site. At a fixed start time, a limited quantity of discounted items (for example 10,000 units of one phone) goes on sale. Each user may buy at most one unit, the system must never sell more units than exist, and users who reserve a unit have 5 minutes to pay, after which the unit returns to stock. The sale page is advertised in advance, so 5 million users are expected to hit "Buy" within the first few seconds, while the rest of the store must keep working normally.
- How many items are in one sale, and do several sales run at once?
- Must users be served strictly first come, first served, or is a fair random draw acceptable?
- Do users need to be logged in with a verified account before the sale starts?
- How long is the payment window, and what happens to unpaid reservations?
- Is it acceptable to tell most users "sold out" quickly rather than queueing them?
- How aggressively should we block bots and scripted purchases?
Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.
- Show the sale page with a countdown and remaining stock.
- Let a user reserve one unit when the sale opens, if stock remains.
- Never oversell: confirmed orders never exceed the units available.
- Hold a reservation for 5 minutes for payment, then release it back to stock.
- Enforce one unit per user.
- Tell users immediately whether they got a unit or the item is sold out.
- Block obvious bots and duplicate accounts.
Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.
- Absorb 5 million users clicking within a few seconds, about 1 million requests per second at peak.
- Zero overselling, even under retries and node failures.
- Respond to every "Buy" click within 1 second, with a reservation or a clear sold out message.
- Isolate the sale so the rest of the store keeps its normal latency.
- Data volume is small: 10,000 reservations and orders; the challenge is throughput, not storage.
- The sale must start exactly on time across all clients, using server time rather than client clocks.
The sale is served by a dedicated, pre-scaled path separate from the normal store. Static content and the countdown come from a CDN, and requests are filtered at the edge by rate limits, bot checks, and a pre-issued sale token, so only legitimate requests reach the origin. Inventory lives in an in-memory store where an atomic script decrements stock and records the user in one step, and successful reservations are written to a queue that creates orders asynchronously in the order database.
Loading diagram...
Before the sale. The sale page, product images, and a countdown script are pushed to the CDN. Logged-in users who open the page receive a signed sale token bound to their user id and the sale id. The inventory store is preloaded with the stock count and an empty set of buyers.
Buy click. At the start time, the page enables the button and the client sends POST /sales/:id/reserve with the token. The edge validates the token signature, applies a per-user and per-IP rate limit, and drops traffic that fails bot checks. The sale service runs an atomic script in the inventory store: if the user is already in the buyers set, return the existing reservation; if stock is zero, return sold out; otherwise decrement stock, add the user, and return a reservation id. Successful reservations are published to the reservation queue and the user is sent to checkout.
Order creation and payment. An order worker consumes the queue and inserts a pending order with a unique constraint on (sale_id, user_id). The user pays through the checkout service within 5 minutes, which marks the order paid.
Expiry. The expiry job finds pending orders older than 5 minutes, marks them expired, and atomically increments the stock counter and removes the user from the buyers set, so another user can reserve the unit.
The first and most important line of defense. It ensures that only a small, legitimate fraction of the spike reaches origin servers.
- CDN edge functions (for example Cloudflare Workers or Lambda at Edge) with a bot management product.
- Signed sale tokens using HMAC, verifiable at the edge without a network call.
Scalability
The CDN absorbs millions of requests per second across hundreds of locations. Once stock is gone, the sale service publishes a sold out flag that the edge caches for a second, so later clicks are answered at the edge without touching origin.
Tradeoffs
Requiring a token issued before the sale stops scripts from hammering the endpoint without first loading the page as a logged-in user. It adds friction for latecomers, which is acceptable for a limited promotion. CAPTCHAs on every click were rejected because they slow down real users at the exact moment speed matters.
Stateless service that turns an admitted click into a reservation or a sold out answer.
- Go or Java service, pre-scaled before the sale rather than relying on reactive autoscaling.
API
GET /sales/:idreturns{ startsAt, serverTime, remaining }.POST /sales/:id/reservewith the sale token returns{ reservationId, expiresAt }or{ status: soldOut }.
Scalability
Each request performs one round trip to the inventory store, so throughput is limited by that store, not the service. Instances are provisioned in advance for the expected peak because autoscaling reacts in minutes while the spike lasts seconds.
Tradeoffs
Answering sold out immediately is preferred over a long queue for most users. With 10,000 units and 5 million clicks, almost everyone will be disappointed, and a fast definite answer is a better experience than minutes of waiting for the same result.
In-memory store holding the remaining stock and the set of users who reserved, updated atomically.
- Redis with a Lua script, so the check, decrement, and buyer set insert happen as one atomic operation.
Diagram
Loading diagram...
Data Model
sale:{id}:stockinteger counter.sale:{id}:buyersset of user ids, or a hash of user id to reservation id.
Scalability
A single Redis primary handles over 100,000 simple scripts per second, which comfortably exceeds the rate admitted by the edge. For very large sales, stock is split into several buckets on different nodes (for example 10 buckets of 1,000), and each request is routed to a bucket by user id hash, falling back to other buckets when its own is empty.
Tradeoffs
Decrementing in Redis instead of the order database avoids row lock contention on a single stock row, which would cap throughput at a few hundred updates per second. The risk is losing in-memory state on failover, so Redis runs with synchronous replication to a replica and append-only persistence, and the order database remains the final authority: the order worker inserts a pending order only while the sale's count of active orders is below its stock limit (checked under a lock on the sale row, which is cheap at 10,000 orders), so a wrong counter can cause a rejected reservation but never an oversold order.
Turns reservations into durable pending orders at a pace the database can sustain.
- Kafka or SQS for the reservation queue; PostgreSQL or MySQL for orders.
Data Model
orders(id, sale_id, user_id, sku, price_minor, status, reserved_at, paid_at)withUNIQUE (sale_id, user_id).
Scalability
Only 10,000 reservations ever reach the queue, so a few workers drain it in seconds without any special database scaling.
Tradeoffs
Writing orders asynchronously decouples the burst from the database. If the queue redelivers a message, the unique constraint makes the insert a no-op. The user sees the reservation before the order row exists, which is safe because the reservation was already granted atomically in the counter store.
Returns unpaid units to stock after the payment window closes.
- Scheduled worker running every few seconds, or delayed queue messages scheduled for 5 minutes after each reservation.
Scalability
At most 10,000 reservations exist, so each sweep is a trivial indexed query.
Tradeoffs
Returning expired units to stock reopens the sale, which can trigger a second, smaller spike. The service publishes the updated remaining count so the edge sold out flag is cleared only when stock is actually available again.