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 seats→Atomic hold→Payment→Confirm 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 / flow | Purpose | Notes |
|---|---|---|
| GET /v1/events/{id}/seats | Seat map + availability | Static layout from CDN; availability overlay from Redis (available|held|booked) |
| POST /v1/events/{id}/holds | Hold seats | {seatIds[]} → {holdId, expiresAt}; atomic; 409 if taken |
| DELETE /v1/holds/{holdId} | Release hold | Frees seats in Redis; idempotent |
| POST /v1/bookings | Confirm booking | {holdId, paymentToken} → bookingId; transactional |
| GET /v1/bookings/{id} | Booking details | QR code, seat info, event details |
| GET /v1/events/{id}/queue | Waiting room status | {position, estimatedWait} — admit when position < threshold |
⑦ Data model & storage
seats:
holds (Redis):
bookings:
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| Store | What | Why |
|---|---|---|
| Redis | Seat holds + availability cache | Atomic SET NX; TTL auto-expires holds; 400K concurrent holds |
| PostgreSQL | Bookings + seat inventory (source of truth) | ACID transactions; row-level lock on confirm |
| CDN | Static seat map SVG/layout | Event layout immutable; availability fetched separately |
| Kafka | Booking events | Payment 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
| Decision | Option A | Option B | Pick when |
|---|---|---|---|
| Hold lock | Redis SET NX (pessimistic) | DB optimistic version | Redis faster for hold; DB for final confirm |
| Sale spike | Virtual waiting room | Scale backend 100× | Waiting room standard; over-provisioning expensive |
| Hold duration | 5 min | 15 min | 5 min reduces inventory lock; 15 min better UX for slow payers |
| Availability | Redis cache | Direct DB read | Redis for speed; DB authoritative on booking |
| Payment fail | Release hold immediately | Retry payment 3× | Release prevents inventory lock; retry better UX |
⑩ 45-minute interview script
- 0–5 min: Clarify browse, hold, pay, confirm flow; flash sale scenario
- 5–12 min: Scale — 100K concurrent, 10K seats in 60s
- 12–22 min: Architecture — waiting room, hold service, inventory, payment
- 22–32 min: Deep dive Redis atomic hold Lua script
- 32–38 min: Booking confirmation DB transaction
- 38–42 min: Virtual waiting room for spike
- 42–45 min: Double-booking prevention guarantee
⑪ Likely follow-up questions
| Question | Short 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