Ticket Booking System
IntermediateOverview
A ticket booking system reserves finite inventory - a specific seat, hotel room, or event ticket - under heavy concurrent demand. The defining challenge is preventing double-booking: two users must never be sold the same seat, even when thousands click "buy" for a hot concert in the same second. This is fundamentally a concurrency and consistency problem, not a raw-throughput one.
Functional Requirements
- Browse events/venues and see seat availability.
- Hold selected seats for a short window while the user pays.
- Confirm a booking after successful payment; release the hold if payment fails or times out.
- Cancel/refund a booking and return the seat to inventory.
Non-Functional Requirements
- Strong consistency on inventory: a seat is sold at most once - no double-booking, ever.
- Availability for browsing: read-heavy browsing must stay up even under load.
- Handle spikes: a popular on-sale creates massive contention on a small set of seats.
- Fairness: users who commit first should win; avoid indefinite starvation.
Capacity Estimation
Assume a hot event with 100K seats and 1M users rushing at on-sale.
- Read QPS: browsing/availability checks dominate — potentially hundreds of thousands/sec during on-sale, mostly cacheable.
- Write QPS (holds/bookings): bounded by inventory — only 100K seats can ever be sold, so successful writes are capped, but attempts massively exceed supply, creating contention on the same rows.
- Storage: modest — events, seats, bookings are small structured records (GBs, not TBs).
The insight: this is a low-write, high-contention problem. The scale challenge is concurrent contention for scarce rows, not data volume.
High-Level Architecture
The system is divided into a highly-cached Search/Browse Service for displaying available seats, and a strongly consistent Booking Service for transactions. Redis is used heavily to manage temporary seat locks (holds) with TTLs. A relational database (PostgreSQL) acts as the source of truth for final inventory and uses ACID transactions to prevent double-booking.
Data Model
| Entity | Fields / Schema | Storage Choice |
|---|---|---|
| event | event_id (PK), venue_id, start_time | Relational, cached |
| seat | seat_id (PK), event_id, status (available/held/booked), version | Strongly consistent relational store (PostgreSQL) |
| hold | hold_id, seat_id, user_id, expires_at | Redis with TTL (auto-expire) |
| booking | booking_id (PK), user_id, seat_ids, status, payment_id, idempotency_key | Relational, ACID |
Detailed Design
The Two-Phase Reserve-Then-Confirm Flow
Booking is split into a temporary hold and a permanent confirmation:
- Hold: the user selects seats; the system atomically marks them
heldin Redis with a short TTL (e.g., 5–10 minutes). This takes the seats off the market while the user inputs credit card details. - Confirm: after payment succeeds, the system writes the final booking to the DB and deletes the Redis hold. If payment fails or the user abandons checkout, the Redis TTL lapses, and the seats automatically return to
availablewithout any background cleanup scripts.
Preventing Double-Booking (The Crux)
The atomic seat claim must be safe under concurrency. Options, in order of preference:
- Conditional Update / Optimistic Locking:
UPDATE seat SET status='held', version=version+1 WHERE seat_id=? AND status='available' AND version=old_version. If zero rows change, someone beat you - return "seat taken." This needs no long-held database lock and scales beautifully. - Pessimistic Lock:
SELECT ... FOR UPDATEon the seat rows within a transaction. Correct, but holds locks and can create contention/deadlocks on hot seats.
Read Path and Caching
Availability browsing is served from cache and read replicas. It can be slightly stale ("almost sold out") because the authoritative check happens at hold time against the strongly consistent inventory DB. This keeps the massive read load off the transactional store.
Bottlenecks & Solutions
During a Taylor Swift-level on-sale, 10 million users hit the "Buy" button at exactly 10:00:00 AM. A relational DB will instantly crash if 10 million connections try to `UPDATE` the same 50,000 seat rows. To fix this, you must introduce a Virtual Waiting Room. The load balancer routes 99.9% of users into a static queue page, and only lets a trickle of users (e.g., 500/sec) through to the actual booking API to match the DB's throughput capacity.