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

MetricValue
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

MetricValue
Event record5 KB
Seat record200 bytes (event_id, section, row, seat, status, price, hold_expiry)
Total seat records7.5B × 200 B = ~1.5 TB
Booking record2 KB
Bookings per year2M/day × 365 = ~730M
Booking storage per year730M × 2 KB = ~1.5 TB
E-ticket/QR data730M × 1 KB = ~730 GB/year
Venue layouts (SVG/JSON)50K venues × 500 KB = ~25 GB

Bandwidth

MetricValue
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

ComponentSpec
API servers50-200 (auto-scaling, 10x for on-sales)
Seat inventory service20-50 instances
Seat inventory DB20-50 shards (PostgreSQL)
Redis (seat holds + queue)10-20 nodes
WebSocket servers (seat map live updates)50-100
Virtual waiting room20-50 (separate infrastructure)
03

Layered architecture

Phase 1 of 4 · Client & edge

Start with the entry points

First, define who uses the system and where requests enter. This is the boundary of the design, not yet a complete request path.

Buyer
Separate browsing a seat map from acquiring the right to purchase a seat.
Waiting room
Throttle entry into the checkout path instead of letting every burst hit inventory.
Architecture after phase 1. Includes Buyer, Waiting room. Buyer connects to Waiting room.
Phase 1 — Start with Buyer, Waiting room.SVGSVGEditable diagramEditable diagram

This first layer defines the system boundary. The next chapters add application logic and data dependencies. After examining the request paths and tradeoffs, we will add operational safeguards and bring the complete architecture together.

04

API & contracts

Phase 2 of 4 · Application & compute

Add the application logic

Keep the entry points and add the services that do the work. Give each service a clear responsibility before defining its contracts. Storage and background dependencies come next.

Booking service
Run an explicit booking state machine with a stable booking ID and bounded hold duration.
Seat inventory
Use durable conditional transitions or uniqueness constraints as the final protection against double booking.
Architecture after phase 2. Includes Buyer, Waiting room, Booking service, Seat inventory. Buyer connects to Waiting room. Waiting room connects to Booking service. Booking service connects to Seat inventory.
Phase 2 — Added Booking service, Seat inventory. Earlier layers remain in place.SVGSVGEditable diagramEditable diagram

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

Phase 3 of 4 · Data & async work

Connect data and background work

The services now need their dependencies: durable state, fast lookups, or work that continues outside a request. Separate these responsibilities rather than treating every component as a database.

Redis holds
Use expiring holds for coordination, but never rely on a Redis lock alone as the durable sale guarantee.
Booking database
Commit confirmed ownership atomically and reject stale or already-sold seat transitions.
Payment adapter
Reconcile a payment that completes after a hold expires before confirming or refunding.
Architecture after phase 3. Includes Buyer, Waiting room, Booking service, Seat inventory, Redis holds, Booking database, Payment adapter. Buyer connects to Waiting room. Waiting room connects to Booking service. Booking service connects to Seat inventory. Seat inventory connects to Redis holds. Booking service connects to Booking database. Booking service connects to Payment adapter.
Phase 3 — Added Redis holds, Booking database, Payment adapter. Earlier layers remain in place.SVGSVGEditable diagramEditable diagram

Data Model

Event

Event
EventSVGSVGEditable diagramEditable diagram

Seat (Per Event Instance)

Seat (Per Event Instance)
Seat (Per Event Instance)SVGSVGEditable diagramEditable diagram

Booking

Booking
BookingSVGSVGEditable diagramEditable diagram

Seat State Machine

Seat State Machine
Seat State MachineSVGSVGEditable diagramEditable diagram
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 Database
Option A: Pessimistic Lock in DatabaseSVGSVGEditable diagramEditable diagram
ProsCons
Correct, no race conditionsLock contention = slow under high concurrency
Simple, database handles itHolding row locks during network calls = deadlock risk
Works with any SQL DBDoesn't scale to 100K hold requests/sec
Option B: Optimistic Lock (CAS) in Database
Option B: Optimistic Lock (CAS) in Database
Option B: Optimistic Lock (CAS) in DatabaseSVGSVGEditable diagramEditable diagram
ProsCons
No lock contentionRetry storms on hot seats
Higher throughputWasted work (read + failed write)
Short transactionsStarvation possible
Option C: Redis Atomic SET (Recommended for Holds)
Option C: Redis Atomic SET (Recommended for Holds)
Option C: Redis Atomic SET (Recommended for Holds)SVGSVGEditable diagramEditable diagram
ProsCons
Atomic, zero race conditionsRedis 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 acquisitionTwo-layer consistency (Redis + DB)
Recommendation: Redis for Holds + PostgreSQL for Bookings
Recommendation: Redis for Holds + PostgreSQL for Bookings
Recommendation: Redis for Holds + PostgreSQL for BookingsSVGSVGEditable diagramEditable diagram

Decision 2: Virtual Waiting Room

Decision 2: Virtual Waiting Room
Decision 2: Virtual Waiting RoomSVGSVGEditable diagramEditable diagram

Decision 3: Real-Time Seat Map Updates

Decision 3: Real-Time Seat Map Updates
Decision 3: Real-Time Seat Map UpdatesSVGSVGEditable diagramEditable diagram
07

Request flows

Detailed Flow Diagrams

Seat Hold + Booking Flow

Seat Hold + Booking Flow
Seat Hold + Booking FlowWide diagram — scroll horizontally.SVGSVGEditable diagramEditable diagram

Hold Expiry Flow

Hold Expiry Flow
Hold Expiry FlowSVGSVGEditable diagramEditable diagram

Waitlist Flow

Waitlist Flow
Waitlist FlowWide diagram — scroll horizontally.SVGSVGEditable diagramEditable diagram
08

Performance & caching

Cache venue layouts and event descriptions aggressively, but treat a cached seat color as a hint rather than a reservation. The authoritative hold operation decides availability. Admit traffic through a waiting room at a rate the inventory and checkout path can sustain, and avoid refreshing every seat for every waiting visitor.

09

Advanced design

Bot Prevention & Fairness

Bot Prevention & Fairness
Bot Prevention & FairnessSVGSVGEditable diagramEditable diagram

Dynamic Pricing

Dynamic Pricing
Dynamic PricingSVGSVGEditable diagramEditable diagram

E-Ticket Generation & Security

E-Ticket Generation & Security
E-Ticket Generation & SecuritySVGSVGEditable diagramEditable diagram
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

DecisionOption AOption BChosenWhy
Seat holdingDB pessimistic lockRedis SET NX EXRedisAtomic, 100K+ ops/sec, auto-expiry via TTL; DB for permanent booking
Double-booking preventionDB onlyRedis hold + DB bookTwo-layerRedis absorbs contention (holds); DB ensures durability (bookings)
High-demand admissionFirst-come-first-serveVirtual waiting roomWaiting roomPrevents site crash; fair (random position); controls admission rate
Seat map updatesClient polling (5s)WebSocket/SSE pushWebSocket pushReal-time (<1s); less bandwidth; better UX
Seat map granularityIndividual seat pushSection summary + on-demand detailHybridSection summaries for overview; individual seats only when section clicked
Seat DB partitioningBy seat_id hashBy event_idBy event_idAll seats for one event on same shard; critical for atomic multi-seat holds
Hold duration3 minutes7 minutes7 minutesEnough time to enter payment; not so long that seats are blocked unfairly
Queue positionFirst-come (arrival order)Random (assigned at on-sale)RandomEliminates bot speed advantage; fairer for humans with slower connections
PricingStatic (organizer-set)Dynamic (demand-based)Dynamic with floor/ceilingCaptures demand value; floor prevents devaluation; ceiling prevents gouging
Bot preventionCAPTCHA onlyMulti-layer (CAPTCHA + fingerprint + limits)Multi-layerBots evolve; single measure insufficient; defense in depth
12

Reliability & fault tolerance

Phase 4 of 4 · Operations & resilience

Add operational safeguards

With the request paths and tradeoffs established, add the controls that help detect trouble and keep the system operating. The final diagram retains every layer introduced so far.

Live seat map
Push best-effort availability updates; revalidate at hold time because the displayed map may be stale.
Architecture after phase 4. Includes Buyer, Waiting room, Booking service, Seat inventory, Redis holds, Booking database, Payment adapter, Live seat map. Buyer connects to Waiting room. Waiting room connects to Booking service. Booking service connects to Seat inventory. Seat inventory connects to Redis holds. Booking service connects to Booking database. Booking service connects to Payment adapter. Seat inventory connects to Live seat map.
Phase 4 — Added Live seat map. Earlier layers remain in place.SVGSVGEditable diagramEditable diagram

Reliability & Fault Tolerance

Single Points of Failure & Mitigations

Single Points of Failure & Mitigations
Single Points of Failure & MitigationsSVGSVGEditable diagramEditable diagram

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

High-Level Architecture

High-Level Architecture
High-Level ArchitectureSVGSVGEditable diagramEditable diagram

Component Breakdown

Component Breakdown
Component BreakdownSVGSVGEditable diagramEditable diagram

Full System Architecture (Production-Grade)

Full System Architecture (Production-Grade)
Full System Architecture (Production-Grade)Wide diagram — scroll horizontally.SVGSVGEditable diagramEditable diagram
14

Further exploration

Hold four adjacent seats, then lose the client connection after payment starts. Decide which durable identifier lets the buyer recover the booking outcome. Next, let the hold expire while the payment provider is delayed: define a resolution that does not both release the seats and silently keep the charge.

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