System Design Case Study

How to handle 5M users booking 50K concert seats simultaneously without double-booking?

?? Design a ticket booking system: 5M users, 50K seats, zero double-booking, fair queue
Concepts Involved

Problem Statement

Design a ticket booking system where 5M users attempt to book 50K concert seats simultaneously, guaranteeing no double booking, fair queue ordering with position tracking, distributed seat locking with expiry, and bot detection.

Core challenge: 100:1 demand-to-supply ratio. 5M users hit "Buy" within seconds of sale opening. You must serialize access to 50K seats without losing fairness, crashing under load, or allowing double-booking.
5M
concurrent users
at sale open
50K
available seats
100:1 ratio
0
double bookings
absolute guarantee
<5s
booking confirmation
once at front of queue

High-Level Architecture

Virtual queue ? seat selection ? distributed lock ? payment ? confirmation

LAYER 1: QUEUE · 5M users ? Virtual Queue (Redis sorted set) + Bot Detection 5M Users hit "Buy" at 10:00:00 100:1 oversubscribed 50K seats available spike: 0 ? 5M in seconds ~3M see "Sold Out" Bot Detection CAPTCHA challenge at entry device fingerprinting velocity checks (rate limit/IP) browser behavior analysis blocks 30-40% bots Virtual Queue Redis Sorted Set (score = arrival timestamp) FIFO: first come, first served position tracking via ZRANK (O(log N)) estimated wait time via SSE (every 5s) batch release: 500 users/min to booking absorbs 5M spike, meters flow SSE Updates position: #4,231 wait: ~8 min push every 5s LAYER 2: BOOKING · Dequeue ? Seat Selection (WebSocket) ? Distributed Lock (Redis SETNX + 5min TTL) ? Payment Seat Selection real-time availability map WebSocket push updates user picks seat(s) available/locked/booked states instant visual feedback Distributed Seat Lock Redis SETNX (atomic lock) TTL = 5 minutes (auto-expire) key: seat:{event_id}:{seat_id} value: user_id + lock_timestamp if expired ? seat released back abandoned carts auto-release Payment idempotency key per attempt Stripe/Razorpay charge retry-safe (no double-charge) timeout = 3 min max success ? confirm fail/timeout ? release lock ? Confirmed ticket issued email + SMS ? Failed lock released LAYER 3: CONFIRMATION · DB UNIQUE constraint (final safety net) + Lock lifecycle PostgreSQL (Source of Truth) seats table: seat_id, event_id, status, locked_by, locked_until UNIQUE constraint on (seat_id, event_id) WHERE status='BOOKED' UPDATE seats SET status='BOOKED' WHERE status='LOCKED' AND locked_by=? even if Redis fails ? DB prevents double-booking DB is final arbiter (Redis is optimization layer) Lock Lifecycle & Guarantees ? No double-booking (Redis lock + DB constraint) ? Fair ordering (timestamp-based queue position) ? Lock expiry (5min TTL auto-releases abandoned carts) ? Idempotent payment (no double-charge on retry) ? Graceful degradation (queue overflow ? "Sold Out") 100:1 demand:supply | Redis lock + DB constraint = double safety | Lock TTL auto-releases abandoned carts | Queue position updates via SSE every 5s 5M users ? 50K seats | Batch release 500/min | CAPTCHA blocks 30-40% bots | Idempotent payment prevents double-charge

Key Design Decisions

DecisionChoiceWhy
QueueRedis Sorted Set (score=timestamp)O(log N) insert, O(1) rank query, handles 5M entries
Seat lockRedis SETNX + TTL (5 min)Distributed, auto-expires, prevents abandoned cart blocking
Double-booking preventionDB UNIQUE constraint + optimistic lockEven if Redis fails, DB is final arbiter
PaymentIdempotency key per booking attemptNetwork retries can't cause double-charge
Seat map updatesWebSocket/SSE push to viewing usersReal-time availability without polling
Batch releaseRelease 500 users from queue per minutePrevents overwhelming seat selection service
Bot detectionCAPTCHA + device fingerprint + rate limitBots can buy 1000s of tickets for resale
Oversell protectionAtomic decrement of inventory counterRedis DECR returns new value · if <0, reject immediately
Locking strategy: Two-phase: Redis lock (fast, distributed, TTL-based) for real-time seat reservation + DB constraint (UNIQUE on seat_id+event_id WHERE status='BOOKED') as final safety net. If Redis and DB disagree, DB wins.
Queue fairness: Users get a queue position (Redis ZRANK) and estimated wait time (position · avg_processing_time). Position updates pushed via SSE every 5s. Users who abandon queue ? position freed for next in line.
Anti-patterns: No queue (direct DB hit) · 5M concurrent connections crash the DB. Infinite lock TTL · abandoned carts block seats forever. Optimistic-only (no lock) · high contention = most users get "seat taken" error. No idempotency · payment retries = double-charge.
Real-world: BookMyShow · Redis queue + seat locking for IPL/concerts. Ticketmaster · virtual waiting room (Queue-it). Amazon · flash sales use atomic inventory decrement. Airbnb · calendar locking with optimistic concurrency for date conflicts.

Interview Cheat Sheet

The 8 things to say for ticket booking design

1. Virtual queue (Redis sorted set) · absorb 5M users, process in order, show position
2. Distributed seat lock (Redis SETNX + TTL) · hold seat for 5 min during checkout
3. DB UNIQUE constraint as safety net · even if Redis fails, DB prevents double-booking
4. Idempotent payment · retry-safe charge with idempotency key (no double-charge)
5. Lock TTL expiry · abandoned carts auto-release seats (no manual cleanup)
6. Bot detection · CAPTCHA at queue entry, device fingerprinting, velocity checks
7. Real-time seat map (WebSocket) · push availability changes to all viewers instantly
8. Graceful degradation · if queue overflows, show "sold out" early rather than crash