SD System Design In Depth
0%
design system-designbookingworkflows

Design: a ticket booking system

Module 6.2: Interactive Product System Designs Updated Sep 18, 2026
📄 Cheat Sheet
System Design Interview: Design Ticketmaster w/ a Ex-Meta Staff Engineer
Hello Interview System Design Interview: Design Ticketmaster w/ a Ex-Meta Staff Engineer Watch on YouTube ↗

The best-known walkthrough of this exact problem: seat holds with TTLs, preventing double booking, a virtual waiting room and search. It follows the lesson's structure closely.

0:00 / --:--

The Core Challenge: Concurrency & Hot Spots

Designing a high-concurrency ticket reservation platform like Ticketmaster or BookMyShow revolves around a single critical invariant: a seat must never be double-booked, even when hundreds of thousands of users attempt to purchase tickets for the same concert within seconds. At the same time, holding a seat during checkout requires a temporary reservation with an automatic timeout if payment is not completed.

Reservation State Lifecycle

stateDiagram-v2 [*] --> Available: Event Announced Available --> Held: User selects seat (10 min TTL) Held --> Available: TTL expires / User cancels Held --> Booked: Payment Confirmed (Atomic) Booked --> [*]

Architectural Strategy

  • Distributed Virtual Waiting Room: When traffic exceeds venue capacity, users are assigned a position in a distributed queue (using Cloudflare Waiting Room or Redis sorted sets) before accessing the seat selection map.
  • Temporary Seat Hold: Give each hold a unique token, owner, and authoritative expiry. Redis may accelerate reads or expiry notifications, but a separately expiring Redis key must not be the only proof that SQL is allowed to book the seat.
  • Database Isolation & ACID Guarantees: The authoritative conditional transition verifies the same hold token, owner, state, and unexpired deadline in an ACID-compliant database. Row-level locking or optimistic concurrency can serialize competing transitions:
    UPDATE seats SET status = 'BOOKED', user_id = :userId, version = version + 1 
    WHERE seat_id = :seatId AND status = 'HELD' AND hold_token = :holdToken
      AND hold_expires_at > CURRENT_TIMESTAMP AND version = :currentVersion;
  • Read Scaling with In-Memory Caches: High-volume reads of the seating chart are served from Redis bitsets or memory-cached floor plans, while write traffic routes directly through the transactional reservation service.

Payment Reconciliation & Webhook Callbacks

Payment processing is asynchronous, so signed callbacks can be duplicated, delayed, or reordered. If buyer A's hold expires, buyer B may acquire the seat before A's success callback arrives. The callback must idempotently attempt the token-bound booking transition; if it loses, record the payment as unmatched and run an explicit void/refund workflow with reconciliation rather than issuing two tickets or assuming an immediate refund always succeeds.

Worked example: a 50,000-seat stadium on-sale

  • 2 million fans arrive in the first 5 minutes. Without a waiting room, seat-map requests peak at hundreds of thousands per second, on data that changes every second.
  • The waiting room admits 3,000 users per minute. If an average buyer takes 6 minutes, about 18,000 people are in checkout at once, a load the reservation database can easily handle.
  • Holds last 10 minutes. With 50,000 seats and roughly 1 in 4 holds abandoned, expired holds release seats continuously, so the seat map must refresh often (poll every few seconds, or push updates to open sessions).
  • Contention is highest on the best seats. The conditional UPDATE ... WHERE status = 'AVAILABLE' returns 0 rows for everyone except the winner, who gets the seat. Everyone else is told immediately to choose another seat.

Concurrency options

TechniqueHow it worksStrengthWeakness
Conditional update / optimistic versionUPDATE ... WHERE status = 'AVAILABLE' AND version = vNo lock held while the user thinks; scales wellLosers must retry or pick again
Pessimistic row lockSELECT ... FOR UPDATE inside a short transactionSimple reasoningLock waits pile up on hot rows; never hold it across user think time
Distributed lock (Redis, ZooKeeper)Lock per seat, then write to the DBOffloads contention from the DBNeeds fencing tokens; the lock and the DB can disagree
Serialised per-event queueOne consumer per event partition processes seat requests in orderNo races by constructionThroughput limited by one partition; adds latency

General admission is a counter, not seats

For standing-room tickets, inventory is a number: UPDATE inventory SET remaining = remaining - :n WHERE event_id = :e AND remaining >= :n. A single counter row becomes a hotspot at thousands of updates per second. Split it into, say, 20 sub-counters of 2,500 tickets each, pick one at random, and fall back to the others when it runs out. This is the same idea as sharded counters.

Fairness and bots

  • Randomise positions for everyone who arrives before the on-sale time, so refreshing early gives no advantage. Ticketmaster and Cloudflare Waiting Room both do this.
  • Limit tickets per account and per payment card, and require verified accounts for high-demand events (Ticketmaster Verified Fan).
  • Issue signed, expiring entry tokens from the waiting room, so bots cannot skip the queue by calling the checkout API directly.
Check yourself Question 1 of 3
Where the hold lives recall
From: Architectural Strategy

Why should a seat hold's token, owner and expiry be stored in the database row that the booking UPDATE checks, rather than only as a Redis key with a TTL?

Key Takeaways

Keep hold token, owner, and expiry in the authoritative conditional booking transition; use Redis only as a cache or accelerator unless it shares that authority safely.
Deploy a virtual waiting room in front of checkout to flatten extreme traffic spikes.
Use optimistic locking or atomic compare-and-swap to prevent race conditions during seat claims.
Implement automated reconciliation for out-of-order or delayed payment gateway callbacks.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue