Skip to main content
Hard ConcurrencyACIDLockingLinkedHashMapReservation

Design Ticketmaster — Event Ticketing System

Design a high-concurrency event ticketing system that prevents double-booking. Covers the ActiveReservationsService with LinkedHashMap, SERIALIZABLE transactions, and in-memory seat locking to handle flash sale traffic.

22 min read · Similar: StubHub, Eventbrite, BookMyShow, SeatGeek

Requirements and Scale

Functional: browse events, view seat maps, select seats, reserve seats (time-limited hold), complete purchase, receive tickets. Non-functional: ZERO double-booking (strongest consistency requirement), high availability, handle flash sales (millions of concurrent users competing for a few thousand seats).

Scale: 500M users, 1M events. For a popular concert, 50K seats go on sale simultaneously to 10M concurrent users. At peak: 100K booking requests/sec for the same event.

The Double-Booking Problem

The hardest problem: two users attempt to book the same seat simultaneously. Both see the seat as available, both proceed to payment — without proper locking, both succeed and one seat is sold twice.

Solution options: (1) DB-level locking (SELECT ... FOR UPDATE) — simple but creates a lock bottleneck at the DB. With 100K concurrent requests, the DB lock queue grows to milliseconds of wait time per seat. (2) Optimistic locking (CAS) — proceed without lock, at commit time check if the seat version changed; if yes, retry. Works well for low contention but catastrophic for flash sales. (3) In-memory reservation service — the preferred approach.

ActiveReservationsService

An in-memory service that owns seat availability state for active events. Data structure: LinkedHashMap<SeatId, Reservation> where each Reservation = {user_id, expiry_timestamp, payment_status}.

LinkedHashMap gives O(1) lookup by seat_id AND maintains insertion order (FIFO) for easy expiry cleanup. To reserve a seat: (1) Check if seat_id is in the map; if yes, seat is taken or held. (2) If not present, atomically insert (seat_id, new Reservation). Atomic insertion prevents race conditions. (3) Return a 10-minute hold timer. The service runs in a single process (or uses distributed locking) to serialize concurrent reservations. Only one server per event shard — so there are no distributed races.

graph TD
  User --> API["Booking API"]
  API --> ActiveResSvc["ActiveReservationsService
(LinkedHashMap per event)"]
  ActiveResSvc --> DB[("Seat DB
(persistent state)")]
  ActiveResSvc --> WaitingSvc["WaitingUsersService
(queue for popular events)"]
  WaitingSvc --> API

WaitingUsersService

For flash sales where demand vastly exceeds supply (10M users for 50K seats): instead of accepting all reservation requests and returning failures, add users to a waiting queue. WaitingUsersService: LinkedHashMap<UserId, WaitEntry> where WaitEntry = {seat_preference, joined_at}. When a reservation expires (user didn't complete payment in 10 min), the seat is released and offered to the next user in the waiting queue via notification. The queue is FIFO to ensure fairness.

Database Transactions — SERIALIZABLE Isolation

Even with the in-memory reservation service, the final DB write (marking a seat as sold) must be ACID-compliant. Use SERIALIZABLE transaction isolation level (strongest level): guarantees that concurrent transactions see results as if they ran sequentially. This prevents phantom reads and lost updates. The transaction: (1) BEGIN TRANSACTION. (2) SELECT seat with LOCK (FOR UPDATE). (3) Verify seat is still reserved for this user and not expired. (4) Mark seat as SOLD, deduct from inventory. (5) Create booking record. (6) COMMIT. If any step fails, ROLLBACK.

SERIALIZABLE is the most restrictive isolation level but necessary for ticketing where a double-sold seat is unacceptable.

Handling Flash Sale Traffic

10M concurrent users hitting the booking page: (1) CDN: static pages (event info, seat map images) served from CDN — no server hits. (2) Rate limiting: per-user quota (e.g., max 5 requests/sec) to prevent bots. (3) Virtual queue: on sale start, users enter a randomized virtual queue rather than all hitting the server at once. (4) Read replicas: seat availability reads (show available seats on map) go to read replicas; only reservations hit the primary ActiveReservationsService. (5) Horizontal scaling of the API tier — stateless, so add servers freely. The ActiveReservationsService is the bottleneck — one instance per event, sharded by event_id.

View all →

Syed Peera Saheb

LinkedIn · Substack

Buy me a coffee