Movie Ticket Booking
Two people staring at the same seat map, both tap seat B12 at the same instant, and only one of them should walk away with a ticket. That race is the entire reason this problem shows up in interviews - everything else is bookkeeping around it.
Requirements
Functional
- A
TheaterhasShows (a movie at a specific time in a specific screen); each show has a seat map inherited from its screen's layout. - A customer selects seats for a show and books them; a booked seat cannot be selected by anyone else.
- Seats have categories (regular, premium) and each category has its own price for a show.
- A booking can be cancelled, which frees its seats.
Non-functional
- Two customers selecting the same seat at nearly the same time must never both succeed - exactly one booking should win.
- A seat a customer is actively choosing (but hasn't paid for yet) shouldn't be immediately grabbable by someone else, but it also shouldn't be locked forever if they abandon checkout.
Design
Every Seat on a Show carries its own state - AVAILABLE, LOCKED, or BOOKED - and
every transition between those states goes through one method, Seat.tryLock /
Seat.confirm / Seat.release. Show never flips a seat's state directly; it only ever
asks the seat to do it, which is what makes "two requests, one winner" enforceable in one
place instead of scattered across every caller.
- 1Alice and Bob both request B12 within the same instant.
- 2Alice's request reaches the seat's atomic lock first and flips AVAILABLE to LOCKED.
- 3Bob's request arrives a moment later.
- 4The seat is already LOCKED, so Bob's tryLock returns false - his booking attempt fails cleanly.
- 5Alice completes payment before any timeout; her lock becomes a permanent BOOKED.
Booking is created only after every requested seat has been locked successfully - if even
one seat in the request is already taken, the whole booking attempt fails and any seats it
did manage to lock are released, so a customer never ends up holding three of the four seats
they asked for.
Every one of a seat's three states has exactly one way back to AVAILABLE, and neither of
them is a coincidence - release() is the single method both paths call:
Class diagram
Code
Design decisions
- Seats get a three-state machine (
AVAILABLE/LOCKED/BOOKED) instead of a booleanisBooked. A boolean can't represent "someone is mid-checkout on this seat right now" - without the middle state, two customers could both see a seat as free during the few seconds between selection and payment. The extra state is what a real booking flow actually needs, not gold-plating. Seat.tryLockis a compare-and-set, not a check-then-set.if (seat.isAvailable()) seat.lock()from two threads can both pass the check before either calls lock - the classic time-of-check-to-time-of-use race.tryLockdoes the check and the state flip as one atomic step (asynchronizedmethod in Java, a lock-guarded compare in Python), so only one caller ever seestruecome back.- Locking is per-seat, not per-show. A single lock around the whole show would correctly prevent double-booking, but it would also serialize every customer browsing that show, even ones picking completely different seats. Locking at the seat level is the difference between "correct" and "correct and doesn't fall over during a popular release's on-sale minute."
- What's missing for a real system: a locked seat here has no expiry - a production
system needs a TTL (a scheduled job or a lock timestamp checked on read) that reverts
LOCKEDback toAVAILABLEif the customer never completes payment, otherwise an abandoned checkout permanently removes a seat from sale.
Common follow-ups
- What happens if a user locks a seat and then closes the browser tab? Nothing in this
design ever reverts it -
LOCKEDis permanent untilconfirm()orrelease()is explicitly called, so an abandoned checkout removes that seat from sale forever. The fix is a TTL: store alockedAttimestamp onSeatand either a scheduled sweep that callsrelease()on anything locked past the timeout, or a check-on-read intryLockitself that treats an expired lock asAVAILABLEbefore attempting to acquire it. - How do you stop one user from locking the whole theater and holding every seat
hostage?
bookSeatsas written has no per-user or per-request limit on how many seats it locks. Add a cap (say, 10 seats per booking attempt) checked before the loop starts inShow.bookSeats, and rate-limit lock attempts per customer at the API layer aboveShowentirely -Seat's locking mechanics don't need to know about either limit. - How would you support dynamic, seat-level pricing (front row cheaper on a slow
Tuesday matinee, back row premium on opening night)?
priceByCategoryis currently a flat map fromSeatCategoryto price, shared by every showing of every movie. Swap it for aPricingStrategykeyed by(Show, SeatCategory)- same shape asFeeStrategyin parking-lot.mdx - soShow.bookSeatsasks the strategy for a price instead of reading a static map, and nothing about seat locking changes. - A customer's payment fails after every seat locked successfully - what's the cleanup
path?
bookSeatsonly rolls back locks acquired during that same call if a later seat in the request fails to lock; it has no path for "all seats locked, but the downstream payment step failed." That needs aShow.abandonBooking(lockedSeatIds)(or equivalent) that callsrelease()on each one - the same method the TTL sweep above would call, just triggered by an explicit failure instead of a timeout.
Check yourself
Why does `Seat` use a three-state machine (AVAILABLE/LOCKED/BOOKED) instead of a boolean `isBooked`?