Skip to content
Back to the libraryProblem

Design: a flash sale

Sell 5,000 phones to 10 lakh shoppers in seconds, never oversell, say a fast "no" to everyone else and keep the rest of the shop standing. Split stock counters, a waiting room, a sold-out flag at the edge and an order queue.

The full lesson, with a worked example: Design: flash sale. The shopper's side is in Flash sales, and why Add to cart doesn't promise the phone.

Requirements

Functional

  • Before: see the sale page and a countdown.
  • At noon: tap Buy and learn quickly whether you got one.
  • Pay: within a fixed time, say 10 minutes, then get an order confirmation.
  • Waitlist: join it if sold out, in case unpaid phones come back.

Non-functional

  • Never : 5,000 phones means at most 5,000 orders.
  • Don't undersell: phones stuck in abandoned checkouts must go back on sale.
  • Answer fast, even when the answer is no. Most shoppers miss out; tell them in a second.
  • Keep the rest of the shop up: normal orders, search and payments mustn't slow down.
  • Be fair: one per customer, no early starts, as little edge for as possible.

Estimates

Say 5,000 phones in three colours, 10 lakh shoppers, sale at 12:00 noon.

  • Before noon: 10 lakh pages checking the countdown every 30 seconds ≈ 33,000 requests a second. All from a .
  • At noon: say 3 lakh taps on Buy in the first 5 seconds, plus repeat taps ≈ well over 60,000 buy requests a second.
  • What can succeed: 5,000 orders in total. Checkout at about 2,000 buyers a second empties the stock in about 3 seconds.
  • Ratio: about 60 people told "no" for every phone sold.
  • Data: 5,000 orders fits on any database. Order workers saving about 100 a second finish in under a minute.

The difficulty is a on one number, and the job is mostly saying no, cheaply.

API

  • GET /v1/sales/:saleId is a static page from the CDN: product, price, opensAt and a small sold-out flag per colour. The countdown uses server time sent with the page.
  • POST /v1/sales/:saleId/enter puts the shopper in the waiting room. Rejected before opensAt by the server's clock. Returns a place, or a signed pass valid for about 2 minutes, or "sold out".
  • POST /v1/sales/:saleId/buy with the pass and sku. Runs the atomic take. Returns orderId and payBy, or 409 "sold out" or "already bought".
  • POST /v1/orders/:orderId/pay uses orderId as the payment's idempotency key.
  • POST /v1/sales/:saleId/waitlist joins the waitlist; returned phones are offered in order with a 5-minute pass.
  • Adding to cart reserves nothing. Only the buy call takes stock.

Data model

  • Stock buckets (in-memory ): one key per and bucket, such as blue bucket 3 = 500. About ten buckets per SKU, on different shards.
  • Buyers set per sale: account IDs that already took one, checked inside the same atomic step.
  • Take log: an append-only of every take (orderId, account, sku, bucket, at). Lets you rebuild the counters.
  • orders (relational, the system of record): order_id, sale_id, account_id, sku, price_paise, status (HELD, PAID, EXPIRED, CANCELLED), hold_until, payment_id. Unique on (sale_id, account_id) as the last guard against a second phone.
  • Sold-out flags: one per SKU, pushed to the CDN and the waiting room.
  • Waitlist: ordered list of accounts per SKU.

High-level design

Step by step11 steps
  1. 1

    ShopperCDN

    Sale page and countdown, from a copy nearby

  2. 2

    ShopperWaiting room

    Buy, at 12:00:00 by the server clock

  3. 3

    Waiting roomShopper

    Pass, valid for 2 minutes

  4. 4

    ShopperCheckout

    Buy one blue phone (with pass)

  5. 5

    CheckoutStock counter

    Take one if above zero and this account has none

  6. 6

    Stock counterCheckout

    Done. 3,412 left

  7. 7

    CheckoutOrder queue

    Order FS-77120, held for 10 minutes

  8. 8

    CheckoutShopper

    It's yours. Pay within 10 minutes

  9. 9

    ShopperPayments

    Pay about Rs 9,999 (key FS-77120)

  10. 10

    PaymentsOrder queue

    Paid

  11. 11

    Stock counterCDN

    At zero: show Sold out, join the waitlist

Page, countdownTap BuyAdmittedTake oneSold outPay in 10 minNew orderAt their paceSaveSale page on CDNShoppersWaiting roomStock counterCheckoutPaymentsOrder queueOrder workersOrders database
  • Sale page on the CDN: a on , with the sold-out flag.
  • Waiting room: a admits shoppers at checkout's pace with a short-lived pass, and turns people away once the flag says sold out.
  • Checkout: stateless; checks the pass, runs the take, starts the and answers at once. It does not write the full order or call the coupon service while the shopper waits.
  • Stock counter: live count per bucket, changed only by steps.
  • Order queue and workers: each take becomes a message; workers save orders at the database's pace.
  • Payments: a keyed by order number.

Deep dives

The atomic take

  • "Take one only if above zero and this account hasn't bought" as one atomic script. Read-then-write lets two buyers both see "1 left" and both win.
  • The per-customer limit must sit inside the same step. Checked separately, a double tap in the same millisecond gets two.
  • Why not a database row? stock = stock - 1 where stock > 0 is correct, but every buyer waits on the same row. A database changes one row a few hundred to a few thousand times a second; an in-memory store does tens of thousands of atomic steps a second on one key. The row is a .
  • For ordinary products the database row is fine. Give only the few hot deals the fast counter.

Stock buckets

Take oneEmpty? Next oneEvery takeCheckoutBucket 1 (500)Bucket 2 (500)Bucket 10 (500)Record of takes
  • One key on one server is still one limit. Split 5,000 into ten of 500 on different .
  • Route each buyer to a bucket by account number; if it's empty, try the next.
  • Near the end, move the last few units to buckets where buyers still are, or some shoppers briefly hear "sold out" while phones remain elsewhere.
  • Each SKU (colour, storage size) has its own buckets, so blue can sell out while black is still available.

Durability and failover

  • An in-memory store copies to its replica a moment behind. If its server fails, the last few takes can vanish and the copy thinks there are more phones than there are.
  • Every take is also recorded durably: the take log, then the order. The orders are the true record.
  • Treat a as a stop sign: pause that shard's buckets for a few seconds, recount from the orders, correct, resume. Pausing 3 buckets for 5 seconds is far cheaper than overselling 40 phones.
  • A pauses the whole sale if counts look wrong.

Holds, the order queue and payment

  • A take starts a 10-minute hold. The order number exists at once; workers save the full order later. This separates the decision from the paperwork, the same idea as the . See Pattern card: message queues.
  • Expiry by timer: a delayed message 10 minutes later returns the phone if the order is still unpaid. The release checks HELD before moving to EXPIRED, so a timer that fires twice returns only one phone.
  • Returned phones go to the waitlist in order, not back to the open sale, which would start a new rush.
  • Late payment after the phone was resold: matched by key, refunded automatically. Evening compares counter, orders and payments.

Saying no cheaply, and protecting the shop

  • When a SKU's buckets all reach zero, push the sold-out flag to the CDN and waiting room with an active purge. Most of the crowd never reaches a server that costs money.
  • The flag can lag a few seconds; the counter answers "none left" in under a millisecond. Flag for speed, counter for truth.
  • Server clock opens the sale. Phones have and scripts skip the button, so the waiting room and checkout reject every buy before 12:00:00 by their own clock.
  • Isolate the sale path: its own servers, counters and connection pool. on the sale path only.
  • Switch off extras with : recommendations, reviews, coupons. That's . Fix the sale price in advance.
  • Prepare: caches and the CDN, add servers the night before ( takes minutes; the stock is gone in seconds), and run a at several times the expected peak, say 2 lakh requests a second.
  • Bots: registration days ahead, per account, device and network, a before the waiting room, a , random order for early arrivals, and checks after the sale that cancel clusters of accounts run by one buyer.

Trade-offs

  • In-memory counter or database row: fast enough for the rush but can lose recent changes, so it needs a durable record and a pause-and-recount plan. The row is simpler and safer for ordinary products.
  • One counter or many buckets: buckets spread load but need rebalancing near zero.
  • Fast flag or fresh flag: the edge flag saves lakhs of requests but lags a few seconds. Use both flag and counter.
  • Hold length: 10 to 15 minutes. Shorter undersells slow payers; longer leaves stock idle in abandoned holds.
  • Reserve at cart or at buy: reserving at cart locks stock for people who never pay. Take stock only at buy.

Interview talking points

  • Find the one hot number early, and say most of the work is saying no.
  • Estimate: tiny data, a huge spike, about 60 no's per yes.
  • Atomic take with the per-customer limit inside, split into buckets, backed by a durable record.
  • Waiting room and edge flag, with the counter as final truth.
  • Expiring holds, idempotent release, payments keyed by order number, the order queue.
  • Isolation from normal shopping, pre-scaling, load tests, a kill switch, bot defences.

Follow-up questions

Why not a database transaction with a row lock? It's correct and fine for normal products, but at tens of thousands of buyers a second on one row the lock is the bottleneck. Keep the database as the record and do the fast step in memory.

How do the counter and the orders stay in agreement? Every take is recorded durably, and orders are unique per account per sale. On failover, pause, recount from orders and resume; reconcile counter, orders and payments after the sale.

How is this different from ticket booking? Tickets are distinct seats, so contention spreads over many rows. A flash sale has identical items and one number everyone changes, so you split it into buckets, and the sold-out path becomes the busiest path. Compare Design: ticket booking.

What if a crore units sit across a thousand products? Most products aren't hot, so ordinary per-SKU inventory works. Give only the few deal items the fast counters, buckets and waiting room.

A helper service, like coupons, falls over at noon. What happens? With a stripped checkout, nothing: price is fixed in advance and address comes on the payment screen. Anything still called gets a short timeout and a .