Skip to main content

Design Ticketmaster

Premium

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 may attract two million people in the first minute of an on-sale. Let's separate the enormous browse load from the small inventory that requires strict transactional correctness.

Clarify 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. Confirm this early because it limits how much traffic reaches the booking path.
  • What about resale and transfers? Ask whether they are out of scope so you can keep the design focused on the initial sale.

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: 2M users÷60 sec≈33k seat-map requests/sec2\text{M users} \div 60\text{ sec} \approx 33\text{k seat-map requests/sec}
  • Purchases: 50k seats50\text{k seats} total, so at most 50k successful writes50\text{k successful writes} for the entire event
  • Read-to-write ratio during the on-sale: 2M÷50k=40:12\text{M} \div 50\text{k} = 40:1 in attempts, and far higher in requests since users reload repeatedly
  • Seat inventory: 50k seats×200 B≈10 MB50\text{k seats} \times 200\text{ B} \approx 10\text{ MB} per event, which fits comfortably in memory

Thirty thousand reads per second call for caching, while fifty thousand total successful writes call for strict correctness. Design those paths separately.

High-level architecture

User

① Waiting room

② API gateway

③ Seat map cache

④ Booking service

⑤ Inventory DB

⑥ Payment provider

⑦ Expiry sweeper

Components
  1. Waiting room. Admits users into the purchase flow at a rate the booking path can absorb.
  2. API gateway. Routes browse traffic to cache and purchase traffic to the booking service.
  3. Seat map cache. Serves the seat availability view, updated continuously and read constantly.
  4. Booking service. The only writer to inventory, performing the atomic claim.
  5. Inventory DB. One row per seat, holding its state and hold expiry, partitioned by event.
  6. Payment provider. External and slow, so the system holds a seat while payment completes.
  7. Expiry sweeper. Releases holds whose deadline has passed.
A waiting room admits users at a controlled rate. Seat maps are served from cache, while the booking service is the only component allowed to claim a seat.

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:

SQL
UPDATE 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.

Call out two details in that statement. The expires_at < now() clause provides lazy expiry, so the claim query can reclaim an expired hold without waiting for the sweeper. The claim is also a single-row operation, which stays fast under contention and avoids 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.

Present the waiting room as load shedding rather than only a product feature. Letting two million requests reach the booking service would produce timeouts for everyone. The queue instead admits users at a rate the purchase path can handle.

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

A large share of on-sale demand may be automated. After covering the purchase mechanics, explain which fairness controls the platform will enforce.

  • 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.

None of these controls eliminates bots. Explain how they change the economics and which fairness tradeoff the design makes.

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 signals

Mid-levelModels seats with explicit states, uses a hold with an expiry between selection and payment, and knows that concurrent claims need database-level protection.
SeniorMakes the claim a single conditional update and uses the affected row count as the result, implements lazy expiry so correctness doesn't depend on a sweeper, and defines each payment failure path including the timeout case.
Staff+Separates the read path from the write path with a cached seat map and a waiting room used as load shedding. Handles payment idempotency and reconciliation, and treats bot mitigation as a policy trade-off rather than a solved problem.
Design a Hotel Booking SystemHard

Reserve rooms across date ranges, where inventory is an interval rather than a single unit.

Design E-commerce CheckoutHard

Take a multi-item cart through reservation, payment, and fulfillment without overselling.