What this prompt is really asking
Cities · holds · pay
Show owns seats
lock + confirm
pricing · events
Analogy: restaurant reservation timer
BookMyShow seats are restaurant tables: you reserve for 10 minutes while you pay. If you don’t show, the host releases the table. Never freeze the whole dining room (global lock) while someone argues with the credit-card machine — hold the seats, release the lock, then pay.
Clarifying questions → locked requirements
- You: Movies only, or events/sports too? They: Movies for v1; events share the same seat-hold pattern later.
- You: Search by city + movie + date? They: Yes — list cinemas/shows; then seat map for one show.
- You: Instant book or hold-then-pay? They: Hold ~5–10 min; pay; confirm or release.
- You: Same seat, two users? They: Exactly one wins the lock.
- You: Payment gateway? They: Stub — success/fail callback; no real PSP.
- You: Variable layouts / seat categories? They: Fixed layout OK for v1; categories/pricing as extension.
Entities: hierarchy + three-state seats
- City / Cinema / Screen — location hierarchy. Screen owns a seat layout (list of seat ids or Seat objects with row/col).
- Movie — catalog title; many Shows reference it.
- Show — the bookable inventory unit: movie + screen + start time + per-seat status map + lock. Not the Cinema.
- SeatStatus — AVAILABLE | LOCKED | BOOKED (optional BLOCKED for admin).
- SeatLock — hold_id, user_id, seat_ids, expires_at.
- Booking — id, show, user, seats, status (PENDING / CONFIRMED / CANCELLED), amount, payment_id.
- BookingService — lock_seats, confirm_payment, cancel, search.
- PaymentGateway (interface) —
charge(booking_id, amount) -> PaymentResultstub.
Class design from the requirements
from enum import Enum, auto
from dataclasses import dataclass, field
from threading import Lock
from typing import Optional
import time
class SeatStatus(Enum):
AVAILABLE = auto()
LOCKED = auto()
BOOKED = auto()
class BookingStatus(Enum):
PENDING = auto()
CONFIRMED = auto()
CANCELLED = auto()
@dataclass
class SeatLock:
hold_id: str
user_id: str
seat_ids: list[str]
expires_at: float
@dataclass
class Booking:
id: str
show_id: str
user_id: str
seat_ids: list[str]
amount: float
status: BookingStatus = BookingStatus.PENDING
hold_id: Optional[str] = None
payment_id: Optional[str] = None
class Show:
# One screening — owns seat inventory + concurrency
def __init__(self, show_id: str, movie_id: str, screen_id: str,
start_ts: float, seat_ids: list[str]):
self.id = show_id
self.movie_id, self.screen_id, self.start_ts = movie_id, screen_id, start_ts
self.seats: dict[str, SeatStatus] = {s: SeatStatus.AVAILABLE for s in seat_ids}
self.locks: dict[str, SeatLock] = {} # hold_id → lock
self._lock = Lock()
def lock_seats(self, user_id: str, seat_ids: list[str],
ttl_s: float = 600) -> str: ...
def confirm_seats(self, hold_id: str) -> None: ...
def release_hold(self, hold_id: str) -> None: ...
class Cinema:
def __init__(self, cinema_id: str, city_id: str, name: str):
self.id, self.city_id, self.name = cinema_id, city_id, name
self.screens: dict[str, list[str]] = {} # screen_id → seat layout
self.shows: list[Show] = []
class BookingService:
def __init__(self, payment: "PaymentGateway"):
self.cities: dict[str, list[Cinema]] = {}
self.shows: dict[str, Show] = {}
self.bookings: dict[str, Booking] = {}
self.payment = payment
def search_shows(self, city_id: str, movie_id: str, day: str) -> list[Show]: ...
def create_booking(self, user_id: str, show_id: str,
seat_ids: list[str]) -> Booking: ...
def complete_payment(self, booking_id: str, success: bool) -> Booking: ...
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
enum SeatStatus { AVAILABLE, LOCKED, BOOKED }
enum BookingStatus { PENDING, CONFIRMED, CANCELLED }
class SeatLock {
String holdId, userId;
List<String> seatIds;
double expiresAt;
SeatLock(String holdId, String userId, List<String> seatIds, double expiresAt) {
this.holdId = holdId; this.userId = userId;
this.seatIds = seatIds; this.expiresAt = expiresAt;
}
}
class Booking {
String id, showId, userId;
List<String> seatIds;
double amount;
BookingStatus status = BookingStatus.PENDING;
String holdId, paymentId; // nullable
}
class Show {
// One screening — owns seat inventory + concurrency
String id, movieId, screenId;
double startTs;
Map<String, SeatStatus> seats = new HashMap<>();
Map<String, SeatLock> locks = new HashMap<>(); // holdId → lock
private final ReentrantLock lock = new ReentrantLock();
Show(String showId, String movieId, String screenId, double startTs, List<String> seatIds) {
this.id = showId; this.movieId = movieId; this.screenId = screenId; this.startTs = startTs;
for (String s : seatIds) seats.put(s, SeatStatus.AVAILABLE);
}
String lockSeats(String userId, List<String> seatIds, double ttlS) { /* ... */ return null; }
void confirmSeats(String holdId) { /* ... */ }
void releaseHold(String holdId) { /* ... */ }
}
class Cinema {
String id, cityId, name;
Map<String, List<String>> screens = new HashMap<>(); // screenId → seat layout
List<Show> shows = new ArrayList<>();
}
class BookingService {
Map<String, List<Cinema>> cities = new HashMap<>();
Map<String, Show> shows = new HashMap<>();
Map<String, Booking> bookings = new HashMap<>();
PaymentGateway payment;
BookingService(PaymentGateway payment) { this.payment = payment; }
List<Show> searchShows(String cityId, String movieId, String day) { /* ... */ return List.of(); }
Booking createBooking(String userId, String showId, List<String> seatIds) { /* ... */ return null; }
Booking completePayment(String bookingId, boolean success) { /* ... */ return null; }
}
Implementation highlights
Show.lock_seats / confirm_seats / release_hold and the BookingService payment orchestration.def _purge_expired(self, now: float) -> None:
expired = [hid for hid, h in self.locks.items() if h.expires_at <= now]
for hid in expired:
hold = self.locks.pop(hid)
for s in hold.seat_ids:
if self.seats[s] is SeatStatus.LOCKED:
self.seats[s] = SeatStatus.AVAILABLE
def lock_seats(self, user_id: str, seat_ids: list[str],
ttl_s: float = 600) -> str:
with self._lock:
now = time.time()
self._purge_expired(now)
for s in seat_ids:
if s not in self.seats or self.seats[s] is not SeatStatus.AVAILABLE:
raise RuntimeError(f"unavailable: {s}")
hold_id = new_id()
for s in seat_ids:
self.seats[s] = SeatStatus.LOCKED
self.locks[hold_id] = SeatLock(hold_id, user_id, list(seat_ids),
now + ttl_s)
return hold_id
def confirm_seats(self, hold_id: str) -> None:
with self._lock:
now = time.time()
hold = self.locks.get(hold_id)
if hold is None or hold.expires_at <= now:
self.locks.pop(hold_id, None)
raise RuntimeError("hold missing/expired")
for s in hold.seat_ids:
self.seats[s] = SeatStatus.BOOKED
del self.locks[hold_id]
def release_hold(self, hold_id: str) -> None:
with self._lock:
hold = self.locks.pop(hold_id, None)
if hold is None:
return
for s in hold.seat_ids:
if self.seats[s] is SeatStatus.LOCKED:
self.seats[s] = SeatStatus.AVAILABLE
void purgeExpired(double now) {
List<String> expired = new ArrayList<>();
for (var e : locks.entrySet())
if (e.getValue().expiresAt <= now) expired.add(e.getKey());
for (String hid : expired) {
SeatLock hold = locks.remove(hid);
for (String s : hold.seatIds)
if (seats.get(s) == SeatStatus.LOCKED) seats.put(s, SeatStatus.AVAILABLE);
}
}
String lockSeats(String userId, List<String> seatIds, double ttlS) {
lock.lock();
try {
double now = System.currentTimeMillis() / 1000.0;
purgeExpired(now);
for (String s : seatIds)
if (!seats.containsKey(s) || seats.get(s) != SeatStatus.AVAILABLE)
throw new RuntimeException("unavailable: " + s);
String holdId = newId();
for (String s : seatIds) seats.put(s, SeatStatus.LOCKED);
locks.put(holdId, new SeatLock(holdId, userId, new ArrayList<>(seatIds), now + ttlS));
return holdId;
} finally {
lock.unlock();
}
}
void confirmSeats(String holdId) {
lock.lock();
try {
double now = System.currentTimeMillis() / 1000.0;
SeatLock hold = locks.get(holdId);
if (hold == null || hold.expiresAt <= now) {
locks.remove(holdId);
throw new RuntimeException("hold missing/expired");
}
for (String s : hold.seatIds) seats.put(s, SeatStatus.BOOKED);
locks.remove(holdId);
} finally {
lock.unlock();
}
}
void releaseHold(String holdId) {
lock.lock();
try {
SeatLock hold = locks.remove(holdId);
if (hold == null) return;
for (String s : hold.seatIds)
if (seats.get(s) == SeatStatus.LOCKED) seats.put(s, SeatStatus.AVAILABLE);
} finally {
lock.unlock();
}
}
def create_booking(self, user_id: str, show_id: str,
seat_ids: list[str]) -> Booking:
show = self.shows[show_id]
hold_id = show.lock_seats(user_id, seat_ids)
amount = self._price(show, seat_ids) # flat rate v1
booking = Booking(
id=new_id(), show_id=show_id, user_id=user_id,
seat_ids=list(seat_ids), amount=amount, hold_id=hold_id,
)
self.bookings[booking.id] = booking
return booking # client now calls payment
def complete_payment(self, booking_id: str, success: bool) -> Booking:
booking = self.bookings[booking_id]
show = self.shows[booking.show_id]
if booking.status is not BookingStatus.PENDING:
raise RuntimeError("not pending")
if success:
show.confirm_seats(booking.hold_id)
booking.status = BookingStatus.CONFIRMED
booking.payment_id = new_id()
else:
show.release_hold(booking.hold_id)
booking.status = BookingStatus.CANCELLED
return booking
# Alternative: BookingService.checkout() calls payment.charge() inline,
# then complete_payment — keep charge outside the Show lock.
Booking createBooking(String userId, String showId, List<String> seatIds) {
Show show = shows.get(showId);
String holdId = show.lockSeats(userId, seatIds);
double amount = price(show, seatIds); // flat rate v1
Booking booking = new Booking();
booking.id = newId(); booking.showId = showId; booking.userId = userId;
booking.seatIds = new ArrayList<>(seatIds); booking.amount = amount; booking.holdId = holdId;
bookings.put(booking.id, booking);
return booking; // client now calls payment
}
Booking completePayment(String bookingId, boolean success) {
Booking booking = bookings.get(bookingId);
Show show = shows.get(booking.showId);
if (booking.status != BookingStatus.PENDING) throw new RuntimeException("not pending");
if (success) {
show.confirmSeats(booking.holdId);
booking.status = BookingStatus.CONFIRMED;
booking.paymentId = newId();
} else {
show.releaseHold(booking.holdId);
booking.status = BookingStatus.CANCELLED;
}
return booking;
}
// Alternative: checkout() calls payment.charge() inline,
// then completePayment — keep charge outside the Show lock.
Verification traces
- Happy path — lock A5,A6 → Booking PENDING → payment OK → seats BOOKED, booking CONFIRMED.
- Concurrent same seat — two lock_seats(A5); lock serializes; second raises; exactly one hold.
- Payment fail — release_hold → seats AVAILABLE; booking CANCELLED.
- TTL expiry — purge before next lock → stale LOCKED becomes AVAILABLE; confirm on expired hold fails.
- Double confirm — second complete_payment on CONFIRMED rejected.
- Happy: Pick show → lock seats → pay → BOOKED tickets issued.
- Failure: TTL expires before pay → seats AVAILABLE; pay fails → release.
- Concurrency: Two users lock same seat — only first transition AVAILABLE→LOCKED wins.
Extensibility follow-ups
- PricingStrategy —
price(show, seat) -> money; category (REGULAR/PREMIUM), weekday vs weekend. Don’t bake amounts into SeatStatus. - Cancel / refund — CONFIRMED → CANCELLED; seats AVAILABLE if before show start; refund via PaymentGateway (idempotent).
- Seat categories / BLOCKED — layout metadata on Screen; BLOCKED never offered in the map.
- Events / sports — same Show + seat-hold machine; swap Movie for Event catalog.
- Scale hint — Redis SET NX PX per
show:seatfor distributed holds; durable Booking in DB; waiting room for tentpole on-sales. Say it; don’t implement Redis on the whiteboard.
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.
- City→Cinema→Hall→Show hierarchy missing — flat “movie list” that can’t price by show.
- Same pitfalls as movie booking: no LOCKED TTL, payment after BOOKED, race on seats.
- Overbuilding events/sports before seats for movies work.
- Search that mutates inventory.
- God class BookMyShowApp doing payment, search, and seat mutation.
- Ignoring that show identity includes screen + time, not just movie id.
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.
- BookMyShow is product-shaped booking: city, cinema, show, seats, payment stub.
- Hierarchy: City→Cinema→Screen→Show; Show owns seat states AVAILABLE→LOCKED→BOOKED.
- APIs: browse shows, get seat map, lock seats with TTL, confirm with payment port, cancel.
- Payment is an interface — success confirms, failure releases locks.
- I’ll trace double booking on the same seat and expired lock reclaim.
- Concurrency: lock at Show for seat transitions.
- Staff: add events by new Show subtype or category without rewriting seat FSM.
Extra verification traces
Walk these three traces on the board. If you can narrate them cleanly, your implementation section usually follows.
def confirm(self, lock_id, payment):
with self.show.lock:
hold = self.holds[lock_id]
if hold.expired(self.now()):
self.release(hold)
raise RuntimeError("expired")
payment.charge(hold.amount) # port
for s in hold.seats:
self.seats[s].book()
return Ticket.from_hold(hold)
Ticket confirm(String lockId, PaymentGateway payment) {
show.lock.lock();
try {
Hold hold = holds.get(lockId);
if (hold.expired(now())) {
release(hold);
throw new RuntimeException("expired");
}
payment.charge(hold.amount); // port
for (String s : hold.seats) seats.get(s).book();
return Ticket.fromHold(hold);
} finally {
show.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.
- Events & sports? — Generalize Show→EventOccurrence; seat map may become sectioned VenueMap.
- Gift cards? — PaymentPort composition — charge splits across instruments.
- Fraud holds? — User risk score shortens TTL / challenges before lock.
- Fan-out search? — Read models for browse; write path remains Show.seat FSM.
Complete solution: lock → pay → confirm
from enum import Enum, auto
from threading import Lock
from dataclasses import dataclass
from typing import Optional
import time, uuid
class SeatStatus(Enum):
AVAILABLE = auto()
LOCKED = auto()
BOOKED = auto()
@dataclass
class Hold:
id: str
user_id: str
seats: list[str]
expires_at: float
class Show:
def __init__(self, seat_ids: list[str]):
self.seats = {s: SeatStatus.AVAILABLE for s in seat_ids}
self.holds: dict[str, Hold] = {}
self._lock = Lock()
def _purge(self, now: float) -> None:
for hid, h in list(self.holds.items()):
if h.expires_at <= now:
for s in h.seats:
if self.seats[s] is SeatStatus.LOCKED:
self.seats[s] = SeatStatus.AVAILABLE
del self.holds[hid]
def lock_seats(self, user_id: str, seats: list[str], ttl: float = 600) -> str:
with self._lock:
now = time.time()
self._purge(now)
for s in seats:
if self.seats.get(s) is not SeatStatus.AVAILABLE:
raise RuntimeError(s)
hid = str(uuid.uuid4())
for s in seats:
self.seats[s] = SeatStatus.LOCKED
self.holds[hid] = Hold(hid, user_id, list(seats), now + ttl)
return hid
def confirm(self, hold_id: str) -> None:
with self._lock:
now = time.time()
h = self.holds.get(hold_id)
if not h or h.expires_at <= now:
self.holds.pop(hold_id, None)
raise RuntimeError("expired")
for s in h.seats:
self.seats[s] = SeatStatus.BOOKED
del self.holds[hold_id]
def release(self, hold_id: str) -> None:
with self._lock:
h = self.holds.pop(hold_id, None)
if not h:
return
for s in h.seats:
if self.seats[s] is SeatStatus.LOCKED:
self.seats[s] = SeatStatus.AVAILABLE
# BookingService.create_booking → lock_seats (ms)
# then payment WITHOUT show lock
# then confirm or release
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.UUID;
enum SeatStatus { AVAILABLE, LOCKED, BOOKED }
class Hold {
String id, userId;
List<String> seats;
double expiresAt;
Hold(String id, String userId, List<String> seats, double expiresAt) {
this.id = id; this.userId = userId; this.seats = seats; this.expiresAt = expiresAt;
}
}
class Show {
Map<String, SeatStatus> seats = new HashMap<>();
Map<String, Hold> holds = new HashMap<>();
private final ReentrantLock lock = new ReentrantLock();
Show(List<String> seatIds) {
for (String s : seatIds) seats.put(s, SeatStatus.AVAILABLE);
}
void purge(double now) {
for (var e : new ArrayList<>(holds.entrySet())) {
if (e.getValue().expiresAt <= now) {
for (String s : e.getValue().seats)
if (seats.get(s) == SeatStatus.LOCKED) seats.put(s, SeatStatus.AVAILABLE);
holds.remove(e.getKey());
}
}
}
String lockSeats(String userId, List<String> seatsToLock, double ttl) {
lock.lock();
try {
double now = System.currentTimeMillis() / 1000.0;
purge(now);
for (String s : seatsToLock)
if (seats.get(s) != SeatStatus.AVAILABLE) throw new RuntimeException(s);
String hid = UUID.randomUUID().toString();
for (String s : seatsToLock) seats.put(s, SeatStatus.LOCKED);
holds.put(hid, new Hold(hid, userId, new ArrayList<>(seatsToLock), now + ttl));
return hid;
} finally {
lock.unlock();
}
}
void confirm(String holdId) {
lock.lock();
try {
double now = System.currentTimeMillis() / 1000.0;
Hold h = holds.get(holdId);
if (h == null || h.expiresAt <= now) {
holds.remove(holdId);
throw new RuntimeException("expired");
}
for (String s : h.seats) seats.put(s, SeatStatus.BOOKED);
holds.remove(holdId);
} finally {
lock.unlock();
}
}
void release(String holdId) {
lock.lock();
try {
Hold h = holds.remove(holdId);
if (h == null) return;
for (String s : h.seats)
if (seats.get(s) == SeatStatus.LOCKED) seats.put(s, SeatStatus.AVAILABLE);
} finally {
lock.unlock();
}
}
}
// BookingService.createBooking → lockSeats (ms)
// then payment WITHOUT show lock
// then confirm or release
Concurrency cases
- Double lock same seats — second lock_seats fails.
- Confirm after TTL — purge/confirm sees expired → fail; seats AVAILABLE for others.
- Pay success + confirm race with expire job — both under show lock; confirm wins only if hold still present.
- Never call payment inside
with self._lock.