System Design: Design a Ticket Booking System (Ticketmaster/Fandango)
A ticket booking system is a classic system design problem involving seat inventory management, race conditions, payment processing, and fairness under extremely high traffic spikes (viral event on-sale moments).
Requirements
Functional: browse events, search by location/date/artist, view seat map, reserve seats (hold temporarily), complete purchase, receive tickets, cancel/refund.
Non-functional: strong consistency for seat allocation (no double-booking), low latency for search, handle spikes (Taylor Swift on-sale: millions of requests in seconds), support 10M+ events, 1B+ tickets sold.
Core Challenge: Preventing Double-Booking
Two users cannot purchase the same seat. This requires careful concurrency control.
Option 1: Optimistic Locking
Each seat row has a version number. Read seat + version, attempt update with WHERE version=old_version. If another transaction committed first, version mismatch → retry.
-- Reserve seat atomically
UPDATE seats
SET status = 'held', held_by = :user_id, held_until = NOW() + INTERVAL '10 minutes', version = version + 1
WHERE seat_id = :seat_id AND status = 'available' AND version = :expected_version;
-- If 0 rows updated: seat taken, show error
Option 2: Redis Distributed Lock
Use Redis SET NX EX to acquire a lock per seat. Only the process holding the lock can reserve the seat. Release after committing to DB.
lock_key = f"seat_lock:{seat_id}"
acquired = redis.set(lock_key, user_id, nx=True, ex=30) # 30s timeout
if not acquired:
return "Seat already being reserved"
try:
# reserve in DB
db.execute("UPDATE seats SET status='held'... WHERE seat_id=?", seat_id)
finally:
redis.delete(lock_key)
Seat Hold Flow
User clicks seat
│
▼
Hold Request
│
┌─────▼────────────────────────────────────┐
│ Redis: SET seat:{id}:held {user_id} EX 600│ (10-min hold)
└─────┬────────────────────────────────────┘
│ success
▼
Checkout timer starts (10 min countdown)
│
User completes payment
│
▼
Payment Service (Stripe/Braintree)
│ success
▼
Mark seat SOLD in PostgreSQL
Send confirmation + e-ticket (email/push)
│
If timer expires without payment:
▼
Release hold → seat becomes available again
Handling Traffic Spikes (On-Sale Moments)
When a major event goes on sale, millions of users hit the system simultaneously. Solutions:
- Virtual waiting room: queue users on entry, issue time-stamped tokens, let them in N at a time. Fair, prevents server overload. Ticketmaster, Amazon use this.
- Queue-based inventory: pre-allocate seat groups to queues; each queue worker processes one user at a time for its group
- Read replicas: all seat browsing hits read replicas; only reservation writes hit primary DB
- Cache availability map: cache section-level availability in Redis; only query DB for specific seat selection
Data Model
events (id, name, venue_id, start_time, artist_id, ...)
venues (id, name, address, capacity, seat_map_url)
seats (id, event_id, section, row, number, status, held_by, held_until, sold_to)
orders (id, user_id, event_id, total, status, created_at)
order_items (id, order_id, seat_id, price)
tickets (id, order_id, seat_id, qr_code_hash, issued_at)
Search and Discovery
- Full-text search: Elasticsearch for artist name, event name, venue search
- Geo search: PostGIS or Elasticsearch geo queries for “events near me”
- Faceted filters: date range, price range, category, availability
- Trending: pre-computed trending events updated every 5 minutes from purchase velocity
Ticket Validation (Anti-Fraud)
- Each ticket has a unique QR code containing signed JWT (venue ID, event ID, seat ID, expiry)
- Scanner app verifies signature offline or via low-latency API call
- Mark ticket as scanned in Redis to prevent reuse
- Detect suspicious patterns: same ticket scanned twice, bulk purchases from single account
Interview Checklist
- Lead with the double-booking problem — it’s the crux; explain optimistic locking or Redis lock
- Describe the hold → payment → confirmed flow with TTL expiry
- Address traffic spike: virtual waiting room is the industry standard answer
- Separate read (search/browse) from write (reservation) paths
- Mention ticket validation with signed QR codes
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you prevent double-booking in a ticket reservation system?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “Use optimistic locking: each seat row has a version number. Read the seat and its version, then update with WHERE version = expected_version. If 0 rows are affected, another user reserved it first—show an error. Alternatively, use a Redis distributed lock (SET NX EX) per seat: only the process that acquired the lock can proceed with reservation. The database constraint (status check) acts as a final safety net regardless of the locking approach.” }
},
{
“@type”: “Question”,
“name”: “How does a virtual waiting room work for high-traffic ticket on-sales?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “When a popular event goes on sale, a virtual waiting room queues incoming users before letting them access the purchase flow. Users are assigned a queue position and see an estimated wait time. The system admits N users per minute based on backend capacity. Users get a time-limited session token to complete their purchase. This prevents the backend from being overwhelmed, ensures fairness, and is more reliable than first-come-first-served under thundering herd conditions.” }
},
{
“@type”: “Question”,
“name”: “How do you implement seat holds with automatic expiry in a ticket system?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “When a user selects a seat, create a hold record in Redis with a TTL (typically 10 minutes): SET seat:{event_id}:{seat_id}:held {user_id} EX 600. Also update the seat status in the database to 'held' with a held_until timestamp. A background job (or DB scheduled query) releases seats where held_until has passed and status is still 'held'. When the user completes payment, atomically update status to 'sold' before the hold expires.” }
}
]
}