What you’re designing
Movies by title
Theater schedule
Pick A5, A6…
Exactly one books
Analogy: theater seat chart with a clipboard
Think of an usher with a seat chart. Instant book = pencil the seats in immediately. The hold extension = light pencil for five minutes, ink when payment clears.
Clarifying questions → locked requirements
- Search? → Simple case-insensitive title substring — no Elasticsearch.
- Seats? → User picks specific seats; multiple seats per reservation.
- Scale of venues? → Multiple theaters, multiple screens.
- Layouts? → Standardize: rows A–Z, seats 0–20 on every screen.
- Manage reservations? → Cancel only (rebook for changes).
- Pricing / payment? → Out of scope; payment always succeeds.
- Concurrency? → Same seat, two bookers → exactly one succeeds.
Entities: what becomes a class
- BookingSystem — orchestrator: theaters, search, browse, book, cancel.
- Movie — id + title; searchable glue across showtimes.
- Theater — named location; owns showtimes list.
- Showtime — bookable unit; tracks reservations (= seat state) + concurrency.
- Reservation — confirmationId, seatIds, back-ref to Showtime.
- Seat — string like
"A5", not a class. - Screen — string label on Showtime (
"Screen 3"), not a class.
Class design
# Constants
SEAT_LAYOUT = [f"{row}{n}" for row in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" for n in range(21)]
# 26 × 21 = 546 seats per showtime
class BookingSystem:
# theaters + indexes (built at construction / updated on add)
+ search_movies(title) -> list[Showtime]
+ get_showtimes_at_theater(theater) -> list[Showtime]
+ book(showtime_id, seat_ids) -> Reservation
+ cancel_reservation(confirmation_id) -> None
class Theater:
- id, name, showtimes: list[Showtime]
+ get_showtimes() / get_showtimes_for_movie(movie)
class Showtime:
- id, theater, movie, datetime, screen_label
- reservations: list[Reservation] # source of truth for seat state
+ is_available(seat_id) -> bool
+ get_available_seats() -> list[str]
+ book(reservation) -> None # atomic; may raise
+ cancel(reservation) -> None
class Movie:
- id, title
class Reservation:
- confirmation_id, showtime, seat_ids: list[str]
# no cancel() here — Showtime mutates its own list
// Constants
static final List<String> SEAT_LAYOUT = /* A0..Z20 → 546 seats */ List.of();
class BookingSystem {
// theaters + indexes (built at construction / updated on add)
List<Showtime> searchMovies(String title) { /* ... */ return List.of(); }
List<Showtime> getShowtimesAtTheater(Theater theater) { /* ... */ return List.of(); }
Reservation book(String showtimeId, List<String> seatIds) { /* ... */ return null; }
void cancelReservation(String confirmationId) { /* ... */ }
}
class Theater {
String id, name;
List<Showtime> showtimes;
List<Showtime> getShowtimes() { return showtimes; }
List<Showtime> getShowtimesForMovie(Movie movie) { /* ... */ return List.of(); }
}
class Showtime {
String id, screenLabel;
Theater theater;
Movie movie;
Instant datetime;
List<Reservation> reservations; // source of truth for seat state
boolean isAvailable(String seatId) { /* ... */ return false; }
List<String> getAvailableSeats() { /* ... */ return List.of(); }
void book(Reservation reservation) { /* atomic; may throw */ }
void cancel(Reservation reservation) { /* ... */ }
}
class Movie { String id, title; }
class Reservation {
String confirmationId;
Showtime showtime;
List<String> seatIds;
// no cancel() here — Showtime mutates its own list
}
Implementation highlights
Showtime.book / cancel fully and sketch BookingSystem orchestration. Search/browse are O(n) scans or prebuilt indexes — describe verbally.movies_by_id, showtimes_by_movie_id, showtimes_by_id, plus reservations_by_id filled on book for O(1) cancel routing.def book(self, showtime_id: str, seat_ids: list[str]) -> Reservation:
if not showtime_id or not seat_ids:
raise ValueError("invalid booking request")
showtime = self.showtimes_by_id.get(showtime_id)
if showtime is None:
raise KeyError("showtime not found")
reservation = Reservation(
confirmation_id=new_id(),
showtime=showtime,
seat_ids=list(seat_ids),
)
showtime.book(reservation) # raises if any seat taken
self.reservations_by_id[reservation.confirmation_id] = reservation
return reservation
def cancel_reservation(self, confirmation_id: str) -> None:
reservation = self.reservations_by_id.get(confirmation_id)
if reservation is None:
raise KeyError("reservation not found")
reservation.showtime.cancel(reservation)
del self.reservations_by_id[confirmation_id]
Reservation book(String showtimeId, List<String> seatIds) {
if (showtimeId == null || seatIds == null || seatIds.isEmpty())
throw new IllegalArgumentException("invalid booking request");
Showtime showtime = showtimesById.get(showtimeId);
if (showtime == null) throw new NoSuchElementException("showtime not found");
Reservation reservation = new Reservation(newId(), showtime, new ArrayList<>(seatIds));
showtime.book(reservation); // raises if any seat taken
reservationsById.put(reservation.confirmationId, reservation);
return reservation;
}
void cancelReservation(String confirmationId) {
Reservation reservation = reservationsById.get(confirmationId);
if (reservation == null) throw new NoSuchElementException("reservation not found");
reservation.showtime.cancel(reservation);
reservationsById.remove(confirmationId);
}
from threading import Lock
class Showtime:
def __init__(self, ...):
self.reservations: list[Reservation] = []
self._lock = Lock()
def is_available(self, seat_id: str) -> bool:
return all(seat_id not in r.seat_ids for r in self.reservations)
def get_available_seats(self) -> list[str]:
booked = {s for r in self.reservations for s in r.seat_ids}
return [s for s in SEAT_LAYOUT if s not in booked]
def book(self, reservation: Reservation) -> None:
with self._lock:
for seat in reservation.seat_ids:
if seat not in SEAT_LAYOUT:
raise ValueError(f"invalid seat {seat}")
if not self.is_available(seat):
raise RuntimeError(f"seat unavailable {seat}")
self.reservations.append(reservation) # all-or-nothing
def cancel(self, reservation: Reservation) -> None:
with self._lock:
self.reservations.remove(reservation)
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
class Showtime {
List<Reservation> reservations = new ArrayList<>();
private final ReentrantLock lock = new ReentrantLock();
Showtime(/* ... */) { }
boolean isAvailable(String seatId) {
for (Reservation r : reservations)
if (r.seatIds.contains(seatId)) return false;
return true;
}
List<String> getAvailableSeats() {
Set<String> booked = new HashSet<>();
for (Reservation r : reservations) booked.addAll(r.seatIds);
List<String> out = new ArrayList<>();
for (String s : SEAT_LAYOUT) if (!booked.contains(s)) out.add(s);
return out;
}
void book(Reservation reservation) {
lock.lock();
try {
for (String seat : reservation.seatIds) {
if (!SEAT_LAYOUT.contains(seat))
throw new IllegalArgumentException("invalid seat " + seat);
if (!isAvailable(seat))
throw new RuntimeException("seat unavailable " + seat);
}
reservations.add(reservation); // all-or-nothing
} finally {
lock.unlock();
}
}
void cancel(Reservation reservation) {
lock.lock();
try {
reservations.remove(reservation);
} finally {
lock.unlock();
}
}
}
Verification traces
- Happy book — book A5,A6 → reservation stored;
reservations_by_idregistered; seats gone from available. - Concurrent same seat — two threads book A5; lock serializes; second raises; exactly one winner (R6).
- Cancel — cancel by confirmation ID → Showtime removes reservation → A5,A6 free again; routing index cleaned.
- Partial multi-seat fail — request A5,A6,A7 when A6 taken → exception before append; A5 never claimed (all-or-nothing).
- Happy: User holds A1+A2 → LOCKED with TTL → payment ok → BOOKED.
- Failure: Hold overlapping already LOCKED seats → reject entire hold; payment fail → seats AVAILABLE.
- Concurrency: Two holds on A1 — showtime lock makes transitions serial; loser sees reject.
Extensibility
add_showtime(theater, showtime) that updates every index the constructor built. Remove only if no active reservations; then clean movie indexes if nothing else shows that title.# Showtime gains:
# holds: dict[hold_id, SeatHold] # seat_ids, expires_at
def is_available(self, seat_id: str) -> bool:
if any(seat_id in r.seat_ids for r in self.reservations):
return False
now = time.time()
for hold in self.holds.values():
if hold.expires_at > now and seat_id in hold.seat_ids:
return False
return True
def hold_seats(self, seat_ids: list[str], timeout_s: float = 300) -> str:
with self._lock:
for s in seat_ids:
if not self.is_available(s):
raise RuntimeError(s)
hid = new_id()
self.holds[hid] = SeatHold(seat_ids, time.time() + timeout_s)
return hid
def confirm_hold(self, hold_id: str, reservation: Reservation) -> None:
with self._lock:
hold = self.holds.get(hold_id)
if hold is None or time.time() > hold.expires_at:
self.holds.pop(hold_id, None)
raise RuntimeError("hold missing/expired")
del self.holds[hold_id]
self.reservations.append(reservation)
# Background: cleanup_expired_holds() under the same lock
// Showtime gains:
// Map<String, SeatHold> holds; // seatIds, expiresAt
boolean isAvailable(String seatId) {
for (Reservation r : reservations)
if (r.seatIds.contains(seatId)) return false;
long now = System.currentTimeMillis();
for (SeatHold hold : holds.values()) {
if (hold.expiresAt > now && hold.seatIds.contains(seatId)) return false;
}
return true;
}
String holdSeats(List<String> seatIds, long timeoutMs) {
lock.lock();
try {
for (String s : seatIds)
if (!isAvailable(s)) throw new RuntimeException(s);
String hid = newId();
holds.put(hid, new SeatHold(seatIds, System.currentTimeMillis() + timeoutMs));
return hid;
} finally {
lock.unlock();
}
}
void confirmHold(String holdId, Reservation reservation) {
lock.lock();
try {
SeatHold hold = holds.get(holdId);
if (hold == null || System.currentTimeMillis() > hold.expiresAt) {
holds.remove(holdId);
throw new RuntimeException("hold missing/expired");
}
holds.remove(holdId);
reservations.add(reservation);
} finally {
lock.unlock();
}
}
// Background: cleanupExpiredHolds() under the same lock
Common interview pitfalls
These mistakes show up constantly on this prompt. Name the trap, then show the fix in your design — don’t wait for the interviewer to catch you.
- Jumping to microservices / payment gateway design instead of seat state ownership.
- Letting UI “selected seats” be the source of truth without server-side hold.
- No distinction between browse and reserve — inventory lies under load.
- Check-then-act on seat.is_free without a lock or atomic state transition.
- Omitting showtime as the aggregate that owns the seat map.
- Confirming booking before payment stub fails — seats stuck forever.
Interview script (say this)
Read this once out loud before a mock. It’s the spine of a strong answer — not a script to recite robotically.
- Clarify search vs book, seat map per showtime, hold TTL, payment stub, cancellation.
- v1: Movie/Showtime/Seat; reserve holds seats; confirm after payment stub; release on fail/expiry.
- Showtime owns seats in AVAILABLE/LOCKED/BOOKED.
- API: search, get_seats(show_id), hold(seats, user), confirm(hold_id), release.
- hold is atomic for the seat set — all or nothing.
- Trace: two users hold overlapping seats — one wins; payment fail releases.
- Concurrency: lock per showtime (start coarse) around seat transitions.
- Extensions: pricing tiers, couple seats as constraints, waitlist.
Extra verification traces
Walk these three traces on the board. If you can narrate them cleanly, your implementation section usually follows.
def hold(self, seat_ids, user_id, ttl):
with self.lock:
if any(self.seats[s].state != "AVAILABLE" for s in seat_ids):
raise RuntimeError("unavailable")
for s in seat_ids:
self.seats[s].lock(user_id, self.now() + ttl)
return Hold(seat_ids, user_id)
Hold hold(List<String> seatIds, String userId, Duration ttl) {
lock.lock();
try {
for (String s : seatIds)
if (!"AVAILABLE".equals(seats.get(s).state))
throw new RuntimeException("unavailable");
for (String s : seatIds)
seats.get(s).lock(userId, now().plus(ttl));
return new Hold(seatIds, userId);
} finally {
lock.unlock();
}
}
Staff-level follow-ups
At staff+, they twist the prompt. Answer in one sentence that names the seam — don’t redesign the whole board.
- Partner cinemas? — CinemaAdapter for seat maps; Showtime still owns booking states.
- Dynamic pricing? — PriceStrategy(show, seat) at confirm time; seat state machine unchanged.
- Group booking contiguous? — Validator on hold checks adjacency before locking.
- Distributed shows DB? — Per-show row lock or conditional update on seat version.
Complete solution: Showtime with lock
from threading import Lock
import uuid
SEAT_LAYOUT = [f"{r}{n}" for r in "ABCDE" for n in range(1, 11)]
class Reservation:
def __init__(self, seats: list[str], showtime: "Showtime"):
self.id = str(uuid.uuid4())
self.seats = list(seats)
self.showtime = showtime
class Showtime:
def __init__(self, sid: str):
self.id = sid
self.reservations: list[Reservation] = []
self._lock = Lock()
def _booked(self) -> set[str]:
return {s for r in self.reservations for s in r.seats}
def book(self, seat_ids: list[str]) -> Reservation:
with self._lock:
booked = self._booked()
for s in seat_ids:
if s not in SEAT_LAYOUT or s in booked:
raise RuntimeError(f"unavailable {s}")
r = Reservation(seat_ids, self)
self.reservations.append(r)
return r
def cancel(self, reservation: Reservation) -> None:
with self._lock:
self.reservations.remove(reservation)
# All-or-nothing: raise before append if any seat taken.
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.UUID;
class Reservation {
String id;
List<String> seats;
Showtime showtime;
Reservation(List<String> seats, Showtime showtime) {
this.id = UUID.randomUUID().toString();
this.seats = new ArrayList<>(seats);
this.showtime = showtime;
}
}
class Showtime {
static final List<String> SEAT_LAYOUT = /* A1..E10 */ List.of();
String id;
List<Reservation> reservations = new ArrayList<>();
private final ReentrantLock lock = new ReentrantLock();
Showtime(String sid) { this.id = sid; }
Set<String> booked() {
Set<String> out = new HashSet<>();
for (Reservation r : reservations) out.addAll(r.seats);
return out;
}
Reservation book(List<String> seatIds) {
lock.lock();
try {
Set<String> booked = booked();
for (String s : seatIds)
if (!SEAT_LAYOUT.contains(s) || booked.contains(s))
throw new RuntimeException("unavailable " + s);
Reservation r = new Reservation(seatIds, this);
reservations.add(r);
return r;
} finally {
lock.unlock();
}
}
void cancel(Reservation reservation) {
lock.lock();
try {
reservations.remove(reservation);
} finally {
lock.unlock();
}
}
}
// All-or-nothing: throw before add if any seat taken.
Concurrency cases
- Same seat, two books — lock serializes; second raises.
- Multi-seat partial — A5 free, A6 taken: fail entire cart; A5 must not stay claimed.
- Different showtimes — different locks → true parallelism.
- Hold extension — hold/confirm/cancel all under the same show lock; never hold across payment I/O.
# Alice book(A5) Bob book(A5)
# acquire wait
# A5 free → append
# release
# acquire
# A5 booked → raise
# release
// Alice book(A5) Bob book(A5)
// acquire wait
// A5 free → append
// release
// acquire
// A5 booked → throw
// release