What the interviewer is asking
"Design Ticketmaster" hides two hard problems in a simple shop. The first is correctness: a seat must be sold exactly once, even when thousands of fans tap it in the same second. The second is the on-sale: when a superstar's tour goes on sale, millions of fans arrive in one minute for a few tens of thousands of seats.
The walkthrough follows the 8 steps of the interview framework: scope, data, API, the busy path, numbers, services, scale and deep dives.
1. Scope: what version 1 must do
- Browse events and their dates.
- See the seat map of the venue, with the seats still free.
- Hold the seats you picked for 10 minutes while you pay.
- Pay and get your tickets.
Two rules shape everything else: never sell a seat twice, and keep working during a big on-sale. Park the rest out loud: resale, prices that rise with demand, recommendations, and search beyond a plain list of events.
2. Data: one row per seat, per event
record fields
event id, venue_id, name, starts_at
venue id, name, layout (sections, rows, seats)
seat event_id, seat_id, price, status (free / held / sold), hold_id, held_until
order id, user_id, hold_id, total, payment_id
The key choice is the seat table: one row per seat per event. A count of seats left can't draw a seat map or hold seat 14C, and one row per venue seat would mark Friday's 14C sold for Saturday's show too. A stadium event has 50,000 seat rows; the venue's layout is stored once. A hold needs no table of its own: its seats carry its id and the time it ends.
3. The API: three calls
GET /events/e7/seats
→ 200 OK {: "free", : "free", : "held", …}
POST /holds {: "e7", : ["14C", "14D"]}
→ 201 Created {: "h31", : "10:11"}
→ 409 Conflict if a seat is already taken
POST /orders {: "h31", : "pm_77", : "k9"}
→ 201 Created {: "o55"}
Card details go straight from the app to the payment provider, which gives back a payment id (pm_77); the order keeps that id, never the card number. The order also carries an idempotency key, as in the Idempotency Keys lesson: if the reply is lost and the app sends the order again, the booking service finds k9 and sends back the first reply instead of charging twice.
4. The busy path: map, hold, pay
- Ana's app loads the seat map.
- She picks 14C and 14D, and the app sends
POST /holds. - The booking service marks both seats held for her until 10:11 and answers with hold h31.
- She types her card, and the app sends
POST /orderswith h31 and a key. - The service checks that h31 is still hers, the card is charged, and both seats are sold.
Most days, traffic is mostly browsing. At an on-sale, every fan takes this path at once: that's the path to design for.
5. The numbers: the first minute of an on-sale
From the interviewer: a stadium of 50,000 seats and 2 million fans arriving in the first minute. Assume each app refreshes the seat map every 2 seconds and each fan tries about one hold in that minute.
arrivals 2 million ÷ 60 s ≈ 33,000 fans a second
fans/seat 2 million ÷ 50,000 = 40 fans for every seat
map reads 2 million ÷ 2 s = 1 million seat maps a second
holds 2 million in 60 s ≈ 33,000 holds a second: reads outnumber holds 30 to 1
So 39 fans in 40 leave without a ticket however good the system is, and every read and every hold is about the same 50,000 rows.
6. Services: browsing apart from buying
- Event service: events, venues and prices: read millions of times, changed rarely, so cached hard.
- Booking service: owns the seats table, the database of record for who holds or owns each seat. It takes holds and turns paid holds into orders.
- Payment provider: outside; charges cards and keeps the card details.
7. Scale: a CDN, and one hot server
Event pages are the same for every fan and change only when the organizer edits them, so they go on a CDN (the CDNs lesson), each copy kept a few minutes. Live seat status doesn't belong in those pages: a copy kept for a day would show sold seats as free.
Say the seats table is split across 10 database servers by event id, so each event's seats sit together and a hold of 4 seats is one transaction on one server. At an on-sale, all 50,000 seats of the tour's event are on one server, taking every hold and map read while the other 9 idle. That's a hot spot, and splitting it further would spread holds across servers. Instead, control how many fans reach it: the waiting room below.
8. Deep dive: holds that never double-sell
From the Concurrency and Locks lesson: a hold is one conditional update, so the check and the write are a single step the database runs against the row as it is at that moment.
UPDATE seats SET status = 'held', hold_id = :hold, held_until = now + 10 min
WHERE event_id = 'e7' AND seat_id = '14E'
AND (status = 'free' OR (status = 'held' AND held_until <= now))
Ben and Cy tap 14E at 10:03. Ben's update runs first and changes 1 row; Cy's finds the seat held and changes 0, so her app says "taken". If Ben doesn't pay, his hold runs out at 10:13 and the next update treats 14E as free, with no cleanup job. Paying checks the hold too, before any card is charged: the sale goes through only where hold_id is his and held_until is still ahead of now. When Ben presses Pay at 10:15, while Dee holds 14E, nothing is sold or charged. Since a charge takes a few seconds, a common fix marks the seats as being paid for when Pay is pressed, so the hold can't run out mid-payment.
Groups of seats. Families hold 4 seats side by side, all or none. One update per seat goes wrong: Eli wants 15A to 15D and Fay 15C to 15F, and if each server holds one seat at a time in click order, Eli ends with 15A, 15B, 15C and Fay with 15D, 15E, 15F, and 6 seats are blocked for nothing. Use one transaction: lock the rows lowest seat first, check that every seat is free or its hold has run out, and hold all or none. Locking in seat order, they meet at 15C: whoever gets it first takes all 4, and the other finds it taken. Locking in click order can deadlock instead, Eli waiting for 15D while Fay waits for 15C. The locks last milliseconds; the hold lasts 10 minutes.
9. Deep dive: the waiting room
Say the booking service, with its one hot seats server, serves about 10,000 shoppers at a time, and a shopper takes about 4 minutes. Then it can let in 10,000 ÷ 4 = 2,500 fans a minute. Everyone else waits in a waiting room, a light page that shows each fan their place in line and the wait. It's the burst from the Message Queues lesson: fans arrive far faster than they're let out, and the line takes up the difference.
time let in so far still in line seats left
10:01 2,500 1,997,500 45,000
10:02 5,000 1,995,000 40,000
10:05 12,500 1,987,500 25,000
10:10 25,000 1,975,000 0
With 2 tickets per order, the seats are gone after 25,000 fans, at 10:10. A fan at place 300,000 would wait 300,000 ÷ 2,500 = 120 minutes and find nothing, so a good waiting room tells fans early when the seats are nearly gone. Seats from holds that run out unpaid go to the next fans in line.
Who goes first? Fans who come early wait in a lobby. A common design shuffles everyone in the lobby into a random order at the start, then lets later arrivals join the end, first come, first served. Opening the page at 9:30 or 9:59 makes no difference, so nobody camps on it and a script gains nothing by being early. The trade-off: coming early earns nothing, which some fans find unfair. Ordering by when fans opened the page rewards camping, and "whoever reloads first after 10:00:00" is a millisecond race that bots win.
Tokens and bots. Each fan let in gets a short-lived signed token, and every request to the booking service must carry one, so nobody skips the line by calling the API directly; as in the Authentication and API Keys lesson, it's checked by its signature with no lookup. Bots are stopped at the door: one place in line per account, a CAPTCHA to join, and a rate limit per account (the Rate Limiting lesson).
10. Deep dive: the live seat map
Inside, 10,000 shoppers refresh the map every 2 seconds: 5,000 maps a second, each all 50,000 seats. Reading the database for each would be 5,000 × 50,000 = 250 million row reads a second on the hot server. Instead, rebuild the map once a second and serve every shopper that cached copy: the database reads 50,000 rows a second, and the cache answers 5,000 reads a second, easy for one cache server (about 100,000 a second). You can also push changes to open maps, as in the Real-Time Updates lesson.
A copy up to a second old can show a just-taken seat as free. That's fine, because the hold decides, not the map: the tap gets "taken" and the fan picks again.
What breaks first
- The seats server of a hot event: keep everything else off it. Map reads go to the cache, browsing to the CDN, and only holds and sales reach it.
- The payment provider: slow or down mid-sale. Retries are safe with the idempotency key, and seats being paid for must not be given away.
- Sending tickets: emails go on a queue once the sale is saved, so checkout answers at once.
Summary: the order to follow
- Scope: browse, seat map, hold for 10 minutes, pay; never sell a seat twice.
- Data: one row per seat per event, with its hold id and when the hold ends.
- API:
GET /events/{id}/seats,POST /holds,POST /orderswith an idempotency key. - Busy path: map, hold, pay, at the on-sale.
- Numbers: 2 million fans in a minute for 50,000 seats, 40 per seat; about 1 million map reads and 33,000 holds a second.
- Services: an event service, a booking service, a payment provider.
- Scale: event pages on a CDN; one event's seats on one hot server.
- Deep dives: holds as one conditional update, groups all or none in seat order; a waiting room that lets in 2,500 a minute, shuffles the lobby and hands out signed tokens; a seat map rebuilt every second.
The same pattern carries over to hotel rooms, restaurant tables and flash sales: one exact place that decides, holds that expire, and a gate in front when demand far exceeds supply.