Ticket booking, where two people want the same seat
The whole design is one guarantee — a seat is sold at most once — held under the worst traffic the system will ever see, in the first minute of an on-sale.
The brief
Users browse an event's seat map, pick seats, hold them while they pay, and receive tickets.
Popular events go on sale at an announced time, so demand arrives all at once rather than spread across the day.
Requirements
Functional
- Show which seats are available for an event
- Hold selected seats for one user for a limited time while they pay
- Confirm the booking when payment succeeds; release the hold when it fails or expires
- Never sell the same seat twice
Non-functional
- A double-sold seat is a correctness failure with a person standing at a door; no amount of speed excuses it
- The seat map may be a few seconds stale; the hold must not be
- Survive an on-sale spike without the database becoming the queue
Back-of-envelope
Assume
- A large venue: 50,000 seats
- An on-sale that attracts 500,000 people in the first ten minutes
- A hold lasts 10 minutes; checkout takes a user about 3 minutes
- Each person refreshes the seat map every few seconds while waiting
Therefore
- Demand is ten times supply, so at least 90% of the people who arrive will not get a seat. Most of the traffic is people who will be told no — the design has to make telling them cheap.
- 500,000 people refreshing every 5 seconds is ≈100,000 seat-map reads a second. The seat-map read, not the booking, is the load.
- Bookings are bounded by the venue: at most 50,000 successful holds in total, however many people try. The write path is small; it only has to be correct.
- With a 10-minute hold and a 3-minute checkout, abandoned holds keep seats off sale for up to 10 minutes each. That number, not the database, is what decides how fast the event sells out.
Change the hold duration and watch the last line move: a 5-minute hold returns abandoned seats twice as fast and fails more slow payers. That trade-off is a product decision the estimate makes visible.
The interface
What is stored
event_id · seat_id · status (AVAILABLE | HELD | SOLD) · hold_id · held_until · PRIMARY KEY (event_id, seat_id)The hold is a conditional UPDATE on this row — set HELD only WHERE status = AVAILABLE, or status = HELD AND held_until < now() — and the number of rows affected says who won. No read-then-write, so no race between them.
hold_id · user_id · event_id · expires_at · state · INDEX (state, expires_at)The index serves exactly one query: the sweeper looking for expired holds to release. Without it the sweeper scans every hold ever made, and it runs every few seconds.
ticket_id · event_id · seat_id · booking_id · UNIQUE (event_id, seat_id)The unique constraint is the last line of defence. If every other part of the design has a bug, the second INSERT for the same seat fails in the database rather than at the venue door.
The design
The decisions
Each of these could go the other way. The choice, the reason, and what it costs — a design that lists only what it chose teaches the choice; one that lists what it gave up teaches the judgement.
What breaks first
In order. Each names what you would actually observe, and each fix carries its cost.
When something fails
Scaling it
Each step is triggered by a number, not a feeling — and carries what it costs.
What gets probed
The design is the easy half. These are where the conversation goes, and each has a defensible answer above.
- Two users click the same seat in the same millisecond. Walk through exactly which statement decides who gets it.
- A payment succeeds eleven minutes after the hold was created. What does the user see, and is anyone's money at risk?
- Why is the seat map allowed to be stale when the hold is not?
- The event shows sold out, but only 70% of tickets are sold. What is happening?