System Design

Design Ticket Booking (Ticketmaster)

Seat inventory, concurrency, temporary holds, payment, and preventing double booking at sale spikes.

Interview tip Core problem: atomic seat reservation. Use DB row lock or distributed lock (Redis) + transactional booking flow. Virtual waiting room for on-sale spikes. Hold expires in 5–10 minutes.

① Functional requirements

  • Browse events and view available seats on interactive map
  • Select one or more seats; system holds them temporarily (5–10 min)
  • Complete payment within hold window to confirm booking
  • Release hold automatically on timeout or user cancel
  • Prevent double booking — seat sold to only one user
  • Send confirmation email with tickets (QR code)
  • Waitlist when event sold out
Out of scope (state in interview)
  • Dynamic pricing / surge pricing algorithm
  • Resale marketplace
  • Venue management / seat configuration UI
  • Fraud detection (brief mention)

② Non-functional requirements

  • Zero double bookings (correctness > availability)
  • Hold + payment flow completes in < 10 minutes
  • Support 100K concurrent users at sale opening
  • Seat map loads in < 2s
  • Booking confirmation within 30s of payment

③ Back-of-the-envelope scale

Assumptions
  • 50M tickets/year → ~1.6 tickets/sec avg; flash sale: 10K seats in 60s → 167 seats/sec
  • 100K concurrent users at sale open → 100K seat map loads + selection attempts
  • Seat inventory: 10K events × 10K seats = 100M seat records
  • Holds: 100K concurrent × 4 seats avg = 400K active holds in Redis
  • Payment: 167 payments/sec peak → payment gateway handles 1K/sec (OK)
  • Seat map read: 100K concurrent → CDN cache static map; availability overlay from Redis
Virtual waiting room queues 100K users; admit 10K at a time with random queue tokens. Seat availability in Redis (SET per seat status) for sub-ms atomic check-and-hold. PostgreSQL for confirmed bookings (ACID).

④ High-level architecture

Ticket Booking System
Users (flash sale spike)
Virtual Waiting Room (queue)
CDN (seat map static assets)
Booking API
Seat Map Service
Hold Service (Redis atomic)
Inventory DB (PostgreSQL)
Payment Service
Confirmation + QR Generator
Notification (email ticket)
Select seatsAtomic holdPaymentConfirm booking
Hold flow: Redis SET seat:{eventId}:{seatId} = userId NX EX 600 (set only if not exists, 10 min TTL). If OK → hold granted. Payment success → DB transaction: INSERT booking, UPDATE seat status=booked, DELETE Redis hold. All in one DB transaction.

⑤ Data flow & execution path

End-to-end execution flow
① Client② API / LB③ Core services④ Cache + DB⑤ Message queue⑥ Async workers
Sync path: validate → authorize → read/write primary store
Async path: publish domain events → consumers (email, analytics, search index)
Read-heavy path: CDN / edge cache → regional cache → DB replica
Failure path: retry with backoff, DLQ, idempotent handlers
In interviews, trace one user action through this diagram. State what is synchronous (user waits) vs asynchronous (background), and where you enforce idempotency.

⑥ API & interfaces

Endpoint / flowPurposeNotes
GET /v1/events/{id}/seatsSeat map + availabilityStatic layout from CDN; availability overlay from Redis (available|held|booked)
POST /v1/events/{id}/holdsHold seats{seatIds[]} → {holdId, expiresAt}; atomic; 409 if taken
DELETE /v1/holds/{holdId}Release holdFrees seats in Redis; idempotent
POST /v1/bookingsConfirm booking{holdId, paymentToken} → bookingId; transactional
GET /v1/bookings/{id}Booking detailsQR code, seat info, event details
GET /v1/events/{id}/queueWaiting room status{position, estimatedWait} — admit when position < threshold

⑦ Data model & storage

seats: event_id, seat_id, section, row, number, status (available|held|booked), version (optimistic lock)

holds (Redis): hold:{holdId} → {userId, seatIds[], expiresAt}

bookings: booking_id, user_id, event_id, seat_ids[], payment_id, status, qr_code
StoreWhatWhy
RedisSeat holds + availability cacheAtomic SET NX; TTL auto-expires holds; 400K concurrent holds
PostgreSQLBookings + seat inventory (source of truth)ACID transactions; row-level lock on confirm
CDNStatic seat map SVG/layoutEvent layout immutable; availability fetched separately
KafkaBooking eventsPayment confirm → async QR generation + email

⑧ Deep dive — core components

Atomic seat hold with Redis

Lua script (atomic): for each seatId, SET seat:{eventId}:{seatId} {userId} NX EX 600. If any SET fails (already held/booked), rollback all previous SETs in script (DEL). Return success only if all seats held. Prevents partial hold race.
Hold expiration: Redis TTL auto-releases. Backup: cron scans holds expiring in 30s, proactively DEL. On expiry event (Redis keyspace notification), update seat map availability.

Booking confirmation transaction

On payment success: BEGIN TRANSACTION → verify holds still valid (check Redis) → UPDATE seats SET status=booked, version=version+1 WHERE seat_id IN (...) AND version=expected → INSERT booking → COMMIT → DEL Redis holds → emit booking event.
Optimistic locking: if version mismatch (concurrent booking somehow passed hold), ROLLBACK and refund payment. Pessimistic: SELECT FOR UPDATE on seat rows — slower but simpler for interview.

Virtual waiting room

Sale opens: all users land on waiting room. Enqueue user_id in Kafka/Redis queue with timestamp. Admit users at rate of 10K/min (protect backend). Client polls queue position every 5s. When admitted: receive session token (30 min validity) → access seat map. Token validated on every API call.
Bots: CAPTCHA on queue entry; rate limit per IP; device fingerprint. Queue token non-transferable (bound to session).

Seat map at scale

Static seat layout (SVG coordinates) served from CDN — never changes. Availability overlay: lightweight API returns {seatId: status} map from Redis (~100KB for 10K seats). Client merges locally. WebSocket push for availability changes (optional — polling every 5s sufficient).

⑨ Trade-offs & alternatives

DecisionOption AOption BPick when
Hold lockRedis SET NX (pessimistic)DB optimistic versionRedis faster for hold; DB for final confirm
Sale spikeVirtual waiting roomScale backend 100×Waiting room standard; over-provisioning expensive
Hold duration5 min15 min5 min reduces inventory lock; 15 min better UX for slow payers
AvailabilityRedis cacheDirect DB readRedis for speed; DB authoritative on booking
Payment failRelease hold immediatelyRetry payment 3×Release prevents inventory lock; retry better UX

⑩ 45-minute interview script

  1. 0–5 min: Clarify browse, hold, pay, confirm flow; flash sale scenario
  2. 5–12 min: Scale — 100K concurrent, 10K seats in 60s
  3. 12–22 min: Architecture — waiting room, hold service, inventory, payment
  4. 22–32 min: Deep dive Redis atomic hold Lua script
  5. 32–38 min: Booking confirmation DB transaction
  6. 38–42 min: Virtual waiting room for spike
  7. 42–45 min: Double-booking prevention guarantee

⑪ Likely follow-up questions

QuestionShort answer
Two users click same seat simultaneously?Redis SET NX — only one succeeds; other gets 409 Seat Unavailable
Payment succeeds but DB commit fails?Idempotent payment ref; retry commit; if hold expired, refund automatically
Hold expires during payment?Extend hold on payment initiation; payment gateway webhook confirms within extended window
Bot buys all tickets?Waiting room + CAPTCHA + per-user purchase limit (4 tickets) + rate limit
Event cancelled after sale?Bulk refund job; UPDATE bookings status=cancelled; notify all bookers
How to load test?Simulate 100K queue entries; verify zero double bookings with concurrent hold attempts

⑫ Revision checklist

  • Atomic hold with Redis SET NX
  • Hold TTL auto-expiration (5–10 min)
  • DB transaction on booking confirm
  • Optimistic or pessimistic locking explained
  • Virtual waiting room for flash sales
  • Zero double booking guarantee
  • Seat map: static CDN + availability overlay
  • Payment failure → hold release
  • 100K concurrent users handling
  • Lua script for multi-seat atomic hold
concurrencylockingRedistransactionsflash sale