Course contents

LLD problem — Movie Booking (BookMyShow-style)

Sign in to run this lab and save your progress

Learn

Before you start

BookMyShow-style movie booking is a rich design problem in one subtle way: atomicity. Everything else is bookkeeping.

The atomicity signal

hold(seats) must reserve ALL seats or NONE. Half-holds are a disaster — a caller thinks they got 5 seats but actually got 3 and lost 2 to a race. In a real production system this leads to "yeah bro I booked 5 seats but only 3 showed up in my confirmation".

Enforcement pattern: two-phase check-then-mutate:

def hold(self, show_id, seats, ...):
    # Phase 1: check ALL seats are available. Raise if any fails.
    for r, c in seats:
        if not self._seat_available(show_id, r, c):
            raise ValueError(f"seat {(r,c)} not available")
    # Phase 2: mutate. Guaranteed to succeed.
    for r, c in seats:
        self._mark_held(show_id, r, c)

If phase 1 raises, no state changes. If phase 1 passes, phase 2 runs cleanly. Not thread-safe (that's a lock or DB transaction), but atomic within a single call — which is what MVP needs.

The state question

Each seat has 3 states: available, held, booked. Transitions:

  • available → held (via hold)
  • held → booked (via confirm)
  • held → available (via release or expiry)
  • booked → nothing (no cancellation in MVP)

Do you use the State pattern (module 3.14) per seat? Almost certainly no — 3 states × N seats = ceremony without payoff. A single status field per seat plus one mutation helper does fine.

But the TRANSITIONS still need to be in ONE place. If confirm and release both write seat.status = ... directly, bugs will drift. Route both through a helper.

The expiry model

Real systems use wall-clock time + background sweeps. For interviews, you sidestep threading by making the caller drive time:

bs.hold("s1", [(0,0)], "alice", hold_seconds=60)
bs.tick(now=1000)   # nothing expires
bs.tick(now=99999)  # everything old expires

Your design.md should acknowledge this is a simplification and mention what a production system would do differently (event loop, DB TTL, background thread).

The rules

Three matter here:

  1. No seat is both held AND booked. Enforced by the seat's status field being one value at a time.
  2. Hold is atomic (all seats or none). Enforced by the two-phase pattern above.
  3. Expired hold can't be confirmed. Enforced by confirm() checking the hold's status is still "active" before promoting to "booked".

Name each enforcement point in design.md.

Interviewer follow-ups to prepare for

  • "How would you handle two users trying to hold the same seat at the same time?" — locks / DB row-level lock / optimistic concurrency. Out of MVP scope; mention.
  • "How would you scale to millions of seats and thousands of requests per second?" — HLD territory. Sharded seat inventory, event-driven cache invalidation. Also out of scope.
  • "How would you allow cancellations?" — new state (cancelled) + refund flow. Manageable extension if your design is clean.

Your task

Model a movie theater booking system. Shows, seats, holds, bookings. Concurrent-holds-on-same-seat prevention is the interesting bit even in a single-threaded MVP.

The problem

Setup:

  • A theater has a fixed grid of seats (rows × cols).
  • Shows are specific movies at specific times.
  • Each show has its own seat map (all seats start "available").

Operations:

  • add_movie(movie_id, title)
  • add_show(show_id, movie_id, start_time) — assumes theater seat layout given at init
  • available_seats(show_id) — list of (row, col) tuples that are still bookable
  • hold(show_id, seats, user_id, hold_seconds=60) — hold the given seats (list of (row, col) tuples) for hold_seconds. Returns a hold_id on success. Raises ValueError if ANY seat is unavailable at hold time.
  • confirm(hold_id) — commit the hold to a booking. Returns a booking_id. Raises if hold expired or already confirmed.
  • release(hold_id) — release the hold without booking.
  • tick(now) — advance time. Expires holds whose expiry <= now. (No threads; the test drives time.)

Seat states: availableheldbooked (via confirm) or → available (via release / expiry).

Requirement-gathering questions

  1. How do we handle time? — Caller passes now (int seconds). hold stores expiry = now + hold_seconds. But hold doesn't take now — we drive expiry via tick(now). Simpler: hold returns the expiry time; tick(now) expires any hold whose expiry <= now.
  2. Concurrent holds on the same seat? — Not modeled (no threading). But the SINGLE-request atomicity (all seats or none) must be enforced.
  3. Payment integration? — No. confirm just books; no payment flow.
  4. Booking cancellation? — Not for MVP.
  5. Refunds? — Not for MVP.

Design goals

  • Seat state transitions are first-class (available → held → booked / released).
  • Atomicity: hold either reserves ALL requested seats or NONE. No partial holds.
  • Invariants: no seat is both held and booked; no expired hold can be confirmed.

Your task

Public API:

from main import BookingSystem

bs = BookingSystem(rows=5, cols=8)
bs.add_movie("m1", "Interstellar")
bs.add_show("s1", "m1", start_time=1000)

bs.available_seats("s1")   # 40 tuples

hold_id = bs.hold("s1", seats=[(0,0),(0,1)],
                  user_id="alice", hold_seconds=60)
# hold_seconds is used with tick(now) later.

booking_id = bs.confirm(hold_id)   # commits

bs.available_seats("s1")   # 38 tuples

# Time-driven expiry:
hold2 = bs.hold("s1", [(1,0)], "bob", hold_seconds=60)
bs.tick(now=1000)   # nothing expires (assume hold created at some time <= 1000)
# After enough time...
bs.tick(now=99999)  # hold2 expires, seat back to available

Time model: for the tests, we'll call hold at the start, then explicitly call tick(now) with a large number to test expiry. Internally you can assume creation time = 0 and expiry = hold_seconds (or use a global counter — your choice).

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
07-movie-booking-starter