Case studies
Case study: ticket booking
Ticketmaster-style inventory under extreme contention, with holds that expire, a virtual waiting room and atomic seat commits so one seat is never sold twice.
A ticket platform looks like ordinary e-commerce until a popular tour goes on sale. Tens of thousands of seats meet millions of fans in the same minute. Browsing can be cached and slightly stale. Selling a seat cannot be. Seat 14C must belong to at most one buyer, even when the database is under a stampede, payments time out and bots hammer the API.
The design rests on two paths with different rules. The browse path (event pages, seat maps) is read-heavy and can be eventually consistent. The buy path (hold, pay, book) must be strongly consistent. Most of the design is about keeping the buy path small, fair and safe.
Requirements
- Browse events, venues and seat maps.
- Hold chosen seats for about 10 minutes while the user pays.
- Finish with a paid ticket, or release the hold on timeout, cancel or payment failure.
- Never double-book a seat, and stay fair to early arrivals.
The numbers
Demand / supply ≈ 2,000,000 people / 50,000 seats = 40 claimants per seat
If everyone polls the seat map once a second: 2M QPS of reads
A booking backend might handle roughly 5,000 to 20,000 holds/s
→ Admission must match that capacity, not the raw crowdReads dominate until a waiting room exists. Writes are fewer but hot, since every hold touches the same event's rows. Partition by event_id, so one hot show does not stall unrelated shows.
Concepts in this topic
| Step | Concept | What it answers |
|---|---|---|
| 1 | Check-then-act races | Why does reading a seat and then writing it lose seats? |
| 2 | Timed holds and lazy expiry | How does a claim expire without a cleanup job? |
| 3 | Virtual waiting room | How do you let in a crowd at a rate the system can handle? |
| 4 | Authorize, commit, capture | In what order do you charge a card and claim a seat? |
High-level design
Catalog and seat maps are cacheable. The hold service is the contention gate: only admitted clients reach it, and it claims seats atomically with a timeout. Checkout turns a valid hold into a sale once payment is authorized.
Read path
| Data | Staleness OK? | Strategy |
|---|---|---|
| Event details, venue | Minutes | CDN and Redis with a long TTL |
| Seat colours | Seconds | Short TTL, or push updates |
| "Your hold" | No | Always from the hold service |
A seat that shows green for a few seconds after someone else held it is acceptable, as long as the hold API still refuses the claim. Showing a sold seat as bookable is not.
Multi-seat holds
A party of four needs four seats together. Partial success leaves orphaned seats. Claim all seats in one statement and require the row count to match. On any mismatch, roll back the whole hold and ask the client to refresh the map.
Bots and fairness
- Only holders of a signed admission token reach the hold path.
- Rate-limit holds per user, device and IP.
- Watch for anomalies, such as a whole front row grabbed in 50 ms.
Fairness is a product policy. Engineering can enforce it only if tokens cannot be forged and holds expire.
Failure modes
| Failure | Mitigation |
|---|---|
| Hold service down | Fail closed on writes; keep browsing up from cache |
| Redis flushed | The database hold_until is the source of truth; clients re-hold |
| Payment result unknown | Idempotent payment intents and reconciliation |
| Over-admission | Feedback control on the waiting room rate |
Common mistakes
- Locking rows while a payment call is in flight. Waiters pile up behind a slow provider.
- Charging before the seat is committed. A buyer who loses the seat then needs a refund.
- Treating the cache as the record of sold seats. Only the strongly consistent path decides ownership.
- Adding bot defenses after launch. They are much harder to add mid-sale.
Key takeaways
- Unique seats need a strong commit path. Caches serve maps, not ownership.
- Holds expire on their own. Check expiry at claim time.
- A conditional update and a unique constraint on sold seats prevent double-booking.
- A virtual waiting room matches demand to what the backend can sustain.
- Payment is a saga: authorize, commit the seat, then capture.
Further learning
A full Ticketmaster-style interview walkthrough with an ex-Meta staff engineer. It shows how the waiting room and hold API get drawn under time pressure.
Related
Transactions, MVCC and isolation levels
What ACID guarantees on one database node, which concurrency bugs each isolation level still allows, how MVCC gives snapshots without blocking, and the code patterns that keep concurrent writes correct. Start here for transactions.
Distributed transactions
Keeping data consistent when one business operation spans several databases or services, using two-phase commit, sagas, the transactional outbox and reservations, and redrawing boundaries first.
Case study: payment system
Moving money correctly, with payment intents, idempotency at every hop, a double-entry ledger, webhooks and reconciliation when the network lies. Start here for the payments map.
Check-then-act races
Why reading a seat's status and then writing it back can sell the same seat twice, and how one conditional UPDATE turns the check and the claim into a single atomic step.
Timed holds and lazy expiry
A hold keeps seats for about ten minutes while a buyer pays. Storing the expiry time on each row lets seats free themselves at claim time, without a cleanup job.
Virtual waiting room
A waiting room admits buyers at a rate the booking system can handle. Signed, short-lived tokens keep bots and forged requests out of the hold path.
Authorize, commit, capture
The order of checkout steps decides whether a buyer can be charged for a seat they lost. Authorize the card, commit the seat, then capture the money, and undo each step on failure.