Requirements & scope
Problem Statement & Requirements
Design an online ticket booking platform for concerts, sports events, theater, and movies where users can browse events, view venue seat maps, select specific seats, and purchase tickets — similar to Ticketmaster, BookMyShow, StubHub, or Fandango.
Functional Requirements
- Browse & Search events — by city, date range, genre, artist, venue; with filtering and sorting
- View venue seat map — interactive map showing available/taken/reserved seats with pricing tiers
- Select & hold seats — user selects seats, system temporarily holds them (prevents double-booking) for a checkout window
- Book tickets — complete purchase with payment; generate e-tickets with QR codes
- Cancel & refund — cancel a booking, release seats, process refund
- Waitlist — join waitlist for sold-out events; auto-notify when seats become available
- Event management — organizers create events, define venue layout, set pricing tiers, manage inventory
- Dynamic pricing — surge pricing based on demand, time to event, section popularity
- Transfer tickets — transfer purchased tickets to another user
- Notifications — booking confirmation, event reminders, cancellation alerts, waitlist offers
Non-Functional Requirements
- No double-booking — a seat must NEVER be sold to two different people (the cardinal rule)
- High concurrency — handle millions of simultaneous users for popular event on-sales
- Low latency — seat map load < 200 ms; seat hold < 100 ms; checkout < 3 seconds
- High availability — 99.99% during on-sale events
- Fairness — first-come-first-served for seat selection; prevent bot abuse
- Consistency — seat availability must be strongly consistent (no phantom availability)
- Scalability — handle 10M+ concurrent users during mega on-sales (Taylor Swift scenario)
Out of Scope
- Secondary market / resale marketplace (StubHub model)
- Venue physical access control (turnstile scanning)
- Full payment processing internals (see Payment System design)
- Advertising / promotional campaigns
Scale estimations
Scale Estimations
Traffic
| Metric | Value |
|---|---|
| Daily active users (normal) | 10M |
| Peak concurrent users (mega on-sale) | 10M+ simultaneously |
| Events listed (active) | 500K |
| Seats per event (avg) | 15,000 (stadium avg; ranges from 200 to 100,000) |
| Total seat inventory (active) | 500K × 15K = 7.5B seats |
| Bookings / day (normal) | 2M |
| Bookings / second (normal avg) | ~23 TPS |
| Bookings / second (mega on-sale peak) | 50,000+ TPS |
| Seat map views / second (peak) | 500K RPS |
| Seat hold requests / second (peak) | 100K RPS |
Storage
| Metric | Value |
|---|---|
| Event record | 5 KB |
| Seat record | 200 bytes (event_id, section, row, seat, status, price, hold_expiry) |
| Total seat records | 7.5B × 200 B = ~1.5 TB |
| Booking record | 2 KB |
| Bookings per year | 2M/day × 365 = ~730M |
| Booking storage per year | 730M × 2 KB = ~1.5 TB |
| E-ticket/QR data | 730M × 1 KB = ~730 GB/year |
| Venue layouts (SVG/JSON) | 50K venues × 500 KB = ~25 GB |
Bandwidth
| Metric | Value |
|---|---|
| Seat map responses (500K RPS × 20 KB) | ~10 GB/s peak |
| API responses (normal) | ~500 MB/s |
| Seat status WebSocket updates (peak) | ~2 GB/s |
Hardware Estimate
| Component | Spec |
|---|---|
| API servers | 50-200 (auto-scaling, 10x for on-sales) |
| Seat inventory service | 20-50 instances |
| Seat inventory DB | 20-50 shards (PostgreSQL) |
| Redis (seat holds + queue) | 10-20 nodes |
| WebSocket servers (seat map live updates) | 50-100 |
| Virtual waiting room | 20-50 (separate infrastructure) |
Layered architecture
High-Level Architecture
High-Level ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
Component Breakdown
Component BreakdownExcalidraw diagram · editable shapes · reveal step by stepExplore
API & contracts
API Design
Search Events
GET /api/v1/events?city=New+York
&date_from=2026-04-10&date_to=2026-04-30
&genre=music
&sort=date_asc
&page=1&per_page=20
Response (200 OK):
{
"total": 342,
"events": [
{
"event_id": "evt-taylor-msg",
"title": "Taylor Swift | The Eras Tour",
"artist": "Taylor Swift",
"venue": {"name": "Madison Square Garden", "city": "New York", "capacity": 20000},
"date": "2026-04-15T19:30:00-04:00",
"genre": "Pop",
"price_range": {"min": 4900, "max": 49900, "currency": "usd"},
"availability": "limited", // available | limited | sold_out | on_sale_soon
"on_sale_at": "2026-04-05T10:00:00-04:00",
"image_url": "https://cdn.example.com/evt-taylor-msg/poster.jpg"
},
...
]
}Get Seat Map (Availability)
GET /api/v1/events/{event_id}/seats?section=FLOOR
Response (200 OK):
{
"event_id": "evt-taylor-msg",
"venue_layout_url": "https://cdn.example.com/venues/msg/layout.svg",
"sections": [
{
"section_id": "FLOOR-A",
"name": "Floor A",
"pricing_tier": "VIP",
"price": 49900,
"total_seats": 500,
"available_seats": 23,
"seats": [
{"seat_id": "FLOOR-A-R1-S1", "row": "1", "number": "1", "status": "booked"},
{"seat_id": "FLOOR-A-R1-S2", "row": "1", "number": "2", "status": "available"},
{"seat_id": "FLOOR-A-R1-S3", "row": "1", "number": "3", "status": "held"},
...
]
},
{
"section_id": "SEC-101",
"name": "Section 101",
"pricing_tier": "standard",
"price": 9900,
"total_seats": 800,
"available_seats": 412,
"seats": [...]
}
],
"hold_duration_seconds": 420 // 7 minutes to complete purchase
}
WebSocket: wss://api.example.com/ws/events/{event_id}/seats
→ Pushes real-time seat status changes to all connected clients
→ {"type": "seat_update", "seat_id": "FLOOR-A-R1-S2", "status": "held"}Hold Seats (Temporary Reservation)
POST /api/v1/events/{event_id}/holds
Authorization: Bearer ...
Request:
{
"seat_ids": ["FLOOR-A-R1-S2", "FLOOR-A-R1-S4"],
"session_id": "sess-abc123" // ties hold to user session
}
Response (200 OK):
{
"hold_id": "hold-xyz789",
"seat_ids": ["FLOOR-A-R1-S2", "FLOOR-A-R1-S4"],
"hold_expires_at": "2026-04-05T10:07:00Z", // 7 min from now
"total_price": 99800, // 2 × $499.00
"currency": "usd"
}
Response (409 Conflict — seats no longer available):
{
"error": "seats_unavailable",
"unavailable_seats": ["FLOOR-A-R1-S4"],
"message": "One or more selected seats are no longer available"
}Book (Complete Purchase)
POST /api/v1/bookings
Idempotency-Key: book-sess-abc123
Request:
{
"hold_id": "hold-xyz789",
"payment_method_id": "pm_card_4242",
"email": "alice@example.com"
}
Response (201 Created):
{
"booking_id": "bk-def456",
"event": {"event_id": "evt-taylor-msg", "title": "Taylor Swift | The Eras Tour"},
"seats": [
{"seat_id": "FLOOR-A-R1-S2", "section": "Floor A", "row": "1", "seat": "2"},
{"seat_id": "FLOOR-A-R1-S4", "section": "Floor A", "row": "1", "seat": "4"}
],
"total": 99800,
"currency": "usd",
"payment_status": "succeeded",
"tickets": [
{
"ticket_id": "tkt-001",
"qr_code_url": "https://tickets.example.com/tkt-001/qr.png",
"barcode": "TKT-2026-0415-FLOORA-R1S2-XYZ"
},
{
"ticket_id": "tkt-002",
"qr_code_url": "https://tickets.example.com/tkt-002/qr.png",
"barcode": "TKT-2026-0415-FLOORA-R1S4-ABC"
}
],
"created_at": "2026-04-05T10:02:30Z"
}Cancel Booking
POST /api/v1/bookings/{booking_id}/cancel
Response (200 OK):
{
"booking_id": "bk-def456",
"status": "cancelled",
"refund": {
"amount": 99800,
"status": "pending", // refund processed async
"estimated_days": 5
},
"released_seats": ["FLOOR-A-R1-S2", "FLOOR-A-R1-S4"]
}Data model
Data Model
Event
EventExcalidraw diagram · editable shapes · reveal step by stepExplore
Seat (Per Event Instance)
Seat (Per Event Instance)Excalidraw diagram · editable shapes · reveal step by stepExplore
Booking
BookingExcalidraw diagram · editable shapes · reveal step by stepExplore
Seat State Machine
Seat State MachineExcalidraw diagram · editable shapes · reveal step by stepExplore
Core design decisions
Core Design Decisions
Decision 1: Preventing Double-Booking (The #1 Problem)
Two users click the same seat at the same instant. Only one must succeed.
Option A: Pessimistic Lock in Database
Option A: Pessimistic Lock in DatabaseExcalidraw diagram · editable shapes · reveal step by stepExplore
| Pros | Cons |
|---|---|
| Correct, no race conditions | Lock contention = slow under high concurrency |
| Simple, database handles it | Holding row locks during network calls = deadlock risk |
| Works with any SQL DB | Doesn't scale to 100K hold requests/sec |
Option B: Optimistic Lock (CAS) in Database
Option B: Optimistic Lock (CAS) in DatabaseExcalidraw diagram · editable shapes · reveal step by stepExplore
| Pros | Cons |
|---|---|
| No lock contention | Retry storms on hot seats |
| Higher throughput | Wasted work (read + failed write) |
| Short transactions | Starvation possible |
Option C: Redis Atomic SET (Recommended for Holds)
Option C: Redis Atomic SET (Recommended for Holds)Excalidraw diagram · editable shapes · reveal step by stepExplore
| Pros | Cons |
|---|---|
| Atomic, zero race conditions | Redis is not the durable source of truth |
| 100K+ holds/sec (single hot event) | Must sync state back to PostgreSQL |
| Auto-expiry via TTL (no sweeper) | Redis failure = holds lost (graceful degradation needed) |
| Sub-millisecond hold acquisition | Two-layer consistency (Redis + DB) |
Recommendation: Redis for Holds + PostgreSQL for Bookings
Recommendation: Redis for Holds + PostgreSQL for BookingsExcalidraw diagram · editable shapes · reveal step by stepExplore
Decision 2: Virtual Waiting Room
Decision 2: Virtual Waiting RoomExcalidraw diagram · editable shapes · reveal step by stepExplore
Decision 3: Real-Time Seat Map Updates
Decision 3: Real-Time Seat Map UpdatesExcalidraw diagram · editable shapes · reveal step by stepExplore
Request flows
Detailed Flow Diagrams
Seat Hold + Booking Flow
Seat Hold + Booking FlowExcalidraw diagram · editable shapes · reveal step by stepExplore
Hold Expiry Flow
Hold Expiry FlowExcalidraw diagram · editable shapes · reveal step by stepExplore
Waitlist Flow
Waitlist FlowExcalidraw diagram · editable shapes · reveal step by stepExplore
Performance & caching
Workshop note · added for the website’s common reading format
Start with the dominant access pattern of Ticket Booking. Redis holds is one place to inspect capacity and tail latency. Measure before introducing a cache: define the cache key, invalidation policy, stale-data budget, and cold-start behavior.
Batch independent work where the latency budget allows it. Bound queues and concurrency, and verify that an optimization does not move the bottleneck to a dependency.
Advanced design
Bot Prevention & Fairness
Bot Prevention & FairnessExcalidraw diagram · editable shapes · reveal step by stepExplore
Dynamic Pricing
Dynamic PricingExcalidraw diagram · editable shapes · reveal step by stepExplore
E-Ticket Generation & Security
E-Ticket Generation & SecurityExcalidraw diagram · editable shapes · reveal step by stepExplore
Edge cases
Handling Edge Cases
Payment Fails After Seats Held
Scenario: Seats held → payment declined → seats stuck in "held"
Solution: Saga with compensation
1. Seats held (Redis SET NX EX) ✓
2. Payment charged → DECLINED ✗
3. Compensation: Release holds immediately
→ DEL seat_hold:evt:S2, seat_hold:evt:S4
→ Broadcast seat release via WebSocket
→ Return: "Payment failed. Seats released. Please try again."
If our system crashes between steps 2 and 3:
→ Redis TTL auto-releases holds in 7 min
→ DB sweeper catches any stragglers
→ No seats permanently stuckUser Opens Multiple Tabs
Problem: User opens 3 browser tabs, holds different seats in each
→ Blocks 12 seats (4 per tab) while only buying 4
Solution: Per-session hold limit
→ session_id derived from auth token (not tab-specific)
→ Holding new seats auto-releases previous holds for same session
→ Implementation:
Before SET NX, check: "does this user already have holds?"
GET seat_holds_user:{user_id}:{event_id} → [S1, S3]
If yes: release old holds, then acquire new ones
Store: SET seat_holds_user:{user_id}:{event_id} [S2, S4]Event Rescheduled / Cancelled
Event cancelled by organizer:
1. Organizer marks event as "cancelled"
2. System queries all bookings for this event
3. For each booking:
→ Status → "cancelled_by_organizer"
→ Initiate full refund (async)
→ Send notification email/push
4. All holds released immediately (Redis DEL pattern match)
5. Event page updated: "This event has been cancelled"
Event rescheduled to new date:
1. Organizer updates event date
2. All ticket holders notified of new date
3. Buyers given option: keep tickets OR request refund
4. Refund window: 30 days from rescheduling announcementSeat Map Inconsistency (Redis vs DB)
Problem: Redis says seat is available, DB says it's booked
(or vice versa)
Root cause: Crash between Redis operation and DB operation
Solution: DB is ALWAYS source of truth for booked seats
→ Redis only tracks holds (temporary)
→ Final booking requires DB write to succeed
→ If Redis and DB disagree:
Seat in Redis as "held" but DB says "available" → hold is valid
Seat not in Redis but DB says "held" → DB sweeper releases it
Seat in DB as "booked" → always booked, regardless of Redis state
Background reconciliation (every 5 min):
→ Scan DB seats with status='held' where hold_expires_at < NOW()
→ Reset to 'available'
→ Scan Redis holds without matching DB entry → clean upTradeoffs
Tradeoffs & Design Decisions Summary
| Decision | Option A | Option B | Chosen | Why |
|---|---|---|---|---|
| Seat holding | DB pessimistic lock | Redis SET NX EX | Redis | Atomic, 100K+ ops/sec, auto-expiry via TTL; DB for permanent booking |
| Double-booking prevention | DB only | Redis hold + DB book | Two-layer | Redis absorbs contention (holds); DB ensures durability (bookings) |
| High-demand admission | First-come-first-serve | Virtual waiting room | Waiting room | Prevents site crash; fair (random position); controls admission rate |
| Seat map updates | Client polling (5s) | WebSocket/SSE push | WebSocket push | Real-time (<1s); less bandwidth; better UX |
| Seat map granularity | Individual seat push | Section summary + on-demand detail | Hybrid | Section summaries for overview; individual seats only when section clicked |
| Seat DB partitioning | By seat_id hash | By event_id | By event_id | All seats for one event on same shard; critical for atomic multi-seat holds |
| Hold duration | 3 minutes | 7 minutes | 7 minutes | Enough time to enter payment; not so long that seats are blocked unfairly |
| Queue position | First-come (arrival order) | Random (assigned at on-sale) | Random | Eliminates bot speed advantage; fairer for humans with slower connections |
| Pricing | Static (organizer-set) | Dynamic (demand-based) | Dynamic with floor/ceiling | Captures demand value; floor prevents devaluation; ceiling prevents gouging |
| Bot prevention | CAPTCHA only | Multi-layer (CAPTCHA + fingerprint + limits) | Multi-layer | Bots evolve; single measure insufficient; defense in depth |
Reliability & fault tolerance
Reliability & Fault Tolerance
Single Points of Failure & Mitigations
Single Points of Failure & MitigationsExcalidraw diagram · editable shapes · reveal step by stepExplore
Graceful Degradation
Tier 1 (WebSocket down):
→ Clients fall back to polling every 5 seconds
→ Seat map slightly delayed but still functional
→ Hold + booking unaffected
Tier 2 (Recommendation / search degraded):
→ Show trending/popular events as fallback
→ Direct event links still work (most traffic during on-sales)
→ Core booking path unaffected
Tier 3 (Redis hold layer down):
→ Fall back to PostgreSQL FOR UPDATE locking
→ Throughput drops from 100K to ~5K holds/sec
→ Virtual waiting room slows admission to match
→ Slower but still correct — no double-booking
Tier 4 (Seat DB shard down):
→ Events on that shard unavailable for booking
→ Other events on other shards continue normally
→ Auto-failover to standby (< 30s)
→ Holds in Redis preserved during failover window
Tier 5 (Payment service down):
→ Cannot complete bookings
→ Extend hold TTLs automatically (give users more time)
→ Show: "Payment processing delayed — your seats are safe"
→ Process payments when service recoversProduction architecture
Full System Architecture (Production-Grade)
Full System Architecture (Production-Grade)Excalidraw diagram · editable shapes · reveal step by stepExplore
Further exploration
Workshop note · added for the website’s common reading format
Rebuild Ticket Booking from memory, then change one assumption: ten times more traffic, a new region, or a stricter consistency requirement. Which component must change first—and which does not?
Compare Booking service with the same boundary in a related design. Write down one alternative you rejected, what it would simplify, and when you would choose it instead. Follow the source link at the end of this article to explore the original document.
Interview playbook
Interview Tips
Lead with the double-booking problem — "The cardinal rule: a seat must never be sold to two people." Immediately show Redis SET NX EX for holds (atomic, auto-expiring) and PostgreSQL CAS for permanent bookings. Two-layer = contention absorbed by Redis, durability by DB.
Virtual waiting room is the most impressive architectural decision — Most candidates skip this. Explain: 10M concurrent users for 20K seats = site crash without admission control. Random queue position (not first-come) eliminates bot speed advantage. Batched admission (2K/30s) keeps system within capacity.
Hold expiry is critical — 7-minute window with Redis TTL auto-expiry. Explain why: too short = frustrated users; too long = seats locked unfairly. Redis TTL eliminates the need for a sweeper (but include DB sweeper as backup).
Real-time seat map via WebSocket — Don't say "polling every 5 seconds." Explain: Redis Pub/Sub → WebSocket fan-out to 500K clients. Optimization: push section-level summaries, individual seats only on zoom-in.
Saga pattern for checkout — Reserve inventory → charge payment → create booking. On any failure: compensating actions (release holds). On system crash: Redis TTL auto-releases; DB sweeper catches stragglers. No seats permanently stuck.
Partition seat DB by event_id — All seats for one event must be on the same shard. Multi-seat atomic operations (hold 4 adjacent seats) require locality. Range partitioning by event_id achieves this.
Bot prevention is multi-layered — CAPTCHA alone is insufficient. Explain: browser fingerprinting + purchase limits per card/address + ML post-purchase review + verified fan program. Defense in depth.
Dynamic pricing shows business awareness — Price adjusts based on demand velocity, scarcity, time-to-event. Floor/ceiling constraints prevent extremes. Price locked at hold time (no change during checkout).
Waitlist converts cancellations to revenue — Cancelled seats don't just go back to the general pool. Waitlist gets first offer (auto-hold 10 min). If they don't act, next in line. Maximizes sell-through rate.
End with scale numbers — "The system handles 10M concurrent users through the waiting room, admits 2K/30s, supports 100K seat holds/sec via Redis, and books ~5K tickets/sec per event. Redis absorbs contention; PostgreSQL ensures no double-booking."