Most of Ticketmaster is an ordinary catalogue: browse events, view a seat map, read details. Then a hot show goes on sale, fifty thousand people hit “buy” in the same second for ten thousand seats, and the whole design is suddenly about one thing — selling each seat exactly once under brutal contention. That is the problem the interviewer cares about, so spend your time there.
Understanding the Problem
Functional Requirements
- Browse/search events — by artist, venue, date
- View seat availability — a seat map for an event
- Reserve seats — hold specific seats while the user checks out
- Purchase — pay and confirm the booking
- Release on timeout — unpaid holds free up after a few minutes
Non-Functional Requirements
| Requirement | Target |
|---|---|
| No double-booking | Absolute — a seat is sold at most once |
| Booking consistency | Strong (the seat state is the source of truth) |
| Browse/search | Eventually consistent, highly available |
| On-sale spike | Handle 50K+ concurrent buyers for a popular show |
| Latency | Reserve a seat in < 500 ms once admitted |
Estimation. The steady-state load is modest — most events sell slowly. The system is defined by its peak: a popular on-sale is a textbook thundering herd, where demand (50K/s) massively exceeds supply (10K seats). Two sub-problems fall out: correctness under concurrency (don’t oversell) and load (don’t let the stampede melt the inventory database).
Core Entities and APIs
Data Model
A seat moves through a small state machine: AVAILABLE → HELD → BOOKED (with HELD → AVAILABLE on expiry).
CREATE TABLE events (
id UUID PRIMARY KEY,
name VARCHAR(200),
venue_id UUID,
starts_at TIMESTAMP,
on_sale_at TIMESTAMP
);
CREATE TABLE seats (
id UUID PRIMARY KEY,
event_id UUID REFERENCES events(id),
section VARCHAR(20),
row VARCHAR(10),
number VARCHAR(10),
price DECIMAL(10,2),
status VARCHAR(10) DEFAULT 'AVAILABLE', -- AVAILABLE | HELD | BOOKED
held_by UUID, -- user holding it, if any
hold_expires_at TIMESTAMP -- when the hold auto-releases
);
CREATE INDEX idx_seats_event_status ON seats (event_id, status);
CREATE TABLE bookings (
id UUID PRIMARY KEY,
user_id UUID,
event_id UUID,
seat_ids UUID[],
status VARCHAR(10), -- PENDING | CONFIRMED | CANCELLED
created_at TIMESTAMP
);API Design
GET /api/v1/events/{id}/seats
response: { seats: [{ seat_id, section, row, number, price, status }] }
POST /api/v1/events/{id}/holds
body: { seat_ids: [...] }
response: { hold_id, expires_at } # 409 if any seat is already taken
POST /api/v1/holds/{hold_id}/purchase
body: { payment_method_id }
response: { booking_id, status: "CONFIRMED" }
DELETE /api/v1/holds/{hold_id} # explicit releaseThe two-step hold then purchase flow is deliberate: it reserves the seat for the duration of checkout so the user doesn’t lose it while entering payment, without marking it permanently sold before they pay.
High-Level Design
The Booking Service owns seat state and is the single source of truth. Browsing and the seat map are served from caches/replicas (eventually consistent is fine — a seat shown as free that turns out to be taken is handled at hold time). The hold and purchase operations go to the authoritative inventory store, which must be strongly consistent.
Deep Dive: Reserving a Seat Without Double-Booking
This is the crux. Two users must never both hold or book the same seat. The tool is a conditional, atomic state transition — only flip a seat to HELD if it is currently AVAILABLE:
-- Atomic hold: succeeds for exactly one racer
UPDATE seats
SET status = 'HELD', held_by = :user, hold_expires_at = now() + interval '5 minutes'
WHERE id = :seat_id AND status = 'AVAILABLE';
-- rows_affected = 1 → you got it; 0 → someone else didBecause the database applies row updates serially, exactly one of two concurrent racers sees rows_affected = 1. That is optimistic concurrency on the seat row — no application-level read-then-write race. (The same pattern underlies inventory, flash sales and the top bid in an auction; see the Dealing with Contention lesson.)
Alternatively, hold the seat in Redis with an atomic SET seat:{id} user NX EX 300 (set-if-not-exists with a 5-minute TTL), which gives the hold a self-expiring lock for free and keeps the hot-path load off the database. Whichever store, the hold has a TTL:
- On purchase, verify the hold still belongs to this user and is unexpired, charge payment, then set
status = 'BOOKED'. - On timeout, the hold auto-releases — a Redis TTL does this natively; a SQL approach needs a sweeper job (or a check that treats
hold_expires_at < now()as available) so an abandoned checkout returns the seat to the pool.
The TTL is what makes “reserve before you pay” safe: a user who walks away can’t lock a seat forever.
Deep Dive: Surviving a Hot On-Sale
Correctness alone isn’t enough — 50,000 people hitting the booking service at once will overwhelm it even if every write is correct. The answer is to control admission with a virtual waiting room:
- When demand is high, users enter a queue (backed by Redis) instead of hitting booking directly. They get a position and a live “you are #12,431” status.
- An admission controller releases users from the front of the queue into the real booking flow at a rate the inventory system can sustain (e.g. a few thousand per minute), handing each a short-lived access token.
- Only admitted users can call the hold/purchase APIs. Everyone else waits.
This converts an uncontrollable stampede into a steady, bounded stream. It also improves fairness (first-come ordering) and lets you show honest wait times. Pair it with aggressive load-shedding at the edge and a CDN-served static seat map so the read path never touches the database during the frenzy.
Deep Dive: Consistency Boundaries
Not everything needs the same guarantees — draw the line deliberately:
| Operation | Consistency | Why |
|---|---|---|
| Browse / search events | Eventual | A slightly stale catalogue is harmless |
| Seat map display | Eventual (cached) | Verified at hold time anyway |
| Hold / purchase | Strong | Overselling is unacceptable |
| Payment | Exactly-once effect | Idempotency key so a retried charge isn’t double-billed |
Keep the strongly-consistent surface as small as possible — just the seat-state transitions and payment — and serve everything else from caches and replicas. Partition the inventory by event so a single blockbuster’s load lands on its own shard and doesn’t starve other shows.
Key Takeaways
| Decision | Choice | Why |
|---|---|---|
| No double-booking | Atomic conditional update / SET NX |
Exactly one racer wins; no read-modify-write race |
| Reserve before pay | HOLD with a TTL | User keeps the seat during checkout; auto-releases if abandoned |
| Hot on-sale | Virtual waiting room + admission control | Turns a stampede into a bounded stream |
| Consistency | Strong for booking, eventual for browse | Minimise the expensive strongly-consistent surface |
| Payment | Idempotency key | A retried charge isn’t applied twice |
| Scale | Shard inventory by event; CDN the seat map | Isolate blockbuster load; keep reads off the DB |
Ticketmaster is the canonical high-contention booking problem. Lead with the atomic hold-with-TTL (it is the heart of the correctness story), then show you can keep the system standing under a hot on-sale with a waiting room — that pairing is exactly what the interviewer is listening for.
