Skip to main content

Movie Ticket Booking

Two people staring at the same seat map, both tap seat B12 at the same instant, and only one of them should walk away with a ticket. That race is the entire reason this problem shows up in interviews - everything else is bookkeeping around it.

Requirements

Functional

  • A Theater has Shows (a movie at a specific time in a specific screen); each show has a seat map inherited from its screen's layout.
  • A customer selects seats for a show and books them; a booked seat cannot be selected by anyone else.
  • Seats have categories (regular, premium) and each category has its own price for a show.
  • A booking can be cancelled, which frees its seats.

Non-functional

  • Two customers selecting the same seat at nearly the same time must never both succeed - exactly one booking should win.
  • A seat a customer is actively choosing (but hasn't paid for yet) shouldn't be immediately grabbable by someone else, but it also shouldn't be locked forever if they abandon checkout.

Design

Every Seat on a Show carries its own state - AVAILABLE, LOCKED, or BOOKED - and every transition between those states goes through one method, Seat.tryLock / Seat.confirm / Seat.release. Show never flips a seat's state directly; it only ever asks the seat to do it, which is what makes "two requests, one winner" enforceable in one place instead of scattered across every caller.

AliceBobShowSeat B12selectSeats([B12])1tryLock()2selectSeats([B12])3tryLock()4confirmBooking()5
  1. 1Alice and Bob both request B12 within the same instant.
  2. 2Alice's request reaches the seat's atomic lock first and flips AVAILABLE to LOCKED.
  3. 3Bob's request arrives a moment later.
  4. 4The seat is already LOCKED, so Bob's tryLock returns false - his booking attempt fails cleanly.
  5. 5Alice completes payment before any timeout; her lock becomes a permanent BOOKED.

Booking is created only after every requested seat has been locked successfully - if even one seat in the request is already taken, the whole booking attempt fails and any seats it did manage to lock are released, so a customer never ends up holding three of the four seats they asked for.

Class diagram

Theater- screens: List<Screen>+ showsOn(date): List<Show>Movie- id: string- title: string- durationMinutes: intShow- movie: Movie- seats: List<Seat>- priceByCategory: Map<SeatCategory, double>+ bookSeats(seatIds): Booking+ cancel(booking)Seat- id: string- category: SeatCategory- state: SeatState+ tryLock(): bool+ confirm()+ release()Booking- show: Show- seats: List<Seat>- totalPrice: double
usescreates
Show owns Seats; each Seat guards its own AVAILABLE/LOCKED/BOOKED state; Booking is only created once every requested seat locks.

Code

import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
 
enum SeatCategory { REGULAR, PREMIUM }
enum SeatState { AVAILABLE, LOCKED, BOOKED }
 
class Movie {
final String id;
final String title;
 
Movie(String id, String title) {
this.id = id;
this.title = title;
}
}
 
class Seat {
final String id;
final SeatCategory category;
private SeatState state = SeatState.AVAILABLE;
private final ReentrantLock guard = new ReentrantLock();
 
Seat(String id, SeatCategory category) {
this.id = id;
this.category = category;
}
 
boolean tryLock() {
guard.lock();
try {
if (state != SeatState.AVAILABLE) return false;
state = SeatState.LOCKED;
return true;
} finally {
guard.unlock();
}
}
 
void confirm() {
guard.lock();
try {
state = SeatState.BOOKED;
} finally {
guard.unlock();
}
}
 
void release() {
guard.lock();
try {
state = SeatState.AVAILABLE;
} finally {
guard.unlock();
}
}
}
 
class Booking {
final List<Seat> seats;
final double totalPrice;
 
Booking(List<Seat> seats, double totalPrice) {
this.seats = seats;
this.totalPrice = totalPrice;
}
}
 
class Show {
final Movie movie;
private final Map<String, Seat> seatsById;
private final Map<SeatCategory, Double> priceByCategory;
 
Show(Movie movie, List<Seat> seats, Map<SeatCategory, Double> priceByCategory) {
this.movie = movie;
this.seatsById = new HashMap<>();
for (Seat s : seats) seatsById.put(s.id, s);
this.priceByCategory = priceByCategory;
}
 
Booking bookSeats(List<String> seatIds) {
List<Seat> locked = new ArrayList<>();
for (String id : seatIds) {
Seat seat = seatsById.get(id);
if (seat == null || !seat.tryLock()) {
locked.forEach(Seat::release);
throw new IllegalStateException("Seat " + id + " unavailable");
}
locked.add(seat);
}
double total = locked.stream().mapToDouble(s -> priceByCategory.get(s.category)).sum();
locked.forEach(Seat::confirm);
return new Booking(locked, total);
}
 
void cancel(Booking booking) {
booking.seats.forEach(Seat::release);
}
}

Design decisions

  • Seats get a three-state machine (AVAILABLE / LOCKED / BOOKED) instead of a boolean isBooked. A boolean can't represent "someone is mid-checkout on this seat right now" - without the middle state, two customers could both see a seat as free during the few seconds between selection and payment. The extra state is what a real booking flow actually needs, not gold-plating.
  • Seat.tryLock is a compare-and-set, not a check-then-set. if (seat.isAvailable()) seat.lock() from two threads can both pass the check before either calls lock - the classic time-of-check-to-time-of-use race. tryLock does the check and the state flip as one atomic step (a synchronized method in Java, a lock-guarded compare in Python), so only one caller ever sees true come back.
  • Locking is per-seat, not per-show. A single lock around the whole show would correctly prevent double-booking, but it would also serialize every customer browsing that show, even ones picking completely different seats. Locking at the seat level is the difference between "correct" and "correct and doesn't fall over during a popular release's on-sale minute."
  • What's missing for a real system: a locked seat here has no expiry - a production system needs a TTL (a scheduled job or a lock timestamp checked on read) that reverts LOCKED back to AVAILABLE if the customer never completes payment, otherwise an abandoned checkout permanently removes a seat from sale.
0%0 of 122 pages studied