In this article
Before you start
This page belongs to the distributed systems interview guide, and it uses the same estimation habits as the back-of-the-envelope estimation page.
Why interviewers ask this
Ticketing, flash sales and hotel booking are one problem with three costumes: many people want a small, fixed supply at the same time, and each unit can be sold only once. Candidates report being given versions of this prompt, such as designing Ticketmaster with a focus on the queue, a ticket-booking service, or a flash sale with limits on traffic and stock.
The prompt separates candidates by what they do with the collision. A weak answer draws services and a database and says "add a lock." A strong answer says where the winner of each seat is decided, what happens when a buyer disappears halfway, and how a retried payment avoids a second charge. A Staff-level answer adds the protection of the database itself: a crowd is slowed before it arrives, because correctness checks are useless if the system is down.
Design Ticketmaster, a flash sale or hotel booking from first principles
- Virtual waiting room
- A virtual waiting room is a queue in front of a sale page that lets buyers into the shopping flow at a fixed rate chosen to match what the backend can handle, and holds everyone else on a lightweight page until their turn.
- Seat hold
- A seat hold is a temporary claim on one seat or room-night that is written to the database for one buyer and expires on its own if payment does not finish, so an abandoned checkout returns the seat to the pool.
- Fencing token
- A fencing token is a number that increases each time a client acquires a lock, sent with every write so that the storage layer can refuse any write that carries an older number than one it has already seen.
In plain words
Picture a theatre box office with one clerk and one seating chart. The clerk looks at the chart, writes "held" next to a seat, and sets a timer. Two buyers cannot get the same seat, because there is one chart and one pen.
The analogy breaks in one place. A real site has many servers, and they cannot see each other's desks. So the chart must live in one shared place, the database, and the check and the write must be one indivisible action there.
In one precise sentence: the database must decide, in a single atomic step, which buyer gets each seat.
Step 1: A waiting room admits buyers at a fixed rate
A waiting room protects the database by limiting how many buyers shop at once, and the limit must be enforced, not just displayed. A flash sale of 6 million burgers in 10 minutes, as one candidate-reported prompt puts it, averages 10,000 orders per second,1 and the opening second is far higher than that average, which is why the crowd is slowed before it arrives.
The queue works like this. Each account gets one place in line, so duplicate tabs and bot accounts add no extra places. Buyers who arrive before the sale opens get random positions, not a race of first requests. Queue position lives in a fast store, and the waiting page polls a cheap endpoint instead of the database.
At the head of the line the service issues a signed admission token with an expiry, and the checkout service verifies it on every call, so nobody skips the line by calling the application programming interface (API) directly. You choose the admit rate from what the database can safely write; a distributed rate limiter applies the same idea at the gateway. Next: who gets each seat.
Step 2: Hold the seat with a conditional write, not a bare lock
A seat hold is claimed by one conditional write: set the seat to held for this buyer, with an expiry time, only if its status is still available. The database runs the check and the write as one step, so exactly one of two simultaneous buyers succeeds and the other receives a refusal. In Structured Query Language (SQL) this is one update statement whose condition is status = 'available', and it reports how many rows it changed; a document store offers the same idea as a conditional write.
The common error is a read followed by a separate write, so two buyers both read "available" and both write. With two buyers and four steps there are six possible orderings, and four of them double-book the seat.1
A lock with a lease does not close this gap alone. A lease is a lock that expires on its own after a set time. Kleppmann (2016) describes a client that acquires a lease with token 33, pauses for a long time, and wakes up after the lease has expired and client 2 has taken it with token 34. His fix: "you need to include a fencing token with every write request." The storage service then rejects the stale token 33. The seat claim above needs no separate lock because the database is the storage that does the checking.
Step 3: Payment retries carry an idempotency key
Payment is the step where a retry can cost real money, so every payment request carries one idempotency key. Networks time out, and a client that does not know whether its charge went through will try again. With a key, the second request returns the first result instead of charging twice.
Input: the buyer's hold and a key created once per checkout, which is one logical payment. Output: one charge and one stored result per key. The client sends the same key on every retry of that payment, and makes a new key only when the buyer starts a genuinely new payment. The server stores the key with the outcome, in the same transaction that records the payment. The payment system design page goes deeper on this, and a webhook from the payment provider is the usual way the provider tells you the final outcome after your request has timed out. You now have a claim that cannot be duplicated and a charge that cannot be repeated.
Step 4: The booking is confirmed once, or the hold expires
Confirmation turns a hold into a booking only when payment succeeded and the hold is still the buyer's. A safe order is authorise, then capture: authorise the card to hold the funds, confirm the seat with a second conditional write (held by this buyer to booked), then capture. If the hold lapsed and another buyer took the seat, the write changes no rows, the authorisation is released, and the buyer sees a clear message; charging first would force a refund.
The expiry is the time to live (TTL) of the hold, the number of seconds a hold stays valid before it counts as free. A short TTL frees seats from abandoned checkouts quickly; a long TTL gives slow payers room, so choose it from how long a normal checkout takes. For a hotel, a room type per night is often sold as a count, so the claim is a conditional decrement on (room_type, night) that refuses at zero; a named room uses one row per room-night, and a three-night stay claims all its rows in one transaction.
The data model and the read path
The minimal model has four tables. An event row describes the sale. A seat row holds status, holder and expires_at. A booking row ties a seat to a buyer after payment. A payment row stores the idempotency key under a unique constraint, with the outcome. The reads and the writes differ. The seat map or availability view is read far more often than it changes, so serve it from a cache or replica that may be a few seconds stale. A seat shown as free can already be taken, and that is acceptable because only the conditional write decides. A refusal tells the page to refresh the map. Reads can be loose; the claim is strongly consistent.
Step-by-step description
- Buyers enter at a rate the database can bear.
- One conditional write picks the winner of a seat.
- One idempotency key makes retries safe.
- The seat is booked once or returned to the pool.
- A waiting room admits buyers at a fixed rate.
- A seat hold is a conditional write that expires.
- Payment carries an idempotency key.
- A booking is confirmed once, or the hold is released.
A waiting room admits buyers at a fixed rate. A seat hold is a conditional write that expires. Payment carries an idempotency key. A booking is confirmed once, or the hold is released.
Knowing it is half the job. Saying it is the other half.
Interview Clozo is an AI interview copilot for real interview rounds: it hears the question and puts a strong answer on your screen, and you say it in your own words. See the features or how it works.
Start Free Trial →Worked example
The numbers below use an assumed venue and are illustrative, not measurements of any real sale. The venue has 50,000 seats, the crowd is 500,000 buyers, the waiting room admits 1,000 buyers per second, and each buyer holds two seats.1
At that rate the crowd needs 500 seconds to admit. Slower rates take longer: 2,000 seconds at 250 admits per second, 1,000 seconds at 500, and 250 seconds at 2,000.1 The admit rate is a trade between waiting time and database load.
Seats run out quickly. If every admitted buyer holds two seats, 25,000 buyers take all 50,000 seats, which takes 25 seconds at 1,000 admits per second, while the other 475,000 buyers are still waiting.1 This is why the page for a sold-out event should say so as soon as the last seat is held, and why expired holds matter: they return seats to the pool for the people still in line.
| Quantity | Value (illustrative or computed) |
|---|---|
| Average orders per second, 6,000,000 orders in 600 seconds | 10,000 |
| Seconds to admit 500,000 buyers at 1,000 per second | 500 |
| Buyers needed to hold all 50,000 seats at two seats each | 25,000 |
| Seconds to hold every seat at 1,000 buyers per second | 25 |
| Orderings of two read-then-write buyers that double-book | 4 of 6 |
| Orderings that double-book with a conditional write | 0 of 6 |
| Token 33 write after token 34 was accepted | rejected |
All values in the table are computed in the data script.1
Text description of the diagram
- Admitting 500,000 buyers takes 500 seconds at 1,000 admits per second.
- Slower rates take longer: 2,000 seconds at 250 admits per second, 1,000 seconds at 500, and 250 seconds at 2,000.
Trade-offs and edge cases
Each way of claiming a seat trades simplicity against reach. A row lock inside the database is simple and safe for short transactions, but every waiting buyer ties up a connection, so a multi-minute hold is a status column, never an open lock. A conditional write needs no lock and refuses the loser, which suits one row per seat; database interview questions cover the isolation levels behind these rules. A lease in a separate lock service lets many services coordinate, and it needs a fencing token because, as Kleppmann (2016) explains, a paused client can wake up holding an expired lease.
Text description of the diagram
- A row lock inside the database is simple and safe for short transactions.
- A conditional write needs no lock and refuses the loser.
- A lease in a separate lock service needs a fencing token.
Kleppmann advises that if locks are needed for correctness, use a consensus system such as ZooKeeper or a database with reasonable transactional behaviour, and he advises against Redlock for that purpose. The practical reading for this design: keep the final decision in the database that stores the seats. A cache or lock service can reduce load, but it should never be the only thing standing between two buyers and one seat.
Edge cases a strong answer names:
- Payment after expiry. A slow payment can finish after the hold lapsed and the seat was resold. Detect it at the confirmation write and refund.
- Hot rows. A single stock counter for one product takes every write. Use a conditional decrement, and consider splitting the counter into several rows for a very hot item.
- Overbooking on purpose. Some businesses sell past capacity deliberately; then it is a business rule, and the design states the limit.
- Retry storms. Clients that retry quickly on errors make overload worse; the waiting room and a backoff rule limit that.
How to answer it in an interview
How would you design Ticketmaster, or a flash sale, so no seat is sold twice?
I'd use three guards. First, a virtual waiting room admits buyers at a rate the database can bear, so the crowd never reaches it together. Second, each seat is claimed with a conditional write, set held where status is available, with an expiry, so the database picks the winner and a paused client cannot create two. Third, payment carries an idempotency key, and the booking is confirmed once, or the hold expires and the seat returns. The trade-off is that holds can lock out buyers who abandon, so I keep the hold short.
The spoken structure: shape the load, decide the winner in the database, make payment safe to retry, then name the hold-length trade-off.
Common mistakes
- "I'll check if the seat is free, then book it." (wrong) The check and the write are two steps, so two buyers can pass the check. (right) "I'll claim it with one conditional write and look at how many rows changed."
- "A distributed lock prevents double booking." (wrong) A lease can expire while its holder is paused. (right) "The write carries a fencing token, or the database enforces the condition itself."
- "On a timeout I'll just retry the payment." (wrong) The first charge may have succeeded. (right) "I create one idempotency key per checkout and send the same key on every retry of that payment, so a retry returns the first result."
- "Holds never expire; we release on cancel." (wrong) Abandoned checkouts lock seats forever. (right) "Every hold has a time to live and is released when it passes."
Follow-up questions
How would you prevent double booking if two users try to book simultaneously?
Make the claim one atomic conditional write in the database, so exactly one request changes the row and the other is refused. Add a unique constraint on the seat and date as a second line of defence.
How can two people edit the same resource at once?
Use optimistic concurrency: each record carries a version number, and a write succeeds only if the version is still the one the editor read. The loser gets a conflict and must merge or retry. See the concurrency interview questions.
How would you design a reservation and payment system for a parking garage?
The shape is the same: one row per space and time slot, a conditional write to hold it, a time-limited hold while the driver pays, and an idempotent charge. Release holds that expire so unpaid spaces return to the pool.
How do you handle a flash sale where stock is limited and traffic is huge?
Throttle at the front with a waiting room, decrement stock with a conditional write that refuses at zero, queue the remaining order work, and accept that the downstream steps are eventually consistent while the stock count is exact.
What happens if payment succeeds after the hold has expired?
The confirmation write fails because the seat is no longer held by that buyer. With authorise-then-capture, the system releases the authorisation and tells the buyer, so nothing was charged; if the money was already captured, it refunds. The seat belongs to whoever holds it now.
What changes if the hold time doubles?
Seats stay locked twice as long when buyers abandon, so fewer seats return to the line, but slow payers fail less often. Choose it from the checkout time you observe, and watch the abandon rate. The API design interview questions cover the refusal responses a client should handle.
- Shape the crowd with a waiting room so the database sees a rate it can bear.
- Decide each seat with one atomic conditional write, never a read followed by a write.
- A lease can expire during a pause, so a lock needs a fencing token or a database check behind it.
- Create the idempotency key once per checkout and reuse it on every retry of that payment, so retries do not charge twice.
- Holds expire, and confirmation is a second conditional write.
Sources
- How to do distributed locking — Martin Kleppmann (personal blog), 8 Feb 2016
- PostgreSQL documentation: UPDATE — PostgreSQL Global Development Group, 26 Oct 2026
- Stripe API documentation: Idempotent requests — Stripe, 26 Oct 2026