Designing a Ticket Booking System
Selling 10,000 tickets to 10,000,000 users in 5 minutes is one of the hardest concurrency problems in engineering.
1. Requirements
Functional
- Search: View available seats.
- Select: Click a seat to temporarily hold it.
- Pay: Complete purchase within 5 minutes.
Non-Functional
- Fairness: First Come, First Served (FCFS).
- No Double Booking: Crucial. Two people cannot buy Seat 1A.
- Scalability: Handle 1M+ Requests Per Second (RPS) during "The Drop".
2. The Core Problem: Race Conditions
Imagine a SQL table seats with status='AVAILABLE'.
- User A checks Seat 1A:
SELECT status ...-> Returns 'AVAILABLE'. - User B checks Seat 1A:
SELECT status ...-> Returns 'AVAILABLE'. - User A Buys:
UPDATE seats SET status='SOLD' WHERE id='1A'. - User B Buys:
UPDATE seats SET status='SOLD' WHERE id='1A'.
Both users think they own the ticket. User B overwrites User A. This is a Default (Read Committed) isolation failure.
3. Database Locking Strategies
Solution 1: Pessimistic Locking (SELECT FOR UPDATE)
We lock the row when reading it.
START TRANSACTION;SELECT * FROM seats WHERE id='1A' FOR UPDATE;(User B blocks here).UPDATE seats ...COMMIT;- Problem: Terrible performance. Database locks are expensive. If User A's internet dies, the lock is held until timeout.
Solution 2: Optimistic Locking (Versioning) - Recommended
We add a version column to the row.
- User A reads Seat 1A (Version 1).
- User B reads Seat 1A (Version 1).
- User A writes:
UPDATE seats SET status='SOLD', version=2 WHERE id='1A' AND version=1.- Result: 1 Row Affected. Success.
- User B writes:
UPDATE seats SET status='SOLD', version=2 WHERE id='1A' AND version=1.- Result: 0 Rows Affected. (DB sees version is now 2, not 1).
- Action: Tell User B "Sorry, someone beat you."
4. Architecture: The Booking Flow
We cannot let 10 Million users hit the Postgres DB directly.
Step 1: The Waiting Room (Virtual Queue)
Before you even see seats, you enter a queue.
- Tech: AWS Lambda + Redis Sorted Set / Kafka.
- Logic: Only allow 500 users per second to enter the "Booking Area".
- Benefit: Protects the database from "The Thundering Herd".
Step 2: Temporary Hold (Redis)
When a user clicks "Seat 1A", we don't write to SQL yet. We write to Redis.
SET seat_1A_lock user_id NX EX 300- NX: Only set if not exists (Atomic lock).
- EX 300: Expire in 300 seconds (5 minutes).
- If Redis returns
OK, show "Time Remaining: 5:00" timer. - If Redis returns
NULL, show "Seat taken".
Step 3: Payment & Final Commitment
User enters credit card info.
- Payment Service: Charge card. Success.
- Booking Service: Now we persist to SQL.
UPDATE seats SET status='SOLD', owner='UserA' WHERE id='1A'.
- Cleanup: Delete Redis key (or let it expire).
5. Handling Data Consistency
What if Redis says "Locked" but the SQL Database crashes?
- Reconciliation Worker: A background job checks:
- "Is this seat locked in Redis for > 5 mins?" -> Release it.
- "Is this seat 'SOLD' in SQL but 'Open' in Redis?" -> Sync them.
6. DB Partitioning (Sharding)
A single Postgres instance can't handle 100k writes/sec.
- Shard by Event ID: All seats for "Taylor Swift NYC" live on Shard 1.
- Risks: "Hot Shard" problem. If everyone wants Taylor Swift, Shard 1 melts while Shard 2 is empty.
7. The Virtual Waiting Room, Dissected
The queue is the piece most designs hand-wave, but it's a product surface with real engineering inside:
- Admission tokens: on sale start, every visitor gets a random position (random, not first-come — otherwise the sale rewards bot-grade connection speed). A signed token encodes their position; the frontend polls a lightweight endpoint asking "am I admitted yet?"
- Metered release: admit users at the rate your booking core can absorb — say 500/second — measured continuously from booking-service latency, not guessed. The queue converts a 2-million-user spike into a smooth, survivable flow. This is backpressure with a UI.
- Session budget: admission grants a bounded shopping window (e.g., 10 minutes). Without it, admitted users camp with seats in carts while millions wait outside.
- The queue must be boring tech: it absorbs the full spike, so it's static pages + a counter in Redis + signed tokens behind a CDN — nothing that touches a database. If the waiting room falls over, the whole defense collapses.
8. Fighting the Bots
At a hot on-sale, scalper traffic can dwarf human traffic. Defense is layered, imperfect, and economically motivated — raise the scalpers' cost per ticket:
- Before the queue: CDN-level bot fingerprinting, rate limits per IP/ASN, and proof-of-work or CAPTCHA challenges at queue entry.
- In the queue: one queue slot per authenticated account (not per connection); device fingerprinting to collapse a bot farm's thousand tabs into one identity.
- At purchase: per-account and per-payment-card ticket limits, verified-fan pre-registration, and delayed ticket delivery (barcodes issued 48h before the event) to poison the instant-resale market.
No single layer works; the combination changes scalping from an API race into an expensive identity-fraud operation.
9. Reads at Scale: The Seat Map
While 100k people fight over writes, millions more are watching the seat map. Serving live truth to every viewer would melt the system — and doesn't matter: a map that's 2 seconds stale is fine, because the Redis lock at reservation time is the actual arbiter. So: aggregate availability is pushed to viewers via periodic snapshots (per-section counts, cached at the CDN with 1–5s TTLs) or WebSockets broadcast diffs, and the inevitable "seat taken while you looked" case is handled gracefully at click time. Separating the advisory read path from the authoritative write path is the core scalability move of the whole design.
Summary
- Queue: Throttle users before they reach the DB — random admission, metered release, boring tech.
- Fast Lock: Use Redis
SET NXfor the 5-minute hold. - Safe Write: Use Optimistic Locking (Version #) for the final SQL commit.
- Stale reads are fine: the seat map is advisory; only the lock and the DB are truth.
Related Concepts
About ScaleWiki
ScaleWiki is an interactive educational platform dedicated to demystifying distributed systems, software architecture, and system design. Our mission is to provide high-quality, technically accurate resources for software engineers preparing for interviews or solving complex scaling challenges in production.
Read more about our Editorial Guidelines & Authorship.
Educational Disclaimer: The architectural patterns and system designs discussed in this article are based on common industry practices, technical whitepapers, and public engineering blogs. Actual implementations in enterprise environments may vary significantly based on specific product requirements, legacy constraints, and evolving technologies.
Related Articles
System Design: Instagram News Feed
Designing a scalable social feed. Fan-out on Write vs Fan-out on Read, and solving the Justin Bieber problem.
System Design: URL Shortener (Bit.ly)
Designing a high-read, heavy-scale service like Bit.ly. Deep dive into ID generation (Base62 vs UUID) and Redirection mechanics.
CAP Theorem
Consistency, Availability, Partition Tolerance. Why you can only pick two in distributed systems, and how real databases like MongoDB, Cassandra, and DynamoDB make the trade-off.