Event Ticketing System Low-Level Design

Requirements

  • Users can browse events, select seats from a seat map, reserve seats, and complete payment
  • Prevent double-booking: no two users can book the same seat for the same event
  • Handle demand spikes: popular events (Taylor Swift, Super Bowl) have tens of thousands of simultaneous buyers
  • Seat holds expire after 10 minutes if payment is not completed

Data Model

Event(event_id, venue_id, name, date, status ENUM(ONSALE,SOLDOUT,CANCELLED))
Venue(venue_id, name, total_capacity, seat_map_json)
Seat(seat_id, venue_id, section, row, number, category ENUM(GA,FLOOR,BALCONY))
SeatInventory(seat_id, event_id, status ENUM(AVAILABLE,HELD,SOLD), held_by, held_until)
Order(order_id, user_id, event_id, status ENUM(PENDING,CONFIRMED,EXPIRED,CANCELLED),
      created_at, expires_at, total_amount)
OrderItem(item_id, order_id, seat_id, price)

Seat Reservation Flow

  1. User selects seats on the seat map
  2. POST /orders/hold: BEGIN TRANSACTION; SELECT * FROM SeatInventory WHERE seat_id IN (…) AND event_id=X FOR UPDATE; verify all are AVAILABLE; UPDATE SeatInventory SET status=’HELD’, held_by=user_id, held_until=NOW()+10min; create Order (PENDING, expires_at=+10min); COMMIT
  3. User completes payment within 10 minutes
  4. POST /orders/{id}/confirm: BEGIN TRANSACTION; verify Order status=PENDING and not expired; charge payment; UPDATE SeatInventory SET status=’SOLD’; UPDATE Order SET status=’CONFIRMED’; COMMIT

The SELECT FOR UPDATE serializes concurrent seat selections. If two users try to hold the same seat, one gets the lock and succeeds; the other finds status=’HELD’ and fails with SeatUnavailable error.

Hold Expiry

A background job runs every 30 seconds: SELECT * FROM SeatInventory WHERE status=’HELD’ AND held_until < NOW(). For each expired hold: UPDATE SeatInventory SET status=’AVAILABLE’, held_by=NULL, held_until=NULL WHERE status=’HELD’ AND seat_id=X (compare-and-swap to avoid racing with a concurrent payment confirmation). UPDATE Order SET status=’EXPIRED’. Notify the next user in the queue (if waitlist exists).

High-Demand Sale Architecture

  • Virtual queue: before sale opens, users join a queue. At sale time, release users in batches (e.g., 1000 every 30 seconds). Each released user gets a signed purchase token (JWT) valid for 10 minutes. Only token holders can enter the purchase flow.
  • Rate limiting: max 1 queue entry per user per event. Redis SET nx per user+event.
  • CDN: event listing page and seat map SVG served from CDN — no origin hit until seat selection
  • Read replicas: seat availability queries go to read replica; writes go to primary
  • Seat map cache: cache seat availability bitmap in Redis (bitset per event, bit per seat), updated on each reservation. Display uses cached bitmap; actual booking uses DB.

Seat Map Availability at Scale

Fetching all 50,000 seats’ status per page load from DB would be too slow. Use a Redis bitset: key=availability:{event_id}, one bit per seat_id. Bit=1 means available. SETBIT on reserve, GETBIT on display. The bitmap for a 50,000-seat venue is only 6KB. Cache with TTL=5s; slight staleness is acceptable for display (actual booking still validates in DB). Serve the bitmap to clients; render the seat map in JavaScript.

APIs

GET  /events/{id}/seats          → seat availability bitmap
POST /orders/hold                → {event_id, seat_ids[]} → Order
POST /orders/{id}/confirm        → {payment_token} → Order (CONFIRMED)
DELETE /orders/{id}              → cancel hold, release seats
GET  /orders/{id}                → Order status

Key Design Decisions

  • SELECT FOR UPDATE on SeatInventory rows prevents double-booking without application-level locking
  • 10-minute hold + expiry job ensures inventory is not held hostage by abandoned sessions
  • Virtual queue absorbs the spike; DB sees metered traffic, not a stampede
  • Redis bitset for display, DB for authoritative booking


{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you prevent double-booking in an event ticketing system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use pessimistic locking with SELECT FOR UPDATE within a database transaction. When a user attempts to hold seats: BEGIN TRANSACTION; SELECT * FROM SeatInventory WHERE seat_id IN (…) AND event_id=X FOR UPDATE; verify all rows have status=AVAILABLE; UPDATE status=HELD with held_by and held_until; create the Order; COMMIT. The FOR UPDATE lock prevents any other transaction from modifying these seat rows until the first transaction commits or rolls back. If two users try to hold the same seat concurrently, one transaction gets the lock and succeeds; the second finds status=HELD and returns a SeatUnavailable error. Never use optimistic locking for seat reservation — the user experience of completing a form only to find the seat taken is terrible.”}},{“@type”:”Question”,”name”:”How do seat holds and expiry work in a ticketing system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”When a user selects seats, create a SeatInventory hold with status=HELD and held_until=NOW()+10 minutes. This gives the user time to complete payment without the seats appearing available to others. A background job runs every 30 seconds: find all SeatInventory rows where status=HELD AND held_until < NOW(). For each expired hold: UPDATE SeatInventory SET status=AVAILABLE, held_by=NULL, held_until=NULL WHERE status=HELD AND seat_id=X (compare-and-swap to avoid a race where payment completes at the exact expiry moment — if the concurrent payment already set status=SOLD, the UPDATE finds status!=HELD and skips). Mark the corresponding Order as EXPIRED. Notify the next user in the waitlist.”}},{“@type”:”Question”,”name”:”How do you handle high-demand ticket sales (Taylor Swift / Super Bowl)?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Five-layer protection: (1) Virtual queue: before sale opens, users register interest. At sale time, release users in controlled batches (e.g., 2000 every 30 seconds). Each released user gets a signed purchase token (JWT, 10-minute TTL) required to access the purchase flow. (2) Rate limiting: max 1 queue registration per user per event (Redis SET NX per user+event). (3) CDN: serve the event page and seat map from CDN — zero origin hits for browsing. (4) Redis seat availability bitmap: 6KB bitset representing all seats, served for display; actual booking validates in DB. (5) Queue depth monitoring: if queue processing falls behind, pause token issuance until backlog clears.”}},{“@type”:”Question”,”name”:”How do you display seat map availability at scale?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Fetching all 50,000 seat statuses per page load from a relational DB is too slow under high concurrency. Maintain a Redis bitset: key=availability:{event_id}, one bit per seat_id. 1=available, 0=held or sold. Set bit=0 on hold, set bit=0 on sale, set bit=1 on hold expiry. The bitset for a 50,000-seat venue is 6KB — fits in a single Redis key. Serve the bitset directly to the browser (base64-encoded or as a binary response); render the seat map in JavaScript by coloring available seats green and unavailable seats grey. Add a 5-second TTL for slight staleness (acceptable for display). Authoritative availability is always confirmed in the DB at the time of booking.”}},{“@type”:”Question”,”name”:”How do you model pricing tiers and dynamic pricing in a ticketing system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Seat categories (Floor, Balcony, GA) have a base price set at event creation. Dynamic pricing: maintain a PriceSchedule table (event_id, category, effective_from, price). The active price for a seat is the latest PriceSchedule entry where effective_from <= NOW(). Prices can increase as sections fill up (supply-demand dynamic pricing). Surge: if a section is 80% sold, trigger a price increase for remaining seats. Store the price at order creation time on OrderItem — the buyer pays the price shown, even if the price changes before they complete payment. Show the price in the UI before seat selection confirmation to avoid surprises.”}}]}

Airbnb system design covers reservation and availability systems. See common questions for Airbnb interview: reservation and booking system design.

Stripe system design interviews cover payment flows and atomic reservations. See patterns for Stripe interview: payment and seat reservation system design.

Amazon system design covers high-demand reservation systems. Review design patterns for Amazon interview: ticketing and reservation system design.

Scroll to Top