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
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.
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.
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.
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
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.
Data Model
Event
Seat (Per Event Instance)
Booking
Seat State Machine
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
| 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
| 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)
| 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
Decision 2: Virtual Waiting Room
Decision 3: Real-Time Seat Map Updates
Request flows
Detailed Flow Diagrams
Seat Hold + Booking Flow
Hold Expiry Flow
Waitlist Flow
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.
Advanced design
Bot Prevention & Fairness
Dynamic Pricing
E-Ticket Generation & Security
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
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.
Reliability & Fault Tolerance
Single Points of Failure & Mitigations
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
High-Level Architecture
Component Breakdown
Full System Architecture (Production-Grade)
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.
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."