Ticketmaster sells 500M+ tickets per year. A Taylor Swift tour launch can generate 3.5 billion system requests in a single day. Designing a ticketing system tests inventory management with strict consistency (no overselling seats), handling flash crowds, and graceful degradation under extreme load. This is a favorite interview question at Ticketmaster, StubHub, Eventbrite, and companies with reservation systems (hotels, restaurants, airlines).
Requirements
Functional: Browse events and view seat maps. Select and reserve seats. Complete checkout with payment within a time window (10 minutes). Cancel reservation if not paid. View booking history. Waitlist for sold-out events.
Non-functional: No double-booking — two users must never get the same seat. Seat availability updates reflect in real time on the seat map. Handle 3.5B requests/day during a Taylor Swift on-sale (spike to 1M+ RPS). Checkout window: hold seat for 10 minutes after selection before releasing. 99.99% uptime for active sales.
Data Model
Event: event_id, name, venue_id, date, status (on_sale / sold_out / cancelled)
Seat: seat_id, event_id, section, row, number, tier (floor / premium / general), base_price
Seat status: seat_id, event_id, status (available / reserved / sold), reserved_by (user_id), reserved_until (timestamp), order_id
Order: order_id, user_id, event_id, seat_ids[], total_price, status (pending / confirmed / cancelled), payment_id
Seat status is stored separately from seat metadata to enable high-write throughput on status without locking the static seat data.
Seat Reservation with Optimistic Locking
When a user selects a seat, two approaches prevent double-booking:
Pessimistic locking (SELECT FOR UPDATE): acquire a row lock on the seat record. Only one transaction can hold the lock; others wait. Simple but poor scalability — lock contention during high-traffic sales causes queuing.
Optimistic locking (recommended): each seat has a version number. Reserve with a conditional update:
-- Attempt to reserve a seat (atomic, no explicit lock)
UPDATE seat_status
SET status = 'reserved',
reserved_by = :user_id,
reserved_until = NOW() + INTERVAL '10 minutes',
version = version + 1
WHERE seat_id = :seat_id
AND event_id = :event_id
AND status = 'available'
AND version = :expected_version;
-- Returns 0 rows updated if seat was taken concurrently
If 0 rows are updated, the seat was concurrently reserved by another user — return “seat no longer available, please select another.” Optimistic locking works well when contention is low (each specific seat has few simultaneous requests). For floor/general admission tickets where all units are equivalent, use Redis DECR on the inventory counter.
Reservation Expiry
Reserved seats are held for 10 minutes. A background job runs every 30 seconds and releases expired reservations:
UPDATE seat_status
SET status = 'available', reserved_by = NULL, reserved_until = NULL
WHERE status = 'reserved'
AND reserved_until < NOW();
Alternatively, store reservations in Redis with TTL and use Keyspace Notifications to trigger release on expiry. Redis approach: HSET reservation:{seat_id} user_id {uid} + EXPIRE reservation:{seat_id} 600. On TTL expiry, the Keyspace Notification triggers a consumer that marks the seat available in PostgreSQL.
Handling the Taylor Swift On-Sale (Flash Crowd)
3.5B requests/day for a single event. Standard web architectures collapse under this load. Solutions:
- Virtual waiting room: all users attempting to access the ticket purchase page are queued. They receive a position and estimated wait time. Queue is drained at a controlled rate (~5K checkout sessions/minute). Users behind a queue page do not hit the database. This is the primary tool — Ticketmaster implemented Queue-it.
- Static event page: the event details page (no seats, just event info) is served from CDN. Only the seat selection page requires dynamic responses.
- Read replicas: seat availability reads go to read replicas. Writes (reservations) go to the primary. Lag between primary and replicas means a seat may appear available on the read replica but already be reserved — handle this with the optimistic lock at write time.
- Inventory pre-loading: before the on-sale, load available seat inventory into Redis as a sorted set per event. Read availability from Redis (fast) rather than PostgreSQL. Asynchronously sync PostgreSQL from Redis on reservation.
Seat Map Real-Time Updates
The seat map must reflect reservation state in near-real time so users do not click on already-reserved seats. WebSocket or Server-Sent Events (SSE) stream seat status changes to all connected clients. Architecture: when a seat reservation is committed, publish a message to a Kafka topic (event_id → seat_id, new_status). A WebSocket server consumes Kafka and pushes updates to all clients viewing that event’s seat map. For events with 50K seats and 100K concurrent viewers, the fan-out is 50K changes × 100K WebSocket connections — segment into event-scoped rooms and use a pub/sub broker (Redis Pub/Sub or Pusher) to distribute across WebSocket server instances.
Waitlist
When an event sells out, users join a waitlist. Waitlist entries: (event_id, user_id, tier_preference, joined_at). When a reserved seat expires or an order is cancelled, the release triggers a notification to the next user in the waitlist (ordered by joined_at). The notification gives them a 15-minute window to purchase. If they do not purchase, move to the next user. Waitlist processing is done by a dedicated service consuming reservation expiry events from Kafka.
Dynamic Pricing
Ticketmaster uses dynamic pricing for high-demand events: prices rise as inventory sells. The pricing engine monitors real-time demand (reservation rate per minute) and adjusts prices for available inventory upward. Floor: base price. Ceiling: typically 200-400% of base. Price updates are applied to available seats every 5 minutes during an on-sale. Implementation: a separate pricing service watches the reservation stream, runs the pricing algorithm, and updates the price column in the seat table. Cached in Redis for read performance.
Frequently Asked Questions
How do you prevent two users from booking the same seat?
The safest approach is a conditional UPDATE with version-based optimistic locking. Each seat row has a version column. When a user attempts to reserve: UPDATE seat_status SET status='reserved', reserved_by=:user_id, version=version+1 WHERE seat_id=:id AND status='available' AND version=:expected_version. If 0 rows are updated, another transaction already reserved this seat (the version changed) — return "seat unavailable." If 1 row is updated, the reservation succeeded atomically. No explicit locking is needed, so concurrent reservations across different seats have no contention. For general admission tickets where any equivalent unit works (standing room, lawn tickets), use Redis DECR on a counter: DECR inventory:event:{id}. If the result is >= 0, the ticket is reserved. If < 0, issue INCR to restore and reject. Redis atomic decrements handle 100K+ operations per second without database load.
How do you handle a Taylor Swift ticket on-sale with millions of simultaneous users?
The primary tool is a virtual waiting room that decouples traffic arrival from ticket purchase processing. All users attempting to access the purchase page are assigned a position in a queue and shown an estimated wait time. The queue is drained at a rate the system can handle (e.g., 10,000 checkout sessions per minute). Users waiting do not hit the database — they see a static waiting room page served from CDN. This converts the thundering herd problem (1M users arriving simultaneously) into a controlled stream. Secondary techniques: serve the event details and artist information pages from CDN (no database reads). Pre-load seat inventory into Redis before the on-sale so availability reads come from Redis, not PostgreSQL. Use read replicas for seat map queries. Implement circuit breakers at the API gateway that reject requests when downstream services are overloaded, returning a "please try again" response rather than waiting for a timeout cascade.
How does a seat reservation expiry system work?
When a user selects seats, they are held for a time window (typically 8-15 minutes) to allow checkout. After this window, the seats are released back to available inventory. Two implementation approaches: (1) Database polling: a background job runs every 30 seconds with UPDATE seat_status SET status='available' WHERE status='reserved' AND reserved_until < NOW(). Simple, reliable, but adds database write load. (2) Redis TTL with keyspace notifications: store each reservation in Redis with an expiration (SET reservation:{seat_id} {user_id} EX 600). Enable keyspace notifications for expired events. When a key expires, Redis publishes an event to a channel. A consumer service listens to this channel and updates the seat status in PostgreSQL. The Redis approach is more responsive (triggers immediately on expiry) and reduces polling overhead. Both approaches must handle the race condition where a user completes checkout exactly as the reservation expires — use the optimistic lock to ensure the database update fails if the seat was already released.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you prevent two users from booking the same seat?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The safest approach is a conditional UPDATE with version-based optimistic locking. Each seat row has a version column. When a user attempts to reserve: UPDATE seat_status SET status=’reserved’, reserved_by=:user_id, version=version+1 WHERE seat_id=:id AND status=’available’ AND version=:expected_version. If 0 rows are updated, another transaction already reserved this seat (the version changed) — return “seat unavailable.” If 1 row is updated, the reservation succeeded atomically. No explicit locking is needed, so concurrent reservations across different seats have no contention. For general admission tickets where any equivalent unit works (standing room, lawn tickets), use Redis DECR on a counter: DECR inventory:event:{id}. If the result is >= 0, the ticket is reserved. If < 0, issue INCR to restore and reject. Redis atomic decrements handle 100K+ operations per second without database load."
}
},
{
"@type": "Question",
"name": "How do you handle a Taylor Swift ticket on-sale with millions of simultaneous users?",
"acceptedAnswer": {
"@type": "Answer",
"text": "The primary tool is a virtual waiting room that decouples traffic arrival from ticket purchase processing. All users attempting to access the purchase page are assigned a position in a queue and shown an estimated wait time. The queue is drained at a rate the system can handle (e.g., 10,000 checkout sessions per minute). Users waiting do not hit the database — they see a static waiting room page served from CDN. This converts the thundering herd problem (1M users arriving simultaneously) into a controlled stream. Secondary techniques: serve the event details and artist information pages from CDN (no database reads). Pre-load seat inventory into Redis before the on-sale so availability reads come from Redis, not PostgreSQL. Use read replicas for seat map queries. Implement circuit breakers at the API gateway that reject requests when downstream services are overloaded, returning a "please try again" response rather than waiting for a timeout cascade."
}
},
{
"@type": "Question",
"name": "How does a seat reservation expiry system work?",
"acceptedAnswer": {
"@type": "Answer",
"text": "When a user selects seats, they are held for a time window (typically 8-15 minutes) to allow checkout. After this window, the seats are released back to available inventory. Two implementation approaches: (1) Database polling: a background job runs every 30 seconds with UPDATE seat_status SET status='available' WHERE status='reserved' AND reserved_until < NOW(). Simple, reliable, but adds database write load. (2) Redis TTL with keyspace notifications: store each reservation in Redis with an expiration (SET reservation:{seat_id} {user_id} EX 600). Enable keyspace notifications for expired events. When a key expires, Redis publishes an event to a channel. A consumer service listens to this channel and updates the seat status in PostgreSQL. The Redis approach is more responsive (triggers immediately on expiry) and reduces polling overhead. Both approaches must handle the race condition where a user completes checkout exactly as the reservation expires — use the optimistic lock to ensure the database update fails if the seat was already released."
}
}
]
}