Understanding the Problem
🔗 What is a movie ticket booking system?
A platform that lets users search movies, select showtimes across multiple theaters, pick individual seats, and reserve them atomically while handling concurrent booking conflicts.
You're designing the class layer of a movie booking system — the kind that powers BookMyShow or Fandango. The headline is atomic multi-seat reservations: when two users try to book the same seat concurrently, exactly one succeeds. Your job is to define the entities, lock strategy, and state transitions cleanly so that the interviewer sees you understand concurrency and encapsulation.
Requirements
In scope
- Multi-theater, multi-screen architecture. Each theater has multiple screens; all screens in a theater have the same seat layout (rows A–Z, seats 0–20 = 546 seats).
- Movie search by title; showtime selection (movie + screen + date/time).
- Atomic multi-seat reservation: user picks 1–4 seats, all claim succeeds or all fail. No partial bookings.
- Concurrent conflict resolution: if two threads pick the same seat, exactly one gets it; the other throws an exception.
- Cancellation releases seats immediately for other users.
- Confirmation IDs for audit trails.
Out of scope
- Payment processing, PCI compliance.
- Variable seat pricing or types (premium, accessible).
- Reservation auto-expiry / TTL (PENDING → auto-release after timeout).
- Notifications, emails, or UI rendering.
- Rescheduling or seat exchanges.
State your assumptions: you're building the BookingSystem orchestrator and supporting entity classes. Lock scope and synchronization are your main design decision.
The Set Up
Entities & Relationships
- BookingSystem (orchestrator): searches movies and theaters; initiates reservations. Owns a collection of theaters.
- Theater: container for multiple screens with identical seat layouts. Queries showtime availability.
- Screen: owns a 2D array of seats (rows A–Z, cols 0–20). Manages seat state transitions and atomically claims multi-seat reservations. This is the concurrency hot zone.
- Showtime: links a movie, screen, and temporal slot (date + time). Immutable once created.
- Seat: individual booking unit with state (AVAILABLE | RESERVED) and a nullable reservation ID. Owned by Screen.
- Reservation: transaction record with a UUID, status (PENDING | CONFIRMED | CANCELLED), list of booked seats, and showtime reference. Returned to caller on success.
Dependency sketch: BookingSystem → Theater → Showtime → Screen → Seats. Reservation is generated by BookingSystem and returned to the caller. Screen owns the seat-locking invariants.
Class Design
enum SeatState { AVAILABLE, RESERVED }
enum ReservationStatus { PENDING, CONFIRMED, CANCELLED }
class Seat:
row: char // A-Z
number: int // 0-20
state: SeatState
reservedBy: String? // Reservation ID (null if AVAILABLE)
class Showtime:
id: String
movie: Movie
screen: Screen
dateTime: DateTime
getAvailableSeats() -> List<Seat> // read-only view of available seats
class Screen(rows=26, cols=21):
seats: Seat[rows][cols]
synchronized reserveSeats(seatList: List<Seat>, reservationId: String) -> void:
// Atomically claim all seats or fail with exception
// Throws ReservationConflictException if any seat is unavailable
synchronized releaseSeats(reservationId: String) -> void:
// Clear the reservedBy flag; mark seats AVAILABLE
class Reservation:
id: String // UUID
showtime: Showtime
seats: List<Seat>
status: ReservationStatus // PENDING, CONFIRMED, CANCELLED
createdAt: DateTime
getConfirmationId() -> String
class Theater:
id: String
name: String
screens: Map<String, Screen>
searchShowtimes(movieTitle: String, date: Date) -> List<Showtime>
class BookingSystem:
theaters: Map<String, Theater>
searchMovies(title: String) -> List<Movie>
searchShowtimes(movie: Movie, date: Date) -> List<Showtime>
reserveSeats(showtime: Showtime, seatList: List<Seat>) -> Reservation:
// Throws ReservationConflictException if conflict detected
cancelReservation(reservationId: String) -> void:
// Throws ReservationNotFoundException if not found
Implementation
The meaty method is Screen.reserveSeats — this is where you guarantee atomicity under concurrent load.
Screen.reserveSeats(seatList: List<Seat>, reservationId: String) -> void:
synchronized(this): // Lock the entire Screen object
// Phase 1: Validate all seats are AVAILABLE
for seat in seatList:
if seat.state != AVAILABLE:
throw ReservationConflictException(
"Seat " + seat.row + seat.number + " already reserved"
)
// Phase 2: Claim all seats atomically (no context switch possible)
for seat in seatList:
seat.state = RESERVED
seat.reservedBy = reservationId
Screen.releaseSeats(reservationId: String) -> void:
synchronized(this):
for each seat in seats array:
if seat.reservedBy == reservationId:
seat.state = AVAILABLE
seat.reservedBy = null
BookingSystem.reserveSeats(showtime: Showtime, seatList: List<Seat>) -> Reservation:
// Validate all seats belong to the showtime's screen (fail fast)
for seat in seatList:
if seat not in showtime.screen.seats:
throw InvalidSeatException("Seat not in this showtime's screen")
reservation = new Reservation(
id: UUID.randomUUID(),
showtime: showtime,
seats: seatList,
status: PENDING,
createdAt: now()
)
// This call may throw ReservationConflictException
showtime.screen.reserveSeats(seatList, reservation.id)
return reservation
Trace through a concurrent scenario:
Showtime: Movie X, 2pm, Screen 1.
Seats to book: [A1, A2] (Thread 1) and [A1, A3] (Thread 2).
Time Thread 1 Thread 2
---- --------- ---------
T0 reserveSeats([A1, A2]) reserveSeats([A1, A3])
T1 acquire lock on Screen 1 waiting for lock
T2 Phase 1: check A1.state (blocked)
T3 A1 == AVAILABLE ✓
T4 check A2.state
T5 A2 == AVAILABLE ✓
T6 Phase 2: claim A1 → RESERVED
T7 claim A2 → RESERVED
T8 release lock acquire lock
T9 return Reservation(id=U1, Phase 1: check A1.state
status=PENDING)
T10 A1 == RESERVED ✗
T11 throw ReservationConflictException("A1 already reserved")
T12 release lock
Result: Thread 1 succeeds with seats [A1, A2]. Thread 2 fails immediately on A1 (before claiming A3). No partial bookings. The lock ensured both validation and claiming happened atomically.
Extensibility
Likely follow-ups in the interview:
-
Reservation auto-expiry (TTL): Add
expiresAt: DateTimeto Reservation and a backgroundReservationExpirerthread that polls PENDING reservations. WhencreatedAt + TTL < now, callcancelReservation(id)to release seats. Seat state transitions: AVAILABLE → RESERVED (pending) → AVAILABLE (expired) or CONFIRMED (user paid). -
Seat pricing by type: Add
type: enum SeatType { STANDARD, PREMIUM, ACCESSIBLE }to Seat. Reservation trackspricePerSeat: Map<Seat, BigDecimal>. DuringreserveSeats, calculate total from seat type → price mapping. Return Reservation with totalPrice so caller can review before confirm. -
Group discounts: Add
discountCode: String?to Reservation. Duringconfirm, if code is set, BookingSystem applies a multiplier to totalPrice. No changes to seat-locking logic. -
Multi-screen reservation (rare): If a user tries to book seats across two different screens, decompose into two separate Screen.reserveSeats calls. If the second fails, rollback the first via releaseSeats. This is a distributed transaction — simple two-phase commit.
What is Expected at Each Level?
Mid-level
- Identifies the core entities: BookingSystem, Theater, Screen, Seat, Showtime, Reservation.
- Recognizes that Screen is the concurrency hot zone and implements synchronized methods.
- Writes the happy path for multi-seat reservation without race conditions (validation + claiming in one lock).
- Handles the basic case: two threads, same seat, one wins.
Senior
- Designs the Reservation return type as an enum-backed status (PENDING, CONFIRMED, CANCELLED), not a bare boolean.
- Explains why synchronizing on Screen (coarse-grained lock) is simpler and safer than per-Seat locks.
- Traces through the concurrent scenario clearly, showing how the lock prevents partial bookings.
- Mentions the cost: high contention during peak hours (movie release day, blockbuster showtime).
- Anticipates one extensibility follow-up cleanly (e.g., TTL or pricing).
Staff+
- Pushes back on vague requirements: "What happens if a user reserves seats from two different screens in one transaction? Do you fail atomically, or accept partial?"
- Names the pattern: "This is a critical section problem. I'm using a monitor (synchronized block) to ensure mutual exclusion."
- Speaks to testability: "I'd unit-test the happy path, the conflict case, and the rollback-on-cancel. I'd integration-test with a thread pool simulating 100 concurrent users."
- Discusses failure modes: What if the JVM crashes after we claim seats but before we return the Reservation? (Suggests durable logging: write to a reservation log before returning.)
- Considers observability: How do you monitor lock contention? Warn ops when reserveSeats latency spikes during peak hours?