Search Tech Journey

Find topics, journeys and posts

back to blog
systemsadvanced 20m read

Designing for Scale · End-to-End Design Review II

A second live design review, tearing apart an event ticketing system. Handling massive, instantaneous write spikes, distributed transactions, and inventory locks without destroying the database.

The problem this post solves

Most system designs are tested against smooth, predictable traffic curves. Peak load is modelled as a gentle hill in the evening.

An event ticketing system (or a flash sale, or a sneaker drop) does not have a traffic curve. It has a wall. When a popular concert goes on sale at 10:00 AM, the system goes from zero traffic to its absolute maximum capacity in one second.

This post models an end-to-end design review for a flash-sale ticketing system. It exposes why standard web architectures shatter against a vertical traffic wall, and how to use aggressive caching, queuing, and pessimistic locking to survive it.

The Prompt

"Design a ticketing system that allows users to view events and purchase tickets. It must handle massive spikes for popular concerts."

Bounding the problem

The Read Path (Viewing Events):

  • Users browsing events, viewing seating charts, and checking availability.
  • High volume, highly cacheable.
  • The exact number of available tickets can be eventually consistent (slightly stale) for browsers, as long as it is strictly consistent at checkout.

The Write Path (Purchasing):

  • A 100,000-seat stadium goes on sale.
  • 5 million users attempt to buy tickets simultaneously at 10:00:00 AM.
  • The system must sell exactly 100,000 tickets. It cannot sell 100,001.
Mental modelThe Thundering Herd
A stadium with a thousand doors, but all five million people are trying to squeeze through the single door labeled 'Checkout' at the exact same millisecond.
  • You cannot scale the database fast enough to handle the spike.
  • You must drop traffic before it hits the database.
  • The architecture's primary job is a funnel: aggressively narrow the pipe at every layer.
🔔 Fires when you see
Use whenever a system features a scheduled event, a flash sale, or a limited-inventory drop.

Architecture: Surviving the Spike

If 5 million users click "Buy" and hit the API server, and the API server forwards 5 million requests to the database, the database will lock up, time out, and crash.

We must protect the database at all costs.

1. The Edge (Static Content)

The seating charts, artist photos, and concert dates are static. They are pushed to the CDN. When the 5 million users load the page at 9:59 AM, the backend servers see almost zero traffic. The CDN absorbs it all.

2. The Redis Funnel (Inventory Cache)

At 10:00 AM, 5 million users click "Buy". The API servers receive the requests.

They do not talk to the database. They talk to a Redis cluster holding the remaining inventory. Using a Lua script (as discussed in the Rate Limiter post), Redis checks if inventory > 0. If yes, it decrements the inventory and returns success to the API server. If no, it returns failure.

Redis can handle hundreds of thousands of operations per second.

  • 100,000 requests get a "Yes".
  • 4.9 million requests get a "No" (Sold Out).

We have just dropped 4.9 million requests in milliseconds, completely bypassing the database.

3. The Queue

The 100,000 users who got a "Yes" from Redis are placed into a Checkout Queue. The API server returns a response: "You are in line. Please wait."

The users are now safely waiting. The system can process them at its own pace.

4. The Database (The Source of Truth)

A fleet of background workers pulls users from the Checkout Queue.

The worker opens a transaction in the relational database. It attempts to reserve the specific seats for the user. Because this is a financial transaction with strict inventory limits, the database must use Pessimistic Locking (e.g., SELECT ... FOR UPDATE). This locks the rows representing those specific tickets so no other worker can touch them until the transaction completes.

The worker processes the payment. If successful, it finalizes the ticket assignment. If the payment fails (or the user times out), the worker releases the database lock, and increments the inventory back up in Redis, allowing someone else to buy the ticket.

The tradeoff
Optimistic vs Pessimistic Locking for Inventory
Optimistic Locking
+ you gain Fast. No database locks held. Uses a version number (e.g., `UPDATE ... WHERE version = 1`).
− you pay Fails violently under high contention. If 1,000 people try to buy the same ticket, 1 succeeds and 999 transactions fail and must be retried by the application, causing massive CPU thrashing.
pick when For low-contention systems (e.g., editing a wiki page).
Pessimistic Locking
+ you gain Safe and predictable. The database forces transactions to wait in line (`SELECT FOR UPDATE`). No application-level thrashing.
− you pay Slow. Holds database connections open.
pick when Mandatory for high-contention, strictly limited inventory (tickets, seats, flash sales).

Tracing the Constraints

Reviewer Question: "Your Redis cache said there were tickets available, but when the worker tried to process the database transaction, the seats were already taken. How do you handle this state mismatch?" Answer: This is a classic distributed systems problem. Redis is an approximation; the relational database is the truth. The Redis inventory might be slightly off (e.g., a worker crashed before restoring inventory after a failed payment). If a user gets through Redis but the database rejects the reservation, the worker must return an error to the user ("Sorry, those seats were just taken"). The architecture accepts that a tiny fraction of users might see a false positive at the funnel layer, because the alternative (hitting the DB for every request) destroys the system for everyone.

Reviewer Question: "What happens if a user is placed in the queue, but they close their browser and walk away before the payment processes?" Answer: The system must implement a strict Reservation Timeout (e.g., 5 minutes). When the worker reserves the ticket in the database, it records a locked_until timestamp. A separate cleanup job constantly scans for expired locks, releases the tickets, and pushes the available inventory back up to Redis.

What to carry forward

Key points
    You can now
    • Architect a system that survives a sudden 100x traffic spike.
    • Design a multi-layered funnel using CDN caching, Redis counters, and message queues.
    • Explain why optimistic locking fails catastrophically under high contention.
    • Handle the edge cases of distributed inventory, including false positives and reservation timeouts.