View Categories

Designing Ticketmaster – The “Concurrency” Logic

2 min read

The “Double Booking” Trap #

The nightmare scenario for any booking platform is selling the same seat to two different people. A Junior Engineer writes the code like this:

// The "Naive" Logic
seat = db.getSeat(123);
if (seat.isAvailable) {
    db.chargeUser();
    seat.isAvailable = false;
    db.save(seat);
}

The Logic Check: This code contains a classic Race Condition (specifically, a “Check-Then-Act” race). If User A and User B click “Buy” at the exact same millisecond:

  1. User A reads DB: Seat is Available.
  2. User B reads DB: Seat is Available.
  3. User A writes “Sold”.
  4. User B writes “Sold” (overwriting User A). Result: Both users are charged. Both get a confirmation email. Only one seat exists. You now have a lawsuit.

The Core Logic: Pessimistic vs. Optimistic Locking #

To solve this, you must rely on Database Consistency, not Application Logic. The application server cannot be trusted because there are many of them (scaled out). The Database is the single source of truth.

You have two mathematical ways to handle this:

1. Pessimistic Locking (The “Taylor Swift” Mode)

  • Logic: Assume conflict is likely.
  • Mechanism: When User A reads the seat status, the database Locks that row. User B cannot even read the seat status until User A finishes their transaction.
  • SQL: SELECT * FROM seats WHERE id=123 FOR UPDATE;
  • Pros: Guarantees zero double bookings.
  • Cons: Kills performance. It serializes traffic. If User A has a slow internet connection, User B is waiting on a spinning loader.

2. Optimistic Locking (The “Airline” Mode)

  • Logic: Assume conflict is rare.
  • Mechanism: Add a version column to the row.
  • SQL: UPDATE seats SET status='Sold', version=2 WHERE id=123 AND version=1;
  • The Trick: If User B tries to update, the database sees the version is now 2, not 1. The WHERE clause fails. The database returns “0 rows affected.” User B gets an error message: “Sorry, this seat was just taken.”
  • Pros: High performance (no waiting).
  • Cons: Annoying UX for high-demand events (users keep clicking buy and failing).

Architecture Diagram: The “Hold” Pattern #

Ticketmaster actually uses a third approach: The Temporary Reservation. They don’t lock the row for the purchase; they lock it for the cart.

sequenceDiagram
    participant UserA
    participant Redis
    participant DB
    
    UserA->>Redis: 1. "Reserve Seat 123" (SETNX key:123 user:A)
    alt Reservation Success
        Redis-->>UserA: OK (TTL: 5 mins)
        UserA->>UserA: 2. Enter Payment Details
        UserA->>DB: 3. Finalize Purchase
        DB-->>UserA: Ticket Confirmed
        UserA->>Redis: 4. Delete Reservation Key
    else Seat Taken
        Redis-->>UserA: Error: Locked by another user
    end

The Decision Matrix: Choosing Your Lock #

ScenarioLogic ChoiceWhy?
High Demand (Concert)Redis Locking (Cart Hold)You need to reserve the seat before payment. Using a database lock for 5 minutes (while user types credit card info) would crash the DB. Redis SETNX (Set if Not Exists) is atomic and fast.
Low Demand (Cinema)Optimistic LockingConflicts are rare. It’s better to let 1,000 users browse freely and handle the 1 collision with an error message than to slow everyone down.
Financial BalancePessimistic LockingWhen transferring money from Account A to B, you absolutely must lock Account A so they can’t withdraw twice. Speed doesn’t matter; correctness is everything.

Real-World Case Study: The “Waiting Room” #

When Ticketmaster has 10 million people trying to buy 50,000 tickets, no database in the world can handle the write throughput (even with Locking). The Logic: They introduce a Queue Layer (The Waiting Room).

  1. Gatekeeper: Users don’t hit the Booking API. They hit a Queue (WebSockets).
  2. Throttling: The system only lets 500 users per minute into the actual “Booking Flow.”
  3. Result: The Booking Database only ever sees 500 concurrent users, which Postgres can handle easily using standard Locking.
  • The lesson: If you can’t scale the database writes, throttle the traffic before it gets there.

Conclusion #

Concurrency is not solved by “faster code.” It is solved by serializing access to shared resources.

  • If the user needs time to think/pay: Use a Redis TTL Lock.
  • If the transaction is instant: Use Optimistic Locking (Version check).
  • Never trust the application state. Trust the Database constraints.