01

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
02

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)
03

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
04

API & contracts

API Design

Search Events

EXAMPLE
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)

EXAMPLE
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)

EXAMPLE
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)

EXAMPLE
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

EXAMPLE
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"]
}
05

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
06

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
07

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
08

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.

09

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
10

Edge cases

Handling Edge Cases

Payment Fails After Seats Held

EXAMPLE
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 stuck

User Opens Multiple Tabs

EXAMPLE
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

EXAMPLE
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 announcement

Seat Map Inconsistency (Redis vs DB)

EXAMPLE
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 up
11

Tradeoffs

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
12

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

EXAMPLE
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 recovers
13

Production architecture

Full System Architecture (Production-Grade)

Full System Architecture (Production-Grade)Excalidraw diagram · editable shapes · reveal step by stepExplore
14

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.

15

Interview playbook

Interview Tips

  1. 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.

  2. 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.

  3. 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).

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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).

  9. 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.

  10. 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."