Theater/Screen/Show/Seat/Booking classes, temporary seat holds with expiry vs immediate commit, and preventing the double-booking race condition.
Published September 23, 2026
class Theater { List<Screen> screens; }
class Screen { String id; List<Seat> seats; }
class Seat { String id; SeatType type; }
class Show { Screen screen; Movie movie; Instant startTime; Map<String, SeatStatus> seatStatuses; } // per-show, not per-seat globally
class Booking { Show show; List<Seat> seats; User user; BookingStatus status; Instant expiresAt; }
Seat availability is tracked per Show, not as a permanent property of Seat — the same physical seat is available for a 2pm show and separately bookable for a 6pm show on the same screen. This is the detail that trips people up first: Seat itself has no booking state at all; Show.seatStatuses does.
enum SeatStatus { AVAILABLE, HELD, BOOKED }
class BookingService {
Optional<Booking> holdSeats(Show show, List<String> seatIds, User user) {
synchronized (show) { // see below for why per-show locking, not a global lock
for (String id : seatIds) {
if (show.getSeatStatus(id) != SeatStatus.AVAILABLE) return Optional.empty();
}
seatIds.forEach(id -> show.setSeatStatus(id, SeatStatus.HELD));
Booking booking = new Booking(show, seatIds, user, BookingStatus.PENDING, Instant.now().plus(Duration.ofMinutes(10)));
scheduleExpiry(booking); // if not confirmed within 10 minutes, seats auto-release back to AVAILABLE
return Optional.of(booking);
}
}
boolean confirmBooking(Booking booking) {
if (Instant.now().isAfter(booking.getExpiresAt())) return false; // hold already expired
booking.getSeats().forEach(seat -> booking.getShow().setSeatStatus(seat, SeatStatus.BOOKED));
booking.setStatus(BookingStatus.CONFIRMED);
return true;
}
}
Going straight to BOOKED on seat selection (immediate commit) would let a user select seats, abandon the checkout flow (browser closed, payment never completed), and permanently lock those seats away from every other user. A temporary HELD state with expiry — the seat is reserved just long enough to complete payment, then automatically released if the flow doesn't finish — is the standard fix, directly analogous to Parking Lot's spot-claiming problem but with a time-bounded hold instead of a permanent claim.
Two users selecting the same seat for the same show simultaneously is the exact same check-then-act race covered throughout this course (HashMap Concurrency Variants, Parking Lot — Implementation). The synchronized (show) block above locks per show, not globally across the whole theater chain — two users booking seats for different shows never contend with each other, only users targeting the same show's seat map do. At real scale, this per-show lock would typically be replaced by a database-level constraint (a unique index on (show_id, seat_id, status=BOOKED), or the optimistic-locking @Version pattern from Locking Strategies) rather than an in-process lock, since booking requests for a popular show likely arrive across multiple application server instances, not one process holding one JVM monitor.
Q: How would you scale seat-hold expiry across multiple application instances, given scheduleExpiry() here implies a single-process timer? A: A single in-process scheduled task doesn't survive that instance restarting or scale across multiple instances — production systems typically use a durable, distributed mechanism instead (a delayed job in a queue, or a database row with an expires_at column that a periodic sweep job checks), rather than relying on an in-memory timer tied to one process's lifetime.
Q: What happens if payment succeeds but confirmBooking() is called after the hold already expired? A: This is exactly the failure scenario worth designing for explicitly: the seats may have already been released and rebooked by someone else — the system needs to detect this (confirmBooking returning false as shown) and trigger a refund/retry flow, rather than silently confirming a booking for seats that are no longer actually held.
Q: Why lock per-show rather than per-seat, given seats are the actual contended resource? A: Per-seat locking would technically be finer-grained and allow slightly more parallelism, but checking availability for MULTIPLE seats atomically (a user typically selects several seats in one booking) requires holding all their locks together anyway to avoid a partial-success race — per-show locking is simpler to reason about correctly for this multi-seat-atomicity requirement, at the cost of some contention between users booking different seats on the same popular show.