Design Ticketmaster
Problem
Design an online ticketing platform for concerts and sports events. Users browse and search events, view a seat map with live availability, select specific seats, and complete a purchase. Once a user selects seats, those seats are held for them for 10 minutes while they pay. No seat may ever be sold twice. The platform lists about 100,000 events at any time, and a popular on-sale can bring 10 million users trying to buy 50,000 seats in the first few minutes.
- Are all events reserved seating, or do some have general admission with only a quantity limit?
- Is there a per-user ticket limit for an event, and how do we detect the same buyer using multiple accounts?
- How long should a seat hold last, and can the user extend it?
- Do we need resale or ticket transfer, or only primary sales?
- How fresh must the seat map be? Is a few seconds of staleness acceptable while browsing?
- Do we need to defend against scalper bots beyond rate limiting?
Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.
- Browse and search events by name, performer, venue, city, and date.
- Show a seat map with current availability for an event.
- Hold selected seats for a user for 10 minutes, then release them if not purchased.
- Complete a purchase that converts held seats into issued tickets, with no double booking.
- Queue users fairly in a virtual waiting room during high-demand on-sales.
- Enforce a per-user ticket limit per event.
- Deliver tickets as signed barcodes in the app and by email.
Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.
- Strong consistency for seat inventory: a seat is sold at most once.
- Handle 10 million concurrent users for a hot on-sale while selling 50,000 seats.
- Browsing is read heavy: about 100,000 reads per second at peak, served mostly from cache with seconds of staleness.
- Reservation writes peak at the rate the waiting room admits users, capped near 5,000 holds per second per event.
- Seat map loads in under 500 ms at p99; hold requests respond in under 300 ms.
- Storage is modest: 100,000 events with an average of 20,000 seats is 2 billion seat rows a year, about 200 GB.
Event discovery is a read-heavy path served by a search index and a CDN-cached catalog. Seat inventory lives in a strongly consistent relational database partitioned by event, where a hold is a conditional update that only succeeds if the seat is still available, and expired holds are released by a sweeper. For hot events, a virtual waiting room in front of the booking path admits users at a controlled rate, so the inventory database only ever sees traffic it can serve.
Loading diagram...
Browse. The client searches with GET /events?q=. The catalog service queries the search index and returns event cards, which the CDN caches for a short time. Opening an event fetches the seat map from the seat map cache, which is refreshed from the inventory database every few seconds.
Join an on-sale. For events flagged as high demand, the gateway redirects users to the waiting room. Each user gets a random position and polls for status. The waiting room admits batches at a rate matched to booking capacity and issues each admitted user a signed, short-lived access token scoped to that event.
Hold seats. The user selects seats and sends POST /events/:id/holds with the token. The booking service runs one transaction that updates those seat rows from AVAILABLE to HELD only where they are still available, setting hold_id and expires_at. If any seat is already taken, the whole transaction rolls back and the user picks again.
Purchase. The user pays with POST /holds/:id/purchase. The booking service charges through the payment service using the hold id as idempotency key, then marks the seats SOLD in a transaction that checks the hold is still valid. It publishes a booking event so the ticket issuer can generate barcodes and the notification service can email them. If the hold expires first, the sweeper flips the seats back to AVAILABLE.
Protects the booking path from on-sale stampedes by queueing users and admitting them at a fixed rate. Users who are not admitted see their position and an estimated wait instead of errors.
- Redis sorted sets for queue positions, keyed by event.
- Edge workers serve the waiting page and status polls so origin servers see little of the raw traffic.
Diagram
Loading diagram...
API
POST /events/:id/queuereturns{ queueTicket, position }.GET /queue/:ticketreturns{ position, estimatedWait, accessToken? }.
Scalability
Ten million users polling every 10 seconds is one million requests per second, so status checks are answered at the edge from a published admit cursor rather than hitting Redis per poll. Only queue joins touch Redis, and one sorted set per event shards naturally.
Tradeoffs
Randomized positions for users who arrive before the on-sale opens are fairer than strict first come, first served, which rewards bots with the fastest connections. The admission rate is tuned slightly below booking capacity so admitted users have a smooth experience, at the cost of a longer queue.
Owns seat holds and purchases, and is the only writer of seat state.
- Stateless Java or Go service.
- PostgreSQL for seat inventory, chosen for row-level locking and multi-row transactions.
API
GET /events/:id/seatsreturns the seat map with statuses.POST /events/:id/holdswith{ seatIds }returns{ holdId, expiresAt }or409if any seat is taken.DELETE /holds/:idreleases a hold early.POST /holds/:id/purchasewithIdempotency-Keyand payment token returns{ orderId, tickets }.
Scalability
All writes for one event land on one partition, so per-event throughput is bounded by one database primary, a few thousand transactions per second. The waiting room keeps admitted traffic below that. Different events are spread across many shards.
Tradeoffs
Holding seats with a conditional UPDATE ... WHERE status = 'AVAILABLE' is optimistic and needs no long-lived locks: losers get a fast 409. A Redis lock per seat was rejected because the database must still be updated, and keeping two sources of truth consistent under failure is harder than one transactional store. The hold has an explicit expires_at instead of relying on a cache TTL so that expiry is visible to the purchase transaction.
Durable source of truth for every seat's state for every event.
- PostgreSQL sharded by
event_id, or a distributed SQL database such as CockroachDB or Spanner.
Data Model
seats(event_id, seat_id, section, row, number, price_tier, status, hold_id, expires_at, order_id, version)with primary key(event_id, seat_id).holds(id, event_id, user_id, seat_ids, expires_at, status)orders(id, user_id, event_id, hold_id UNIQUE, amount_minor, payment_id, status)
Scalability
Sharding by event keeps every booking transaction single-shard. A 20,000 seat event is only a few megabytes, so even the hottest event fits in memory on its shard.
Tradeoffs
Strong consistency is non-negotiable here, which is why a relational store is chosen over an eventually consistent key-value store. The UNIQUE constraint on orders.hold_id stops a double purchase if the client retries a payment.
Background job that returns abandoned holds to the available pool.
- Scheduled worker running every few seconds per shard, using an index on
(status, expires_at).
Scalability
Each sweep is a bounded indexed query per shard, so cost scales with the number of expiring holds rather than total seats.
Tradeoffs
A sweeper is chosen over delayed queue messages per hold because it is self-healing: if a worker crashes, the next sweep still finds every expired row. The purchase transaction also checks expires_at directly, so a seat whose hold has lapsed but not yet been swept still cannot be bought on a stale hold.
Serves event listings, search, and venue details. Everything here tolerates staleness.
- Elasticsearch or OpenSearch for full text and geo search.
- CDN caching of event pages and seat map snapshots for a few seconds.
Scalability
Read traffic is absorbed by the CDN and search replicas. Seat map snapshots are rebuilt from inventory every few seconds per hot event and pushed to cache, so millions of viewers do not query the inventory database.
Tradeoffs
Showing a seat map that is a few seconds stale means some users click a seat that was just taken. That is handled gracefully by the 409 on hold, and is far cheaper than streaming live seat state to millions of clients during an on-sale.