InterviewPrepKit

Home / Cheat Sheet / Object-Oriented Design

Cheat sheet

How to design a movie-booking system

Read the full lesson →

Selling numbered cinema seats is an object-model problem whose hard part is concurrency: the read that decides and the write that acts must be one indivisible step.

API (the whole external surface)

  • hold(seat_ids, user_id) -> SeatHold — reserves a set of seats with a TTL; raises HoldRejected and changes nothing on failure.
  • confirm(hold_id, amount_cents) -> Booking — turns a live hold into a paid receipt.
  • release_expired() -> int — the reaper; drops abandoned holds, returns how many.
  • available() -> list[str] — read-only seat map, current now.

Core objects

ObjectIsLifetime
Seatphysical chair (row, number, class)years
Showone movie, one auditorium, one timeone screening
ShowSeatthis chair at that screening + statuswith the show
SeatHoldtemporary claim on a set, with expiryseconds–minutes
Bookingpaid claim + price chargedforever (receipt)
  • ShowSeat is the class first drafts miss. Seat.is_booked makes a chair bookable once ever; ShowSeat makes it bookable once per show.
  • Booking stores the amount charged, not a pointer to a price rule — a refund from a rule that changed later is a silent bug.
  • A 200-seat auditorium × 6 shows/day = 1200 ShowSeat rows/day; the 200 Seat rows are created once.

The race: check-then-act

Two servers read C7 = AVAILABLE, both decide “allowed”, both write HELD, both confirm. One chair, two tickets, no error line.

read  status -> AVAILABLE     read  status -> AVAILABLE
decide: allowed               decide: allowed
write HELD by A               write HELD by B   <- overwrites
  • The bug is not “forgot a lock”; it is that read and write were two steps. Any fix that rejoins them works.

Three fixes, priced

FixMechanismCost / scope
Monitor (one lock per Show)with self._lock: around read-decide-writecorrect only in one process
Compare-and-set per seatatomic AVAILABLE -> HELDlock-free; multi-seat needs rollback
Conditional UPDATEWHERE status='AVAILABLE'the real multi-server answer
  • One lock per show is not a bottleneck: ~0.33 holds/sec vs a ~5µs critical section = ~600,000x headroom.

Seat state machine

AVAILABLE --hold()--> HELD --confirm()--> BOOKED
HELD   --release()/TTL expiry-->  AVAILABLE
BOOKED --cancel()------------->   AVAILABLE
  • HELD exists only because payment takes human time. Remove payment, the state disappears.

Rules and gotchas

  • Clock is injected as a Protocol (now() -> float). Reading the wall clock makes expiry untestable (tests would sleep); a FakeClock.advance(121) tests the TTL in zero wall-clock time.
  • TTL = 120s is p95 checkout time; costs ~1% of inventory temporarily unsellable (double the TTL, double the cost).
  • Contract: HoldRejected or nothing changed. Validate unknown seat ids and the empty set before the reap loop; validate money at the top of confirm (non-negative int).
  • hold takes a set, all-or-nothing — group booking is then free and cannot be retrofitted onto a single-seat API.
  • Sort seat ids in hold (sorted(set(...))): a global acquisition order that prevents deadlock, for free.
  • Reap on every read path, not just the timer, so a stale seat shows free one second after expiry.
  • The lock protects the method, not the field. Public seats/holds let one line defeat every lock; production uses private fields + the DB WHERE clause.
  • Booking/SeatHold are frozen dataclasses — facts that happened; an editable receipt disagrees with the money that moved.

Change absorbed by interfaces

  • Pricing = Strategy + Decorator, computed by the caller and handed to confirm as amount_cents; Show holds no PricingStrategy.
  • Refunds = policy over Booking.amount_cents (the stored snapshot, not a rule).
  • Group-adjacent = one pure adjacent_runs function; a run of n is genuine when window[-1] - window[0] == n - 1. It runs on a snapshot, so treat the search as advisory and let hold reject and re-search.
  • Baked in (no interface): a seat is a physical location (so overbooking is a bug), contention unit is one Show, money is integer cents, hold authority is a timestamp.
Want the full picture? The lesson has the derivations, worked examples, and diagrams this card compresses into bullets. Read the full lesson →
Report a bug