Skip to content
FollowtheDamnCode

LLD: Movie Ticket Booking

Cinemas, shows, seat maps, and the hard part — holding seats for a few minutes so two people can never buy the same one.

Big idea

A ticket-booking system is mostly catalogue (cinemas, screens, movies, shows) plus one genuinely hard problem: two people must never book the same seat. The standard answer is a temporary hold: selecting seats atomically locks them for a few minutes while the user pays; payment confirms the hold into a booking; timeout or failure releases it. Seats move through a small state machine, and every transition is checked.

Requirements

Functional

  • Browse movies and shows by city/cinema/time; see a show's seat map.
  • Select seats → they're held for the user for N minutes.
  • Pay → booking confirmed, tickets issued. Payment failure or timeout → seats released.
  • Seat categories with different prices (regular, premium).

Non-functional: no double booking under heavy concurrency (a popular show opening); holds expire reliably; seat map reads are fast.

Out of scope: payment provider details, refunds/cancellations (extensions).

Entities

Movie, Cinema, Screen (seat layout), Show (movie + screen + start time; owns the seat states), Seat (row, number, category), SeatState (AVAILABLE, HELD, BOOKED), Hold (user, seats, expires_at), Booking, PricingPolicy, BookingService (the use cases), Clock.

Think of it like

A restaurant's waiting list with a pager. When a table frees up, the host holds it for you and buzzes your pager; you have five minutes to walk over. If you don't show, the table goes to the next party. Nobody else can be seated there while the hold is on — but the hold doesn't last forever either.

Class diagram

A show owns its seat states; holds become bookings
A seat's lifecycle within one show

Key flow: two users race for the same seat

Only one hold can win

Code skeleton

Holds, a race, a timeout, and a confirmation
import threading

class Show:
    def __init__(self, sid, seats):
        self.id = sid
        self.state = {s: "available" for s in seats}        # seat -> available / hold id / booked
        self.lock = threading.Lock()                       # one lock per show

    def try_hold(self, seats, hold_id):
        with self.lock:                                    # check-and-set must be atomic
            if any(self.state.get(s) != "available" for s in seats):
                return False
            for s in seats:                                # all-or-nothing
                self.state[s] = hold_id
            return True

    def release(self, hold_id):
        with self.lock:
            for s, v in self.state.items():
                if v == hold_id:
                    self.state[s] = "available"

    def book(self, hold_id):
        with self.lock:
            for s, v in self.state.items():
                if v == hold_id:
                    self.state[s] = "booked"


class BookingService:
    HOLD_MINUTES = 5
    PRICES = {"F": 12, "A": 20}                           # row letter -> price (a PricingPolicy in full)

    def __init__(self, shows, clock):
        self.shows, self.clock, self.holds, self.n = shows, clock, {}, 0

    def hold(self, show_id, user, seats):
        self.n += 1
        hold_id = f"H{self.n}"
        if not self.shows[show_id].try_hold(seats, hold_id):
            return f"{user}: seats {seats} not available"
        self.holds[hold_id] = (show_id, user, seats, self.clock() + self.HOLD_MINUTES)
        return f"{user}: holding {seats} as {hold_id} until t={self.clock() + self.HOLD_MINUTES}"

    def confirm(self, hold_id, paid):
        if hold_id not in self.holds:
            return f"{hold_id}: expired or unknown"
        show_id, user, seats, expires = self.holds.pop(hold_id)
        if not paid:
            self.shows[show_id].release(hold_id)
            return f"{user}: payment failed, seats released"
        self.shows[show_id].book(hold_id)
        return f"{user}: booked {seats} for {sum(self.PRICES[s[0]] for s in seats)}"

    def expire_holds(self):                                # run every minute by a scheduler
        for hold_id, (show_id, user, seats, expires) in list(self.holds.items()):
            if self.clock() >= expires:
                del self.holds[hold_id]
                self.shows[show_id].release(hold_id)
                print(f"  t={self.clock()}: {hold_id} for {user} expired")


now = [0]
svc = BookingService({"S1": Show("S1", ["F7", "F8", "F9", "A1"])}, clock=lambda: now[0])
print(svc.hold("S1", "ana", ["F7", "F8"]))
print(svc.hold("S1", "ben", ["F8", "F9"]))           # loses the race for F8
print(svc.hold("S1", "ben", ["F9", "A1"]))
now[0] = 6
svc.expire_holds()                                    # ana didn't pay in time
print(svc.confirm("H1", paid=True))
print(svc.hold("S1", "cy", ["F7", "F8"]))
print(svc.confirm("H4", paid=True))
print("seat map:", svc.shows["S1"].state)
output
ana: holding ['F7', 'F8'] as H1 until t=5
ben: seats ['F8', 'F9'] not available
ben: holding ['F9', 'A1'] as H3 until t=5
  t=6: H1 for ana expired
  t=6: H3 for ben expired
H1: expired or unknown
cy: holding ['F7', 'F8'] as H4 until t=11
cy: booked ['F7', 'F8'] for 24
seat map: {'F7': 'booked', 'F8': 'booked', 'F9': 'available', 'A1': 'available'}

Run and edit it in the app.

Ben's second hold (H3) was never confirmed, so it also expired at t=6 — holds must always end, one way or another.

Watch it

Why must "check the seats are free" and "mark them held" happen in one atomic step?

A user selects seats F7 and F8, but F8 was just taken. What should hold() do?

Extensions and trade-offs

  • Database-level locking — in production the seat state lives in a database: conditional updates or SELECT … FOR UPDATE per show; a unique constraint on (show, seat) for bookings is the final safety net.
  • Expiry — a scheduled job, or a key with a TTL in a cache; also check expiry on confirm (a late payment must not book a released seat).
  • Hot shows — queue users before the seat map opens; shard by show.
  • Pricing — weekend, premium, dynamic → a PricingPolicy Strategy.
  • Cancellations and refunds — a booking state machine of its own (State).
  • See also Idempotency and retries for safe payment confirmation.

Key terms

  • Seat hold — A temporary, expiring reservation that blocks others while a user pays.
  • Double booking — Two confirmed bookings for the same seat; the failure the design must make impossible.
  • Check-then-act race — A bug where state changes between checking a condition and acting on it.
  • All-or-nothing — A multi-seat request either reserves every seat or none.