Keep the sale's stock in an in-memory counter that only decrements atomically while stock remains, so it can never go below zero. Put a waiting room in front so only a manageable rate of buyers reaches it, give each successful buyer a short reservation, and return stock from reservations that expire unpaid. Isolate all of it from normal shopping.
hardPattern: Sharding and replicationFrom Devana's Amazon question set
Problem
A limited quantity of a popular item goes on sale at a fixed time, and millions of customers press buy within the same few seconds.
Design it so that you never oversell, never leave stock unsold because of abandoned checkouts or errors, and keep the rest of the site working normally.
POST /sales/{sale_id}/reserve {customer_id, request_id} -> {reservation_id, expires_at} | sold_out | queued
POST /reservations/{id}/confirm {payment} -> {order_id}This is one of Devana's own questions, written in the style of Amazon's interviews. It is not a question leaked from Amazon, and Devana is not affiliated with Amazon.
Requirements
- 10,000 units available.
- 2 million buy attempts within 5 seconds.
- Overselling by even one unit is unacceptable.
- The rest of the site must not degrade.
- Worth asking first: how long may a buyer hold a unit before paying? Ten minutes here.
Scenarios
Scenario 1
2 million buy presses in 5 seconds for 10,000 units. Expected: 10,000 reservations, and everyone else told quickly that it is sold out or that they are in a queue.
The counter answers in microseconds, so the hard part is not the decrement but keeping 400,000 requests a second away from everything else.
Scenario 2
A buyer reserves a unit and closes the tab. Expected: The unit goes back on sale ten minutes later.
The reservation expires, a sweeper adds the unit back to the counter, and the next buyer can take it.
Hints
Open one at a time, and try the problem again after each.
Hint 1
What goes wrong with reading the stock and then writing stock minus one, under concurrency? And with a row lock?
Hint 2
How many requests a second arrive at the peak, and how many of them can possibly succeed?
Hint 3
Which operation guarantees the count never goes below zero without holding a lock across a network round trip?
Hint 4
What happens to a unit when a buyer reserves it and never pays?
A worked answer
Requirements and estimates
2 million attempts in 5 seconds is about 400,000 requests a second, all for one item, against 10,000 possible successes. 99.5% of requests will fail, so failing them cheaply and fairly is most of the design.
Why a database row with a lock fails
Every request contends for the same row. A read followed by a write, without a lock, oversells. With a lock, requests queue on one row, throughput is capped far below the demand, waiting requests hold connections, and the database that other pages depend on slows down too.
Admission control in front
Serve the product page from a CDN before the sale. At the start, a gate at the edge lets through a bounded rate, sized to what the reservation service can handle, and puts everyone else in a waiting room with a position. Once the stock reaches zero, the edge answers "sold out" itself without touching the backend.
Atomic reservations
Hold the stock in an in-memory store and decrement it with one atomic operation that checks and decrements together, such as a Lua script in Redis or a conditional update. A success creates a reservation with an expiry, and the request id makes retries idempotent.
To spread the load, split the 10,000 units across several counters and route each request to one of them; a request that finds its counter empty tries another before reporting sold out.
Confirming orders and returning stock
Payment and order creation happen behind a queue, at a pace the order system can take. A confirmed reservation becomes an order in the durable database. A sweeper returns expired reservations to the counter, and if the in-memory store fails, the counter is rebuilt from the database as units minus orders minus live reservations.
How to say it in the interview
Put the 400,000 requests a second next to the 10,000 possible successes early. It reframes the problem: the decrement is easy, and rejecting 99.5% of requests cheaply and fairly is the design. Then show why the row lock fails, describe the atomic check-and-decrement, and finish with reservations and expiry, which is where naive designs lose stock.
The core in code
The reservation logic without the network: an atomic check-and-decrement, holds that expire, and idempotent retries. In production these steps run inside one atomic operation in the in-memory store; here a single-threaded class stands in for that atomicity.
Python
class FlashSaleInventory:
def __init__(self, units, hold_seconds=600):
self.available = units
self.hold = hold_seconds
self.holds = {} # request id -> expiry time of its reservation
self.sold = set() # request ids whose reservation became an order
def reserve(self, request_id, now):
self.expire(now)
if request_id in self.holds or request_id in self.sold:
return True # a retry of a request that already succeeded
if self.available == 0:
return False # sold out: the count never goes below zero
self.available -= 1
self.holds[request_id] = now + self.hold
return True
def confirm(self, request_id, now):
self.expire(now)
if request_id not in self.holds:
return request_id in self.sold # expired, or never reserved
del self.holds[request_id]
self.sold.add(request_id)
return True
def expire(self, now):
for request_id in [r for r, until in self.holds.items() if until <= now]:
del self.holds[request_id]
self.available += 1 # back on saleJavaScript
class FlashSaleInventory {
constructor(units, holdSeconds = 600) {
this.available = units;
this.hold = holdSeconds;
this.holds = new Map(); // request id -> expiry time of its reservation
this.sold = new Set(); // request ids whose reservation became an order
}
reserve(requestId, now) {
this.expire(now);
if (this.holds.has(requestId) || this.sold.has(requestId)) return true; // a retry
if (this.available === 0) return false; // sold out: the count never goes below zero
this.available -= 1;
this.holds.set(requestId, now + this.hold);
return true;
}
confirm(requestId, now) {
this.expire(now);
if (!this.holds.has(requestId)) return this.sold.has(requestId); // expired, or never reserved
this.holds.delete(requestId);
this.sold.add(requestId);
return true;
}
expire(now) {
for (const [requestId, until] of [...this.holds]) {
if (until <= now) {
this.holds.delete(requestId);
this.available += 1; // back on sale
}
}
}
}Both versions are tested against the same cases, including every example on this page.
expirescans every hold, which is fine for a sketch. In production, keep holds ordered by expiry, such as a sorted set in Redis, and pop from the front.- A retried request returns success without taking a second unit; the request id is the idempotency key.
availablechanges in only two places, a guarded decrement and an expiry, which is what makes "never below zero" easy to argue.
Follow-up questions
Why does a single counter with a database lock fail here?
It queues 400,000 requests a second on one row. Throughput is capped far below the demand, waiting requests hold connections, and the shared database slows every other page.
How do you handle customers who reserve then abandon?
Short holds that expire and return the unit to the counter. The length is a product decision: shorter returns stock sooner, longer gives slower payers a fair chance.
How do you keep this traffic from starving normal shopping?
Separate everything the sale touches: its own edge rules, its own reservation service and in-memory store, and a queue in front of the shared order system, so normal pages never wait behind sale traffic.
How do you make the queue feel fair to customers?
Admit in order of arrival at the edge from a fixed start time, show each customer their position, and limit each account to one reservation. Randomizing order within the first second avoids rewarding whoever has the lowest latency.
Common mistakes
- Reading the stock and writing stock minus one as two separate operations.
- Letting every request reach the database.
- No expiry on reservations, so abandoned checkouts strand stock.
- No idempotency, so a retried request takes a second unit.
Practice it in a mock interview
Reading a solution and explaining one under questioning are different skills. The button below starts this problem as a voice mock interview with Josh, Devana's AI interviewer, with a whiteboard he can read.
Practice this problem in a mock interview
Related problems
- Design an Event and Webhook Platform: An outbox for exactly the committed changes, per-endpoint queues, signed deliveries and replay.
- Design a News Feed: Push to most followers, pull from the few huge accounts, and rank the merged candidates.
- Sharding and replication: the pattern guide, with more practice questions.