Designing a Ticket Booking System
High Level Design·intermediate·~25 min read
- system-design
- ticket-booking
- concurrency
- distributed-systems
- hld
What you'll learn
Requirements
Functional Requirements
1. Search and Discovery — users can search movies, view showtimes, browse cinemas by city 2. Seat Map Viewing — display real-time seat availability for a selected show 3. Seat Selection and Reservation — user selects seats, system tempor…
Non-Functional Requirements
- Consistency — absolutely no double-booking (two users cannot book the same seat) - Low Latency — seat selection and reservation must feel instantaneous - High Availability — search and browsing must always work - Fairness — waiting use…
Extended Requirements
- Notifications (email/SMS/push) for booking confirmation and waiting queue alerts - Booking history and past tickets - Analytics: popular shows, peak hours, revenue dashboards - Multi-language and multi-currency support ---
Capacity Estimation
Traffic
| Metric | Value | |--------|-------| | Page views/month | 3 Billion | | Tickets sold/month | 10 Million | | Reads:Writes ratio | ~300:1 (browsing vs. booking) | | Peak concurrent users | 100K+ during popular movie launch |
Infrastructure
| Resource | Calculation | |----------|------------| | Total seats in system | 500 cities x 10 cinemas x 2000 seats = 10M seats | | Shows per day (estimate) | 500 x 10 x 4 shows/screen x 3 screens = 60K shows/day | | Reservations per sec…
Storage
| Data | Size Estimate | |------|---------------| | Seat record | ~100 bytes (IDs + status + metadata) | | All seats for all shows/month | 10M seats x 4 shows x 30 days x 100B ~ 120GB/month | | Reservations | 10M x 500B ~ 5GB/month | | B…
Core Architecture
Component Responsibilities
| Component | Role | |-----------|------| | Booking Service | Handles seat selection, reservation, payment flow | | ActiveReservationService | Tracks unpaid reservations, expires after 5 min | | WaitingUserService | FIFO queue for users…
Design Evolution & Trade-offs
How this system grew from an MVP to what a production version looks like — and the choices you'd defend in an interview.
Stage 1 — MVP: One box, one database
The simplest thing that could possibly work for a ticket-booking service. - A single API server (monolith) that handles browsing, seat selection, reservation, and payment. - A single relational database (Postgres) with show, seat, reserv…
Stage 2 — Scale-out: Multiple pods behind a load balancer
- API layer becomes stateless; N replicas of the Booking / Show / Search services behind an L7 load balancer. - Sessions and reservation timers moved out of process memory into Redis so any pod can serve any request. - Read replicas abso…
Stage 3 — Bottleneck: Concurrent seat locking & overselling
This is the problem most characteristic of ticket-booking-system: many users, ONE seat, exactly one winner, no double-books. Every option below has been used in production; the trade-off is where you draw the line between latency, correc…
Stage 4 — Second bottleneck: Read-heavy seat-map fan-out & thundering herds
Ticket booking is ~300:1 reads:writes. The seat-map endpoint — "show me every seat's current status for show X" — is hit by every browsing user, refreshed constantly on the client, and can be requested by 100K+ concurrent users the secon…
Stage 5 — Mature architecture
All the additions from stages 2–4, wired together. Sessions and seat locks live in Redis. Postgres primary handles writes; read replicas serve the browse tier. Kafka carries async work — notifications, analytics, expiry events, and per-s…
Cost vs Complexity — When to Pick What
| Concern | MVP choice | Growth choice | Enterprise choice | |---------|-----------|---------------|-------------------| | Compute | Single VM running the monolith | K8s Deployment per service (Booking / Show / Search) | Multi-region K8s…
Migration Path (MVP → Enterprise)
Stepped, in order. Each step is safe to ship on its own and each one unlocks the next. 1. Add read replicas. Zero risk — no writes affected. Point the seat-map and browse endpoints at replicas via a read-only connection pool. Booking wri…
End-to-End Flow
1. User browses shows → Show Service returns available shows 2. User selects a show → Cache returns seat map (with availability hints) 3. User selects seats → Booking Service starts SERIALIZABLE transaction 4. Transaction succeeds → seat…
ActiveReservationService — Deep Dive
Purpose
Keeps track of all currently active (unpaid) reservations and automatically expires them after the 5-minute payment window.
Data Structure
Why LinkedHashMap?
- Maintains insertion order — the oldest reservations are always at the front - O(1) insertion — new reservations go to the end - O(1) lookup by ShowID — check active reservations for any show - O(1) identification of next-to-expire — th…
Background Expiry Thread
Why 5 Minutes?
- Too short (1-2 min): users can't complete payment, leads to frustration - Too long (15-30 min): seats held too long, especially during peak demand - 5 minutes: enough for most payment flows, short enough to keep seats circulating
Timer Skew Handling
The client shows a countdown timer to the user, but the server adds a 5-second buffer beyond the displayed time. This prevents a race condition where the client believes time remains but the server has already expired the reservation. Wi…
WaitingUserService — Deep Dive
Purpose
Fair queuing mechanism for users who want seats that are currently reserved or booked.
Data Structure
The inner LinkedHashMap maintains insertion order so the head pointer always references the longest-waiting user — enabling O(1) FIFO fairness without scanning the queue.
How It Works
1. User requests seats that are all RESERVED/BOOKED 2. System offers: "Join waiting queue? You'll be notified if seats free up" 3. User joins → added to FIFO queue for that ShowID 4. When ActiveReservationService releases seats: - Dequeu…
Edge Cases
- User in queue cancels → remove from queue, shift others forward - More seats released than first user needs → can notify multiple users - Show starts → purge entire queue for that ShowID - Maximum wait time in queue: 1 hour — after thi…
Partial Availability Problem
User #1 wants 10 seats but only 5 are currently available. User #2 wants exactly 5 seats. The system handles this by: when seats become available, all WaitingUserService servers check the free seat count. Waiting users who need more seat…
All-or-Nothing Booking Policy
Users get ALL requested seats or none — no partial ticket orders. If a user requests 4 seats and only 3 are available, the request fails entirely. This simplifies reservation logic (no partial holds, no "do you still want 3 of 4?" flows)…
Database Schema
Tables
Indexes
---
Partitioning Strategy
Partition by ShowID
Key insight: All seats for a single show must be on the same database partition. Why this works: - A booking transaction only touches seats within ONE show - All seats for that show are on the SAME partition - Therefore: no distributed t…
Why NOT Partition by MovieID?
If partitioned by MovieID, a hot movie (blockbuster release) overloads a single server since all shows of that movie land on one partition. ShowID distributes the load naturally because a popular movie has many shows across many theaters…
Why NOT Partition by CinemaID or City?
- A cinema has multiple shows → one partition handles ALL concurrent bookings for all shows at that cinema - Creates unnecessary contention between shows that don't conflict - ShowID gives the finest granularity where transactions actual…
Consistent Hashing for Daemon Services
ActiveReservationService and WaitingUserService are distributed across multiple servers. Use consistent hashing on ShowID to allocate approximately 3 servers per show. This provides: - Load distribution — different shows map to different…
Handling High Concurrency (Popular Movie Launch)
The Problem
When a blockbuster launches, thousands of users try to book the SAME show simultaneously: - All traffic concentrates on ONE show's seats - One DB partition handles all these requests - Normal horizontal scaling doesn't help (it's the sam…
Solutions
1. Vertical Scaling of Hot Partition - Temporarily assign more CPU/RAM to the partition handling the hot show - Works well for predictable peaks (known release dates) 2. Queue-Based Serialization - All booking requests for a show go into…
Caching Strategy
What to Cache
| Data | Cache Strategy | Invalidation | |------|---------------|-------------| | Seat availability map per show | Read-through | Write-through on reservation/booking | | Show listings by cinema/city | TTL-based (5 min) | Update on show…
The Consistency Challenge
Solution: Cache is advisory only - Cache provides fast reads for seat map display - Users may see a seat as "available" that was just reserved - The actual booking attempt ALWAYS goes through a SERIALIZABLE DB transaction - If the DB tra…
Fault Tolerance
Failure Scenarios and Recovery
| Failure | Impact | Recovery | |---------|--------|----------| | Booking Service dies mid-reservation | Seats stuck in RESERVED | ActiveReservationService timer still runs → seats released after 5 min | | Payment Service timeout | User…
Key Resilience Properties
- Self-healing reservations — the 5-minute timeout is a natural circuit breaker. Any failure that prevents payment completion results in seats being automatically freed. - Stateless services — Booking Service is stateless; all state is i…
Fault Tolerance Gap: WaitingUserService
WaitingUserService data is in-memory only — a crash loses the entire waiting queue unless master-slave replication is configured. Unlike ActiveReservationService (which can recover from the Booking table by querying Status = Reserved), t…
Key Design Decisions Summary
| Decision | Rationale | |----------|-----------| | SERIALIZABLE isolation | Prevents double-booking without application-level locks | | 5-minute payment timeout | Prevents seat hoarding while giving users enough time | | 5-second server…
Anti-Abuse Measures
Maximum Seats Per Booking
Restrict users to a maximum of 10 seats per booking to prevent scalping. Without this limit, a single user (or bot) could reserve an entire show's worth of seats, holding them hostage for 5 minutes at a time or reselling at inflated pric…
Enumerations
BookingStatus
| Status | Meaning | |--------|---------| | Requested | User initiated booking, awaiting seat lock | | Pending | Seats reserved, awaiting payment | | Confirmed | Payment successful, ticket issued | | Checked-in | User scanned ticket at t…
SeatType
| Type | Description | |------|-------------| | Regular | Standard seating | | Premium | Extra legroom, better view, higher price | | Accessible | Wheelchair-accessible, companion seats | | EmergencyExit | Exit row seats with extra legro…
PaymentStatus
| Status | Meaning | |--------|---------| | Unpaid | Payment not yet initiated | | Pending | Payment processing in progress | | Completed | Payment successfully charged | | Failed | Payment attempt failed (retry possible) | | Declined |…
Scale Reference Numbers
| Parameter | Value | |-----------|-------| | Cities | 500 | | Cinemas per city | 10 | | Seats per cinema | 2,000 | | Shows per day per cinema | 2 | | Storage per day | 500 x 10 x 2000 x 2 x 100 bytes = 2 GB/day | | 5-year storage | ~3.6…
API Design
Key Endpoints
---
Comparison: Ticket Booking vs. Ride-Sharing Allocation
| Aspect | Ticket Booking | Ride-Sharing (Driver Assignment) | |--------|---------------|----------------------------------| | Resource | Fixed seat (static) | Moving driver (dynamic) | | Contention pattern | Many users → ONE seat | ONE…