Design Ticketmaster
You're asked to design a ticketing platform like Ticketmaster. Users browse events, pick specific seats from a venue map, hold them while they enter payment details, and complete a purchase. The system must never sell the same seat twice, and it has to survive the on-sale moment, when a stadium tour goes live and hundreds of thousands of people load the same seat map in the same second.
An event with fifty thousand seats might see two million people arrive within a minute of the on-sale. The seat inventory is tiny and the demand for it is enormous, which is a very different shape from most systems you'll design.
Clarifying the requirements
- Do users pick specific seats, or just a quantity? Assigned seating means a row per seat and a hold per seat. General admission is a counter. Most versions of this question want assigned seating, since it's harder.
- How long is a hold? Five to ten minutes is standard. It has to cover entering card details and a payment round trip, without letting abandoned carts strand inventory.
- How extreme is the burst? Ask for the ratio between steady-state and on-sale traffic. A thousand-fold spike drives a different architecture than a tenfold one.
- Is there a queue or waiting room? Most large ticketing platforms use one. Confirming this early makes the rest of the design far simpler.
- What about resale and transfers? Usually out of scope, but worth naming so the interviewer can steer.
Assume: assigned seating, ten-minute holds, an extreme on-sale burst, and a virtual waiting room.
Back-of-envelope numbers
- Browse traffic at an on-sale:
- Purchases: total, so at most for the entire event
- Read-to-write ratio during the on-sale: in attempts, and far higher in requests since users reload repeatedly
- Seat inventory: per event, which fits comfortably in memory
The two numbers above pull in opposite directions. Thirty thousand reads a second is a caching problem, and fifty thousand total writes is a correctness problem. Recognizing that they need separate treatment is the central insight in this design.
High-level architecture
- Waiting room. Admits users into the purchase flow at a rate the booking path can absorb.
- API gateway. Routes browse traffic to cache and purchase traffic to the booking service.
- Seat map cache. Serves the seat availability view, updated continuously and read constantly.
- Booking service. The only writer to inventory, performing the atomic claim.
- Inventory DB. One row per seat, holding its state and hold expiry, partitioned by event.
- Payment provider. External and slow, which is precisely why the hold exists.
- Expiry sweeper. Releases holds whose deadline has passed.
Deep dive 1: claiming a seat without selling it twice
Every correctness question in this design reduces to one operation: moving a seat from available to held, in a way that cannot interleave with another attempt on the same seat.
A conditional update does this in one statement, and the row count it returns is the answer:
SQLUPDATE seats
SET status = 'HELD', held_by = ?, expires_at = now() + interval '10 minutes'
WHERE seat_id = ? AND (status = 'AVAILABLE'
OR (status = 'HELD' AND expires_at < now()));
One row updated means the seat is yours. Zero rows updated means someone else claimed it first, and the user sees it disappear from the map. There's no window between checking and claiming for a second request to slip into, because the database evaluates the condition and applies the write as one operation.
Two details in that statement are worth calling out. The expires_at < now() clause implements lazy expiry: an expired hold is treated as available by the very query that would claim it, so correctness never depends on the sweeper running on time. And the whole thing is a single-row operation, which means it stays fast under contention and doesn't require a transaction spanning multiple statements.
Deep dive 2: surviving the on-sale burst
The seat map is read tens of thousands of times a second while the inventory is written at most fifty thousand times in total. Serving both from the same path would put read load on the component that has to stay correct.
Serve the seat map from cache, refreshed from the inventory database on a short interval rather than on every write. A user seeing a seat that was taken half a second ago is acceptable, because the atomic claim rejects them when they try. Staleness in the map costs a small amount of user frustration, whereas serving every map read from the primary database costs availability.
Admit users through a waiting room so that the booking path receives a controlled rate of requests rather than everything at once. On arrival a user is placed in a queue, given a position, and admitted when capacity allows. Two things make this work well:
- The queue position should be assigned when the user arrives, not when they're admitted, so refreshing the page doesn't lose their place.
- Admission is a token with a deadline, which bounds how long an admitted user can occupy capacity without buying.
This is worth presenting as a load-shedding decision rather than a product feature. The alternative — letting two million requests reach the booking service — produces timeouts for everyone rather than a fair, ordered experience for the people who will actually get tickets.
Deep dive 3: from hold to confirmed purchase
Once a seat is held, the purchase spans a database the system owns and a payment provider it doesn't, so a single transaction across both is not available. The workflow instead moves through explicit states, and every failure path has a defined destination.
- Payment succeeds: the seat moves from held to confirmed, the order is recorded, and the ticket is issued.
- Payment fails: the hold is released immediately rather than waiting for expiry, so the seat returns to the map while demand is still high.
- Payment times out with no answer: the seat stays held until expiry, and a reconciliation job queries the provider for the final status. This is the case that produces the worst incidents if left undefined.
- The user abandons: the hold expires and the seat becomes available again through lazy expiry.
Idempotency is what makes the retries safe. The client sends an idempotency key with the purchase request, and the payment provider is called with that same key. A retry after a timeout returns the original result rather than charging twice, which matters here because a double charge on a concert ticket is a support incident and a chargeback rather than a minor inconvenience.
Deep dive 4: fairness and bots
Ticketing is unusual in that a large share of the demand at an on-sale is automated, and a design that ignores this is incomplete. This is where the conversation moves from mechanism to policy.
- Per-account and per-payment-method limits are the most effective control, since they cap the value of any one bot account rather than trying to detect it.
- Rate limiting by IP alone works poorly, because a residential proxy pool defeats it and a corporate NAT gets penalized unfairly.
- The waiting room is itself a fairness mechanism when position is assigned at arrival, since it turns a race for milliseconds into an ordered queue.
- Randomized admission from the pool of users who arrived within a short window is what some platforms use instead, on the argument that rewarding the fastest network connection isn't fairness either.
The point to make in an interview is that none of these eliminate bots. They change the economics, and the design should say which trade it's making rather than claiming the problem is solved.
Common pitfalls
- Checking availability and then claiming in a separate statement. This is the read-check-write race, and at an on-sale it will fire.
- Selling only after payment completes. The seat is unprotected for the seconds or minutes the payment takes.
- Selling before payment with no expiry. Abandoned checkouts permanently remove seats from sale.
- Depending on a sweeper for correctness. If cleanup lags, seats sit unavailable. Lazy expiry in the claim query removes that dependency.
- No idempotency key on payment. A network retry becomes a double charge.
- Serving the seat map from the inventory primary. Read traffic then competes with the writes that have to succeed.
Leveling the answer
Related lessons
Reserve rooms across date ranges, where inventory is an interval rather than a single unit.
Take a multi-item cart through reservation, payment, and fulfillment without overselling.