Lesson 425 min read

Design Ticketmaster

April 22, 2026

// TL;DR

Design Ticketmaster: the hard part is selling each seat exactly once when thousands of people want it at the same instant. Put a short-lived HOLD (a lock with a TTL) on a seat before payment — confirm on success, auto-release on timeout. For a hot on-sale, a virtual waiting room admits users at a controlled rate so the inventory system is never stampeded. Booking must be strongly consistent; browse/search can be eventual.

Design Ticketmaster

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

  1. Browse/search events — by artist, venue, date
  2. View seat availability — a seat map for an event
  3. Reserve seats — hold specific seats while the user checks out
  4. Purchase — pay and confirm the booking
  5. 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 release

The 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

Seat reservation: the booking service places a HOLD (a lock with a TTL) on the seat before payment, confirms on success, and releases on timeout, preventing double-booking

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 did

Because 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:

A virtual waiting room buffers a huge spike of users and admits them into the booking and inventory system at a controlled rate so inventory is never stampeded
  1. 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.
  2. 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.
  3. 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.