Ticketmaster
Walk through a complete Ticketmaster-style ticket booking system, from a basic event/seat model to a globally-consistent reservation system that handles flash-sale traffic spikes and prevents double-booking.
What is Ticketmaster?
Ticketmaster sells tickets to concerts, sports games, and theater. The system looks like a simple shopping platform until 500,000 users simultaneously hit "Buy" the moment Taylor Swift tickets go on sale.
Every seat is a unique, finite resource that must be sold to exactly one buyer. That constraint forces the design into ACID transactions, distributed locking, and queue-based traffic shaping. I've seen teams underestimate this: they build a nice e-commerce checkout and then discover on launch day that 200 people bought the same seat. This question tests pessimistic versus optimistic concurrency control, flash-sale architecture, and the boundary between strong and eventual consistency.
Functional Requirements
Core Requirements
- Event organizers can create events with a seat map and price tiers.
- Users can browse available events and view the seat map with real-time availability.
- Users can reserve one or more seats, hold them for a 10-minute checkout window, and complete the purchase.
- If a user abandons checkout, held seats are automatically released back to available.
Below the Line (out of scope)
- Ticket transfer and secondary market resale
- Mobile ticket delivery and NFC check-in
- Venue and event organizer analytics
- Dynamic pricing (surge pricing based on demand)
Ticket transfer and secondary market resale involves a second transaction lifecycle: a resale listing, a buyer payment, and a transfer of ownership that invalidates the original booking and creates a new one atomically. To add it, I would model a Resale entity linked to an existing Booking and handle ownership transfer in a separate service isolated from the primary booking flow.
Mobile ticket delivery and NFC check-in is a read-only verification flow with no write contention on seats. Each confirmed Booking generates a signed QR code (an HMAC of booking_id, event_id, and seat_id), stored as a JWT in the booking record. The check-in scanner verifies the signature against the system's secret key without hitting the booking database on every scan.
Venue and organizer analytics is a read-only, eventually consistent reporting pipeline that sits beside the booking system. The integration point is a Kafka topic where every booking event is published; an analytics consumer writes into a data warehouse for reporting without touching the booking critical path.
Dynamic pricing requires a pricing engine with write access to the Seat.price field that runs a separate real-time demand model. The integration is a background pricing service that subscribes to booking events, computes demand signals, and updates price tiers on remaining seats outside the synchronous booking path.
The hardest part in scope: Preventing two users from booking the same seat simultaneously when 500,000 concurrent users hit the booking endpoint is the central engineering challenge. Every concurrency decision in this design is a direct response to this constraint.
Non-Functional Requirements
Core Requirements
- Scale: 50 million active users; 100 events active concurrently; peak events with up to 100,000 seats.
- Concurrency: Up to 500,000 simultaneous users at the booking endpoint for a single high-demand event at sale time.
- Latency: Seat reservation confirmation under 200ms p99.
- Consistency: Strong consistency for seat booking. A seat must never be sold twice.
- Availability: 99.99% uptime for the booking path (roughly 52 minutes of downtime per year).
- Hold window: Seats held for exactly 10 minutes; automatically released on expiry.
Below the Line
- Geo-replication across multiple regions (single-region is sufficient for this design)
- Real-time fraud detection during checkout (a separate async scoring pipeline)
Read/write ratio: Browsing the event catalog and viewing seat maps accounts for roughly 100 reads for every 1 booking write. The booking writes are the critical path. A slow seat-map read shows stale data; a failed or duplicated booking write loses revenue and violates correctness. This ratio tells you where to invest caching: the seat map read path can absorb a generous cache TTL, while the booking write path must bypass cache entirely and go to the source of truth.
Core Entities
- Event: A scheduled performance at a venue. Holds name, date, venue reference, and sale open time.
- SeatMap: The physical layout of a venue for an event. Contains sections, rows, and seat identifiers.
- Seat: An individual seat tied to an event. Tracks
status(available, held, booked), price tier, and the current reservation holding it. - Reservation: A 10-minute hold on one or more seats. Created when checkout begins. Expires and auto-releases on timeout.
- Booking: A confirmed, paid purchase of reserved seats. The permanent record of a completed transaction.
- User: An account that can browse events, hold seats, and complete purchases.
Schema details such as the expires_at index and seat status enum are addressed in the Interview Cheat Sheet. These six entities are enough to drive the API and High-Level Design.
API Design
FR 1: Create an event with a seat map:
# Organizer creates a new event; server expands seat map template into seat rows
POST /v1/events
Body: { name, venue_id, date, seat_map: { sections: [...] }, price_tiers: [...] }
Response: { event_id: "ev_abc123" }
POST creates a new resource. The seat_map in the request body describes sections and seat counts; the server generates individual Seat rows from this template inside a single transaction. Price tiers link to sections, not individual seats, keeping the schema manageable for 100,000-seat venues. I'd call this out early in the interview because interviewers sometimes expect you to model price per seat, which explodes the schema for large venues.
FR 2: View seat availability:
# Returns per-seat status for a given event; served from Redis on cache hit
GET /v1/events/{event_id}/seats
Response: {
event_id: "ev_abc123",
seats: [
{ seat_id: "s_1A", section: "Floor", row: "A", number: 1, status: "available", price: 150 },
{ seat_id: "s_1B", section: "Floor", row: "A", number: 2, status: "held" }
]
}
This is the hottest read endpoint: 100 reads for every 1 booking write. Responses are served from Redis; only cache misses hit PostgreSQL. Seat status changes (hold, release, book) write through to Redis immediately to keep the map accurate.
FR 3: Reserve seats (start checkout):
# Atomically holds requested seats; returns reservation with 10-minute expiry
POST /v1/events/{event_id}/reservations
Body: { seat_ids: ["s_1A", "s_1B"], user_id: "u_789" }
Response: { reservation_id: "res_xyz", expires_at: "2026-03-29T12:10:00Z", seats: [...] }
This is the hardest endpoint. Two requests for the same seat_id arriving simultaneously must result in exactly one succeeding (201 Created) and one failing (409 Conflict). The atomic seat-locking mechanism is the subject of Deep Dive 1.
FR 4: Confirm booking (complete checkout):
# Processes payment and converts the reservation into a confirmed booking
POST /v1/reservations/{reservation_id}/confirm
Body: { payment_method_id: "pm_abc" }
Response: { booking_id: "bk_def", seats: [...], total_amount: 300 }
The server validates the reservation has not expired, processes payment, then atomically transitions seats from "held" to "booked" and inserts a Booking record in one transaction. Payment failure leaves seats held until expiry; no manual rollback is needed.
Abandon checkout (release hold):
# Explicit release; expired reservations are also released by background worker
DELETE /v1/reservations/{reservation_id}
Response: 204 No Content
The client calls this on explicit cancel. Expired reservations are released automatically by a background expiry worker; this endpoint handles the explicit cancel path only.
High-Level Design
1. Event creation and seat storage
The foundation: an organizer creates an event; the server generates all seat records atomically and stores them in a relational database.
Components:
- Client (Organizer): Admin dashboard sending the event creation request.
- Event Service: Validates the event data, expands the seat map template into individual seat rows, and writes everything in a single transaction.
- PostgreSQL: Stores
events,seat_maps, andseatstables. ACID transactions ensure the event and all seat rows are written together or not at all.
Request walkthrough:
- Organizer sends
POST /v1/eventswith event metadata and a seat map template. - Event Service opens a transaction: INSERT into
events, then for each seat in the template, INSERT intoseatswithstatus = 'available'. - Event Service commits and returns the
event_id.
This creates the event and initializes every seat as available. I'd mention at the whiteboard that the INSERT of 100,000 seat rows is a one-time batch operation (seconds, not milliseconds), so it does not need the latency guarantees of the booking path. The next step covers how to serve this seat data to thousands of concurrent viewers without overloading the database.
2. Seat availability read path
The read path: a user views the seat map; the system serves per-seat availability from Redis rather than from the primary database.
With 100 events active and up to 500,000 concurrent viewers during a flash sale, sending every seat map request directly to PostgreSQL creates a stampede that overwhelms the database. I'd always build this path cache-first: PostgreSQL is the source of truth for writes, and Redis is the source of truth for reads. The write-through pattern on every seat status change keeps them synchronized.
Components (added):
- Booking Service: Stateless service handling both the read and write paths. Implements a cache-aside pattern for seat map reads.
- Redis (Seat Map Cache): Stores per-event seat data as a Redis Hash keyed by
seats:{event_id}. Each field is aseat_id; each value is a JSON blob with status and price. Field-levelHSETupdates on status changes keep it current.
Request walkthrough:
- Client sends
GET /v1/events/{event_id}/seats. - Booking Service runs
HGETALL seats:{event_id}on Redis. - Cache hit: returns seat data directly (under 2ms).
- Cache miss: reads all seats from PostgreSQL, populates the Redis hash, returns the result.
- Seat status changes (during reservations and bookings) call
HSET seats:{event_id} {seat_id} {new_status}to keep Redis synchronized.
This read path serves the high fan-out browsing traffic efficiently. The harder problem is what happens the moment two users click "Hold" on the same seat at the same time. That is the next section.
3. Seat reservation and the double-booking problem
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.