Design Ticketmaster / Event Ticketing
Browsing is an ordinary read-heavy site. The interview is decided in the first minutes of a popular on-sale, when millions of fans want the same 50,000 seats and no seat can be sold twice.
1 · Requirements
The Prompt
Sell reserved seats for concerts and sports. Popular on-sales draw millions of fans at once, and no seat can ever be sold twice.
Functional
- Search and browse events by name, city, and date
- Show a live seat map for each event
- Hold chosen seats for a few minutes during checkout
- Pay and receive tickets
Non-Functional
- A seat is never sold twice
- An on-sale with 10M fans stays up and treats fans in arrival order
- Seat maps load in under a second and are at most a few seconds stale
2 · Estimates
Drill This Phase First
Show the Answer and Estimates
Answer
About 10 minutes
50,000 seats at 2.5 an order is 20,000 orders. At 2,000 admitted fans a minute, that takes 10 minutes, assuming each admitted fan buys.
- Browsing
- 100M event page views a day: about 1,200 a second, served mostly from cache and the CDN
- Bookings
- About 1M tickets a day outside on-sales: roughly 12 a second, easy for one SQL primary per shard
- On-Sale
- 10M fans for 50,000 seats. At about 2.5 seats an order, that is 20,000 orders
- Waiting Room
- 10M fans checking their place every 10 seconds: 1M requests a second, handled at the edge and in Redis
- Admission
- Admitting 2,000 fans a minute keeps checkout near 33 orders a second and sells the show in about 10 minutes
- Seats
- About 50,000 live events with 20,000 seats each: 1B rows at 100 bytes, roughly 100 GB in SQL sharded by event_id
3 · API
GET /events?q=&city=&date= → { events[] } (search index)
GET /events/:id/seats → { sections[], seats[{ id, status, price }] } (cached a few seconds)
POST /events/:id/holds { seat_ids[] } → { hold_id, expires_at }, or 409 if any seat is taken
POST /holds/:id/checkout { payment_token, idempotency_key } → { order_id, tickets[] }
GET /queue/:event_id/position → { position, eta_s, admit_token? }Data Model
seats (SQL, sharded by event_id):
event_id, seat_id (PK)
section, row, number
price_cents : int
status : enum(available|held|sold)
hold_id : uuid?
hold_expires_at : timestamp?
orders (SQL, same shard as the event):
order_id (PK) : uuid
user_id : uuid
event_id : uuid
seat_ids : list<string>
status : enum(pending|paid|refunded)
idempotency_key : string, unique
events (SQL, mirrored to the search index):
event_id, name, venue, city, starts_at, on_sale_at4 · High-Level Design
5 · Deep Dives
Never Sell a Seat Twice
A hold is one conditional update: mark the seats held only where they are available or their hold has expired, then check that every requested row changed. Two fans racing for seat A12 cannot both win, because the database row decides. A Redis lock with a TTL is faster, at the cost of a second source of truth that can drift from the database.
Holds That Expire
Checkout gets 10 minutes. The hold stores hold_expires_at, and every read treats an expired hold as available, so no background sweeper has to run on time for a seat to come back. If a hold expires while payment is in flight and another fan takes the seat, checkout fails and the charge is voided.
The Waiting Room
An on-sale is a stampede on one event's shard. Queue fans at the edge, give each a signed position, and admit them at the rate checkout can serve. Admitted fans carry a short-lived token that the booking service checks, so calling the API directly skips nothing.
6 · Wrap Up
The Short Version
Hold seats with a conditional update that sets an expiry, then confirm them in a SQL transaction after payment. A virtual waiting room meters hot on-sales, and event search lives in a search index.