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.
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.
- 1Switch it onOnly for hot events, before the sale opens
- 2Join the queueRedis sorted set, scored by arrival time
- 3Show your positionLive updates stop endless refreshing
- 4Admit in batchesA steady rate the booking system can handle
- 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
- Learn the building blocks in the free system design course.
- Rehearse answers with system design interview questions.
- See which open jobs fit your skills: upload your resume.