Design a ticket booking system for named seat reservation under massive concurrent onsale demand. The core challenge: reserve a specific seat ID atomically while serving millions of users browsing the seat map in real time.
Ticket booking is fundamentally different from other inventory systems, and interviewers use this distinction to separate candidates who truly understand distributed systems from those who apply generic patterns. The critical difference is named seat reservation versus anonymous inventory units. A flash sale sells N identical, interchangeable items — the entire problem reduces to a single atomic counter (Redis DECR). Ticketmaster sells named seats: seat 14E in section 103 is a unique, specific resource with its own identity (section, row, number). This forces candidates to reason about per-entity locks versus shared counters, hold-and-confirm patterns, and seat map freshness at massive scale. The Ticketmaster interview question is uniquely rich because it exposes multiple independent hard problems simultaneously.
Three core challenges define the Ticketmaster design space. First, double-booking prevention: a seat sold means exactly one owner, with zero tolerance for overselling. Unlike hotel overbooking (which is sometimes deliberate), ticket double-booking is a catastrophic business failure — two people show up to the same seat. The system must enforce mutual exclusion at the per-seat level with no gaps. Second, the thundering herd problem at onsale: when a high-demand artist releases tickets, 5 million fans simultaneously attempt to access the seat map and hold seats the instant the sale opens. Every user hits the system at the same millisecond. Traditional rate limiting and queueing strategies break down because the bottleneck is not sustained load but an instantaneous spike. Third, seat map freshness: those 5 million concurrent viewers need to see a near-real-time availability overlay for 60,000 seats. Showing a seat as available when it was actually held 1.9 seconds ago creates user frustration — they click a seat and get SEAT_UNAVAILABLE. The system must balance freshness (low staleness) against the cost of serving 5M concurrent reads.
The reason Ticketmaster is architecturally different from a flash sale is subtle but important. A flash sale uses one shared inventory counter — Redis DECR on a single key. This creates a hot-key problem: all 5M users hammering one Redis key in parallel. Ticketmaster has 60,000 independent seat keys. Redis SETNX per seat key means there is no contention between different seats — seat 14E and seat 15F can both be held simultaneously without any coordination. This eliminates the hot-key problem entirely. But it introduces the seat map challenge: how do you serve a real-time availability overlay for 60,000 named seats to millions of concurrent viewers without reading all 60,000 Redis keys per render? The answer requires a dedicated availability cache with a compact bitmap representation.
The hold-and-confirm pattern is central to every viable Ticketmaster design. When a user selects a seat, the system places a 10-minute hold via a Redis SETNX call with a 600-second TTL. The user then enters payment details and completes checkout. If the payment succeeds, the hold is converted to a confirmed purchase written to the orders database. If payment fails or the user abandons checkout, the Redis TTL expires and the seat automatically returns to availability — no manual cleanup required. This two-phase approach decouples the instantaneous seat reservation (2ms Redis SETNX) from the slow, fallible payment process (200-500ms, 2-5% failure rate). The 10-minute window is deliberately chosen: short enough to prevent seat hoarding, long enough for payment flows to complete even on slow connections.
The Ticketmaster interview question is asked at Ticketmaster directly, as well as StubHub, AXS, Amazon Events, Eventbrite, and Walmart. The same patterns appear across the entire reservation industry: airline seat selection (delta.com forces holds during payment), movie ticket booking (AMC, Fandango), sporting event ticketing, hotel room booking, restaurant reservation systems, and parking spot booking. Any system where a specific named resource must be exclusively reserved for one user during a checkout flow is a Ticketmaster variant. Mastering this design covers a broad class of reservation problems that appear across every industry vertical.
The V1 canonical architecture uses 10 components organized into two independent paths: the event browse path (read-heavy, cacheable) and the seat hold path (write-critical, latency-sensitive). Understanding why these paths are separated — and why they must stay separated — is the architectural insight that drives the entire design.
The browse path handles 99% of all traffic by volume. Users searching for events, viewing event details, and rendering seat maps are served by SearchService backed by EventCache (ElastiCache Redis). EventCache maintains two distinct cache spaces with different TTLs: event metadata (artist, venue, date, pricing tiers) is cached for 60 seconds since it changes rarely, and seat availability overlays are cached for only 2 seconds since they change constantly during onsale. The browse read-to-purchase ratio is approximately 100:1 — for every ticket sold, 100 users browse without buying. Serving this read traffic from cache means EventDB (PostgreSQL) sees only cache miss traffic, roughly 1% of browse requests.
The seat hold path handles the critical 1% of traffic that actually moves money and inventory. When a user selects a seat, SeatService executes Redis SETNX on the key seat:{event_id}:{seat_id} with a 600-second TTL. SETNX is atomic and single-round-trip: it succeeds only if the key does not already exist, providing mutual exclusion with no race conditions. The 2ms Redis SETNX latency versus 50ms PostgreSQL SELECT FOR UPDATE (V0 approach) is a 25x improvement, and critically, different seats can be held simultaneously in parallel since each has its own independent key. 60,000 seats equals 60,000 independent parallel operations.
SeatHoldCache stores all active seat holds. The key is seat:{event_id}:{seat_id} and the value contains user ID, hold timestamp, and expiry. When a hold expires (Redis TTL auto-deletion), the seat silently returns to availability on the next EventCache invalidation cycle. SeatService triggers an EventCache invalidation on every successful SETNX, so the seat map availability bitmap is refreshed within 2 seconds. On confirmation, SeatService writes an order record to OrderDB (PostgreSQL RDS Multi-AZ) and publishes a ticket_confirmed event to TicketStream (Kafka).
TicketWorker consumes from TicketStream and handles QR barcode generation and email delivery asynchronously. This decoupling is critical: QR generation and email add 100-200ms to the checkout flow. At 5,000 concurrent confirmations during onsale, blocking on these operations would saturate service threads. Kafka decouples confirmation (instant) from delivery (within 30 seconds). Kafka also provides replay capability: if the email provider goes down, TicketWorker can reprocess events when it recovers without losing any confirmed orders.
The key metric that reveals the design's correctness is the read-to-write ratio: EventCache handles 5M browse RPS at 99% hit rate, meaning SearchService serves 4.95M RPS from in-memory cache. SeatService handles 50K seat hold RPS at onsale peak. These two services have radically different scaling needs — SearchService scales for read concurrency (many pods, large EventCache), SeatService scales for write throughput (SETNX parallelism, Redis connection pooling). Running them in the same service would mean a browse surge starves seat hold threads during the most critical moment: the exact instant of onsale.
Choice
Per-seat Redis keys (seat:{event_id}:{seat_id})
Rationale
Each seat is an independent resource. SETNX per seat gives atomic mutual exclusion with no contention between different seats — 60K seats means 60K parallel operations with zero cross-seat coordination. Flash sale uses a single DECR counter for anonymous inventory; Ticketmaster requires per-entity keys because seat 14E in section 103 is not interchangeable with seat 15F. This architectural choice eliminates the hot-key problem entirely while introducing the seat map challenge.
Choice
Two-phase: 10-minute TTL hold via Redis SETNX, then payment confirmation
Rationale
Instant purchase forces synchronous payment within the checkout flow, creating a hard dependency on payment gateway availability (typically 200-500ms per call, 2-5% failure rate). The hold-and-confirm pattern decouples seat reservation (2ms Redis SETNX) from payment processing. Users see seat confirmation immediately; payment settles asynchronously. The 10-minute TTL is chosen to accommodate slow connections and hesitant users while preventing indefinite seat hoarding.
Choice
EventCache with 2s TTL compact availability bitmap per event
Rationale
Reading SeatHoldCache directly for every seat map render would require SCAN across all seat keys per event — O(60,000 reads) per user per render. With 5M concurrent viewers, this generates 300 billion Redis ops/sec — physically impossible. A dedicated availability bitmap (1 bit per seat, 7.5KB for 60K seats) is read once per 2s cache window, reducing Redis reads by a factor of 60,000. SeatService invalidates EventCache on every SETNX success to keep staleness under 2 seconds.
Choice
Dedicated services with independent scaling profiles
Rationale
Event browse is read-heavy and highly cacheable, with a 100:1 browse-to-purchase ratio. Seat holds are write-heavy and time-critical at onsale. Running both in one service means a browse surge at onsale open — the exact worst moment — can starve seat hold threads in the same thread pool. Separation allows SearchService to scale to 20 pods for browse read concurrency while SeatService scales independently for hold write throughput.
Choice
Kafka TicketStream → TicketWorker generates QR + sends email asynchronously
Rationale
QR barcode generation and email delivery add 100-200ms to the checkout flow. At 5,000 concurrent confirmations during onsale, this blocks service threads during the highest-stakes moment. Kafka decouples confirmation from delivery: users see 'your seat is confirmed' immediately, QR email arrives within 30 seconds. Kafka provides replay capability if email delivery fails — TicketWorker reprocesses on recovery without affecting any purchase record in OrderDB.
Target RPS
50K seat hold RPS at onsale peak; 5M browse RPS (mostly EventCache hits)
Latency (p99)
2ms seat hold (Redis SETNX), <100ms browse (cache hit), <30s ticket delivery (async)
Storage
~200 GB (event catalog + order history); SeatHoldCache ~3.6MB per active event in Redis
Availability
99.99% for seat hold path (Redis cluster + RDS Multi-AZ); 99.9% for browse
Stores the physical seat geometry and price configuration for every seat in every venue. One row per seat per event. This table is the source of truth for seat metadata (section, row, number, price tier) but NOT for availability — SeatHoldCache (Redis) owns the authoritative availability state. At 80K seats per event across 100K events, this table grows to approximately 8 billion rows at scale.
Indexes: idx_seats_event ON (event_id) — seat map query for all seats in an event, idx_seats_event_status ON (event_id, status) — count available seats per event
This table does NOT reflect Redis holds — it reflects only confirmed sales (SOLD) and expired holds returned to AVAILABLE. Real-time hold state lives in SeatHoldCache. At 80K seats × 100K events = 8B rows, this table requires partitioning by event_id in production.
Records confirmed ticket purchases. Written only when a hold is successfully converted to a confirmed purchase (payment succeeded). The orders table is NOT written on hold creation — holds exist only in Redis. On confirmation, SeatService writes to orders and publishes to TicketStream for async QR + email generation.
Indexes: idx_orders_user ON (user_id) — user order history lookup, idx_orders_event ON (event_id) — event sales reporting, idx_orders_seat ON (seat_id) — verify no double-confirmation
At 5K purchases/sec peak during a single major event onsale × 3600 seconds = 18M orders per onsale window. Write rate is bounded by Redis SETNX throughput (only successful holds can become confirmed orders). ticket_generated flag is updated by TicketWorker after async QR delivery.
This template is for educational and illustration purposes only. It may not represent the optimal production design for this problem. Real-world systems involve additional considerations (compliance, specific cloud provider constraints, organizational requirements) not captured here. Use this as a starting point for discussion, not as a production blueprint.
Ticket booking combines the most interesting system design challenges in one problem: named entity reservation (not anonymous inventory), the thundering herd problem at onsale, real-time availability display for millions of concurrent viewers, the hold-and-confirm two-phase pattern, and TTL expiry management. It tests distributed systems knowledge (Redis, Kafka, CDN) alongside product thinking (fairness, bot mitigation, user experience during queuing). Companies like Ticketmaster, StubHub, AXS, and Amazon Events ask this directly, and the same patterns appear in hotel booking, airline seat selection, restaurant reservation, and parking spot systems. The question rewards candidates who can reason about per-entity locks versus shared counters — a subtle but fundamental distinction.
A flash sale sells N identical, anonymous inventory units — any unit is interchangeable with any other. The entire problem reduces to one atomic counter (Redis DECR). A ticket booking system sells named seats: seat 14E in section 103 is a unique, specific resource. This requires per-seat keys (Redis SETNX per seat_id) rather than a shared counter. The consequence is double-edged: Ticketmaster has no hot-key problem (60K seats = 60K independent Redis keys, all lockable simultaneously), but it introduces the seat map freshness challenge — how to display which specific named seats are currently held versus available across millions of concurrent viewers in near real time.
Redis SETNX (SET if Not eXists) on the key seat:{event_id}:{seat_id} provides the atomic mutual exclusion guarantee. SETNX is a single-round-trip atomic operation: it succeeds only if the key does not already exist. The first caller to SETNX for a given seat wins the hold; all subsequent callers for that same seat receive a 'key exists' failure (SEAT_UNAVAILABLE). Because Redis SETNX is atomic at the command level (single-threaded command execution in Redis), there is no race condition between checking and setting. The 600-second TTL on the key ensures holds are automatically released if payment is not completed, returning the seat to availability without any manual cleanup job.
The naive approach — reading SeatHoldCache for every seat map render — fails catastrophically: SCAN across 60K seat keys per render multiplied by 5M concurrent users equals 300 billion Redis ops/sec. The solution is a dedicated AvailabilityCache storing a compact bitmap per event (1 bit per seat = 7.5KB for a 60K-seat venue). In V1, this bitmap is refreshed on a 2-second TTL with SeatService triggering invalidation on every successful SETNX hold. In V2, NotificationWorker consumes Kafka seat-state-change events and performs atomic bit-flip updates, keeping staleness under 1 second without any polling pressure on SeatHoldCache.
The V1 design breaks at Taylor Swift or Super Bowl scale: 5 million users simultaneously executing SETNX on the seat hold cluster at onsale open overwhelms even a well-sized Redis node. V2 adds three layers to address this. First, a CloudFront CDN absorbs 90% of browse traffic at the edge, reducing origin load from 10M to 1M RPS. Second, a virtual WaitingRoomService gates access to SeatService by releasing admission tokens at 5K/sec using a Redis sorted set queue — preventing 5M concurrent SETNX calls from reaching Redis simultaneously. Third, AvailabilityCache is updated via Kafka push events from SeatService rather than TTL polling, reducing seat map staleness from 2 seconds to under 1 second without creating additional hot-key pressure.
Sign in to join the discussion.
Ready to design your own Ticketmaster?
Open the simulator, place components on the canvas, wire them up, and run a traffic simulation to see how your architecture performs under real load.
Open Simulator