Skip to main content
PProfileMatch

MediumProblem 5 of 36 in the system design series

Design Ticketmaster: Event Booking That Survives On-Sale Spikes

Prefer watching? This article is the written companion to the video above, from the Shubham Aher YouTube channel.

An event ticketing platform lets fans find a concert or a match, look at the venue's seat map, choose specific seats and pay for them. For most of the year the traffic is unremarkable: people search, browse and occasionally buy. The defining moment is the on-sale, when a famous act releases a stadium tour and an enormous crowd arrives at the exact same second.

That moment is why the problem shows up so often in interviews. It combines a strict correctness rule, since a seat can belong to only one person, with a traffic pattern that looks almost like a denial-of-service attack. A design that handles one but not the other fails in public: either the site collapses, or two fans show up holding tickets for seat 14C.

The interesting insight is that different parts of the product deserve different guarantees. Booking must be strongly consistent, browsing can be a little stale, and the spike itself should be absorbed and metered rather than fought head-on. The walkthrough below designs a Ticketmaster-like service around those three ideas.

This is an interview practice design for a Ticketmaster-like service, built from first principles. It is not a description of how Ticketmaster is actually built.

Functional Requirements

Users search for events by name, performer, city or date. They open an event and see its seat map with current availability, so they can tell which sections still have room. Finally, they book: pick seats, have those seats reserved briefly while they enter payment details, pay, and receive confirmed tickets.

Splitting booking into hold, pay and confirm is a deliberate product choice. Without a temporary hold, a fan could spend two minutes typing card details only to discover the seat was taken mid-checkout. Secondary resale, demand-based pricing and organiser dashboards are left out because each brings its own workflows that would distract from the core booking problem.

Search events

Find events by name, artist, city or date.

View the seat map

See which seats for an event are still available.

live availability

Book tickets

Hold seats, pay, and get a confirmed ticket.

hold → pay → confirm

  • Out of scope today: resale, dynamic pricing, organiser tools

Non-Functional Requirements

The requirements here differ by area, and saying that explicitly is a strong interview signal. Booking requires strong consistency: every seat sells to exactly one buyer. The platform must survive on-sale spikes in which millions of users hit a single event within a minute.

Fairness matters too. Fans should be admitted roughly in the order they arrived, and automated scripts should not grab the best seats ahead of humans. Browsing, by contrast, prioritises speed and availability. Search results and seat maps must load quickly and stay up, and it is acceptable for a seat map to lag reality by a second, because the booking step performs the authoritative check anyway.

No double booking

A seat is sold to exactly one person.

strong consistency

Survive on-sale spikes

Millions of users hit one event in the same minute.

millions at once

Fair access

Fans get in roughly in arrival order; bots don't win.

waiting room

Fast browsing

Search and seat maps are quick and always up.

a second stale is OK

Scale Estimation

On an ordinary day, assume roughly 20 million daily active users who mostly search and browse. If they run around 200 million searches per day, that works out to about 2.3K searches per second, a load any well-built search cluster handles comfortably.

Now consider one popular event: a stadium concert with about 50 thousand seats. At the on-sale moment, assume 5 million fans arrive together, which is roughly 100 people competing for each seat, all within a few seconds. That burst is orders of magnitude above the daily baseline and concentrated on a single event's data. The estimate tells us where to spend effort: the everyday load is routine, and the architecture must be shaped around that on-sale spike.

20million

Daily active users

mostly search & browse

2.3K avg

Searches per second

200M searches / day

50thousand

Seats in a hot event

one stadium concert

5million

Fans at on-sale

≈ 100 fans per seat

Normal days are easy → design for the on-sale spike

Core Entities & API

Four entities model the domain: the event, the venue, the ticket representing one seat at one event, and the booking that groups the tickets a user is buying. Treating each seat-event pair as its own ticket row gives us a natural unit to hold, lock and mark as sold.

Search uses GET /events/search with query, city and date filters. Opening an event uses GET /events/{id}, returning the event, its venue and the seat map. Booking is two calls. POST /bookings/hold takes the event and the chosen seat_ids, places a temporary hold, and returns a booking_id with an expires_at timestamp. POST /bookings/{id}/confirm sends a payment_token; when the charge succeeds, the seats are marked sold and tickets are returned. Splitting the flow keeps payment out of the hold step, so holds stay fast.

Core entities

EventVenueTicket (seat)Booking

API

  • GET /events/search?q&city&date→ { events[] }
  • GET /events/{id}→ { event, venue, seat_map }
  • POST /bookings/hold { event_id, seat_ids[] }→ { booking_id, expires_at }
  • POST /bookings/{id}/confirm { payment_token }→ { tickets[] }

High-Level Design

All traffic enters through an API gateway that authenticates users, applies rate limits and routes requests. Searches and event pages go to an event service backed by an Elasticsearch index, and event pages are cached aggressively because thousands of people view the same few pages.

Booking requests for a high-demand event first pass through a virtual waiting room that admits users to the booking flow in controlled batches. Once admitted, the booking service places seat holds in Redis with a ten-minute expiry, so seats left in abandoned carts free themselves without any cleanup job. On confirmation, the booking service charges the user through a payment provider such as Stripe and then marks the seats as sold in a Postgres tickets database, which is the system of record.

Changes to events and seat status are streamed into the search index via change data capture. Keeping the event service and the booking service apart ensures that heavy browsing never competes for resources with people in the middle of paying.

searchadmit batcheshold seatschargemark soldCDC syncClientapp · webAPI gatewayauth · rate limitsEvent servicesearch · pagesSearch indexElasticsearchWaiting roomhot events onlyBooking servicehold · confirmSeat holdsRedis · 10 min TTLPaymentse.g. StripeTickets DBPostgres · truth
High-level design. Dashed arrows are taken only on a cache miss or as a fallback. Scroll sideways on small screens.

Deep Dive 1 — No Double Booking

One option is pessimistic locking: take SELECT ... FOR UPDATE on the ticket rows and hold that lock until payment completes. That is correct, but holding row locks for minutes during an on-sale would exhaust connections and stall the database. A second option records a reserved status and expiry directly on the ticket row. That keeps one source of truth, yet every hold becomes a database write, and a background job must sweep expired holds back to available.

The chosen option places holds in Redis. A small atomic script runs SET seat:{id} {user} NX EX 600 for each requested seat, succeeding only if no other user already holds it. The first writer wins, and if the buyer walks away the keys simply expire after ten minutes. Redis handles this at very high throughput, which is exactly what the on-sale needs.

Redis is a fast gatekeeper, not the final authority. At confirmation, the booking service runs a conditional update in Postgres that changes status to sold only where the seat is still available. Even if a Redis node failed and a hold vanished, that check prevents a second sale. The payment request also carries an idempotency key, so a retried confirmation cannot charge the card twice.

Lock rows in the DB

  • + Correct
  • − Locks held for minutes
  • − DB melts at on-sale

Reserved status in DB

  • + One source of truth
  • − Cleanup job for expiry
  • − Every hold is a DB write

Redis hold + TTLChosen

  • + Fast and atomic
  • + Expires by itself
  • − Re-check DB at confirm
  • SET seat:{id} {user} NX EX 600 → first writer wins, auto-expires
  • Confirm: UPDATE … SET status = 'sold' WHERE status = 'available'
  • Idempotency key on payment → a retry never charges twice

Deep Dive 2 — Virtual Waiting Room

The waiting room is enabled ahead of time for events expected to sell out, while ordinary events bypass it. Each arriving user is added to a Redis sorted set scored by arrival time, which preserves fairness and lets the system compute anyone's position quickly.

Users watch their place in line update over a WebSocket. A concrete message such as being number 40,000 in the queue is surprisingly important: it stops people from hammering refresh, which would otherwise multiply the load. Behind the scenes, an admission service releases batches of users at a pace the booking service can sustain, and slows down as inventory runs low so people are not admitted to an empty venue.

Each admitted user receives a short-lived, signed access token. The booking APIs reject any request without a valid token, so a script cannot jump the queue by calling the hold endpoint directly. Combined with per-account and per-device rate limits, this turns an uncontrolled stampede into a steady, predictable stream of buyers.

  1. 1Switch it onOnly for hot events, before the sale opens
  2. 2Join the queueRedis sorted set, scored by arrival time
  3. 3Show your positionLive updates stop endless refreshing
  4. 4Admit in batchesA steady rate the booking system can handle
  5. 5Signed access tokenBooking APIs accept only admitted users
  • Plus rate limits per account and device to slow down bots

Deep Dive 3 — Search & Browsing

Event discovery runs on Elasticsearch, indexing name, performer, venue, city and date, which supports typo-tolerant queries alongside filters. The index is kept current through change data capture, with Postgres changes flowing through Kafka into the index within seconds, so the booking service never writes to two stores.

Before a big release, huge numbers of fans load the identical event page and seat map, so popular searches and pages are cached in Redis and at the CDN. The seat map itself is split in two: the venue layout, which almost never changes, is served as a static asset from the CDN, while only a compact list of seat statuses is fetched live. That keeps the dynamic payload small even for a 50,000-seat stadium. Finally, browsing and booking run as separate services, so a surge in searching cannot slow down a buyer who is in the middle of paying.

  • Elasticsearch for event search

    Index name, artist, venue, city and date; typo-tolerant with filters.

  • Keep the index in sync with CDC

    Postgres changes stream through Kafka into the index within seconds.

  • Cache hot searches and event pages

    Before a big on-sale, everyone opens the same page; Redis and the CDN serve it.

  • Seat map: static layout + live status

    The venue layout comes from the CDN; only a compact seat-status list is live.

  • Separate browsing from booking

    A search spike can never slow down someone who is paying.

Trade-offs

Holding seats in Redis instead of the database gives speed and automatic expiry, with the cost of a second check in Postgres at confirmation. The waiting room protects the backend and feels fair, but users accept a delay instead of booking instantly. Applying strong consistency only to booking, while letting search and seat maps lag by a few seconds, keeps most of the system cheap and highly available.

Hold duration needs tuning. A window that is too short causes users to lose seats while still entering card details; one that is too long leaves seats locked by people who have wandered off while others wait in line. The overall pattern is worth memorising for any flash-demand system: be strict where money and ownership change hands, relax consistency for browsing, and put a queue in front of the spike.

  • Redis holds vs database holds

    Fast and self-expiring — but we must re-check the database at confirm.

  • Waiting room

    Protects the system and feels fair — but users wait in line.

  • Consistency only where it matters

    Booking is strict; seat maps and search may be seconds stale.

  • Hold duration

    Too short: users fail mid-payment. Too long: seats sit idle.

Strict for booking. Eventual for browsing. Queue the spike.

Key takeaways

  • Normal traffic is easy; shape the whole architecture around the on-sale spike.
  • Hold seats with an atomic Redis set-if-absent plus a TTL so abandoned holds expire on their own.
  • Confirm with a conditional database update so a lost Redis key can never cause a double sale.
  • A virtual waiting room with signed tokens meters the crowd and keeps access fair.
  • Serve the static venue layout from a CDN and fetch only compact live seat status.
  • Be strict for booking, eventually consistent for browsing, and queue the spike.

Interview questions

How does Ticketmaster prevent double booking of seats?

A Ticketmaster-like design uses two layers. First, an atomic Redis command sets a hold key per seat only if none exists, with an expiry of around ten minutes, so the first buyer wins. Second, at payment confirmation, a conditional database update marks the seat sold only if it is still available, which blocks a double sale even if a Redis hold was lost.

What is a virtual waiting room in system design?

It is a queue placed in front of a high-demand flow. Arriving users are recorded in order, often in a Redis sorted set, and shown their live position. An admission service lets them into the real booking system in batches at a sustainable rate and issues each a signed, short-lived token. Requests without a valid token are rejected, so nobody bypasses the line.

Why use Redis with a TTL for ticket seat holds?

Seat holds are temporary and created in huge bursts during an on-sale. Redis handles atomic set-if-absent operations at very high throughput, and a TTL means abandoned holds disappear automatically without a cleanup job. Keeping holds out of the main database avoids long-lived row locks and heavy write load, while the database still makes the final decision at confirmation.

How do you handle millions of users when tickets go on sale?

Separate browsing from booking, cache event pages and seat layouts at the CDN, and place a virtual waiting room in front of the booking service for high-demand events. The waiting room meters entry so the backend sees a steady flow instead of a stampede, while rate limits and signed admission tokens reduce the advantage of bots.

What happens if a user holds tickets and never pays?

The hold has an expiry, typically around ten minutes. When it passes, the Redis keys vanish and the seats show as available again on the next seat map refresh, with no background job required. Because the database still lists those seats as available, the next buyer can hold and confirm them through the normal flow without special handling.

Keep practising