SkillByAIOpen interactive version →

Lesson 10 / 25

Seat Booking With Concurrency

Holding and confirming seats safely.

Hold, pay, confirm

In a movie or event booking system (a library system has a similar shape with copies instead of seats), the core risk is double booking when two users pick the same seat. Model Show -> Seat with status AVAILABLE, HELD or BOOKED, and a Booking for a user. Flow: the user selects seats, the service places a temporary hold with an expiry (for example, an assumed 5-10 minutes), the user pays, and the booking is confirmed. Holds must be atomic across all requested seats: either all are held or none. In a single process, lock per show; in a database, use a conditional update (UPDATE ... WHERE status = 'AVAILABLE') or row locks, and release expired holds with a background sweep or on read.

Concurrency and data-structure designs

These problems add concurrency and algorithmic design to object modelling.

Figure 4.1 — Seat holds, LRU structure and token bucket.

All-or-nothing hold

Java sketch with a lock per show.

enum SeatStatus { AVAILABLE, HELD, BOOKED }

class Seat {
    final String id; SeatStatus status = SeatStatus.AVAILABLE;
    String heldBy; java.time.Instant holdExpiry;
    Seat(String id) { this.id = id; }
}

class Show {
    private final java.util.Map<String, Seat> seats = new java.util.HashMap<>();
    private final java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock();

    boolean hold(java.util.List<String> seatIds, String userId, java.time.Duration ttl) {
        lock.lock();
        try {
            java.time.Instant now = java.time.Instant.now();
            for (String id : seatIds) {
                Seat s = seats.get(id);
                boolean expired = s.status == SeatStatus.HELD && s.holdExpiry.isBefore(now);
                if (s.status != SeatStatus.AVAILABLE && !expired) return false;  // nothing changed yet
            }
            for (String id : seatIds) {
                Seat s = seats.get(id);
                s.status = SeatStatus.HELD; s.heldBy = userId; s.holdExpiry = now.plus(ttl);
            }
            return true;
        } finally {
            lock.unlock();
        }
    }
}

// Database equivalent per seat (all inside one transaction):
// UPDATE seat SET status = 'HELD', held_by = ?, hold_expiry = ?
//  WHERE show_id = ? AND seat_id = ? AND (status = 'AVAILABLE' OR hold_expiry < now())

Check then act inside the lock

Checking availability and updating status must happen in the same critical section or transaction; otherwise two users can both see a seat as free.

Quick check: Why use a temporary hold with an expiry?

  • So seats are reserved during payment but released if the user abandons
  • To make payment optional
  • To avoid needing a database
  • To let two users share a seat
Answer

So seats are reserved during payment but released if the user abandons — Holds protect the payment window without locking seats forever.