Movie Ticket Booking (LLD)#
Problem statement (interviewer prompt)
Design the seat-booking domain for a cinema / multiplex (BookMyShow-LLD-style). Model cinemas, screens, shows, seat categories, seat-hold-then-pay flow, payments, ticket generation, and refunds - focusing on the OO classes + state machine.
classDiagram
class Theater
class Hall
class Show
class Seat
class Booking
class Payment
Theater "1" *-- "many" Hall
Hall "1" *-- "many" Show
Show "1" *-- "many" Seat
Show "1" -- "many" Booking
Booking --> Payment
classDiagram
direction TB
class Theater {
+id
+name
+city
+halls: List~Hall~
}
class Hall {
+id
+name
+rows
+shows: List~Show~
}
class Movie {
+id
+title
+duration
+genre
+lang
+rating
}
class Show {
+id
+movie: Movie
+start
+end
+hall: Hall
+seats: List~ShowSeat~
+pricing: PricingPolicy
}
class Seat {
+id
+row
+col
+type
}
class ShowSeat {
+show: Show
+seat: Seat
+state: SeatState
+heldBy
+heldUntil
}
class SeatState {
<<enumeration>>
AVAILABLE
HELD
BOOKED
}
class User
class Booking {
+id
+user: User
+show: Show
+seats: List~ShowSeat~
+totals
+status: BookingState
+idempotencyKey
}
class BookingState {
<<enumeration>>
PENDING
PAYMENT_INIT
CONFIRMED
CANCELLED
FAILED
}
class PaymentService
class PricingPolicy {
<<interface>>
+price(seat, show) Money
}
class HoldService {
+hold(seats, user, ttl)
+release(seats)
}
class BookingSaga
Theater *-- Hall
Hall *-- Show
Show *-- "many" Seat
Show *-- "many" ShowSeat
Show --> PricingPolicy
User -- Booking
Booking *-- "many" ShowSeat
Booking --> BookingState
ShowSeat --> SeatState
BookingSaga ..> HoldService
BookingSaga ..> PaymentService
Detailed class model with multiplicities#
The expanded model below names the actors that show up in a real cinema chain: BookingService orchestrates the booking flow; Cinema owns many Auditoriums (also called screens or halls); each Auditorium runs a stream of Shows through the day; each Show materialises one Seat row per physical seat with a category attached. The seat categories are encoded as a SeatType enum that drives both pricing and the seat-map UI. Booking carries its state machine inline; Payment is a separate aggregate so the gateway lifecycle does not couple to the booking lifecycle.
classDiagram
direction TB
class BookingService {
+selectSeats(showId, seatIds, customerId) HoldToken
+confirmBooking(holdToken, paymentInfo) Booking
+cancelBooking(bookingId)
}
class Cinema {
+id
+name
+city
+auditoriums: List~Auditorium~
}
class Auditorium {
+id
+name
+rows
+cols
+format
}
class Show {
+id
+movie
+auditorium: Auditorium
+startTime
+endTime
+pricingStrategy: PricingStrategy
}
class Seat {
+id
+row
+col
+type: SeatType
+basePrice: Money
}
class SeatType {
<<enumeration>>
REGULAR
PREMIUM
RECLINER
}
class Booking {
+id
+customer: Customer
+show: Show
+seats: List~Seat~
+total: Money
+state: BookingState
+idempotencyKey
}
class BookingState {
<<enumeration>>
HELD
CONFIRMED
CHECKED_IN
CANCELLED
REFUNDED
}
class Payment {
+id
+amount: Money
+method
+status
+authorizedAt
}
class Customer {
+id
+name
+email
+phone
}
class PricingStrategy {
<<interface>>
+price(show, seats) Money
}
class Notifier {
<<interface>>
+notify(event)
}
BookingService ..> Booking
BookingService ..> Payment
Cinema "1" *-- "many" Auditorium
Auditorium "1" *-- "many" Show
Show "1" *-- "many" Seat
Show "1" --> "1" PricingStrategy
Booking "many" --> "1" Customer
Booking "many" --> "1" Show
Booking "1" *-- "many" Seat
Booking "1" --> "1" Payment
Booking --> BookingState
Seat --> SeatType
BookingService ..> Notifier
Multiplicities make the inventory math explicit. One Cinema owns many Auditoriums. Each Auditorium schedules many Shows. Each Show materialises its own row per physical seat (a Seat row per show, not a shared row across shows) so that holds and bookings on the 7pm show do not collide with the 10pm show in the same hall. A Booking belongs to exactly one Customer and one Show, references many Seats, and points at exactly one Payment outcome.
Seat locking flow in Java#
The seat-locking flow has three operations on the critical path: selectSeats acquires a short TTL distributed lock for the chosen seats, confirmBooking validates the lock is still held, takes payment, persists the booking, and finally releases the lock. The Redis SETNX per seat key gives us atomic "first writer wins" semantics across replicas; the 8-minute TTL guarantees abandoned carts never wedge inventory.
public class BookingService {
private final SeatLockManager locks; // Redis-backed SETNX + TTL
private final BookingRepository bookings;
private final PaymentGateway payments;
private final EventBus bus;
private static final Duration HOLD_TTL = Duration.ofMinutes(8);
public HoldToken selectSeats(long showId, List<Long> seatIds, long customerId) {
String token = UUID.randomUUID().toString();
List<Long> acquired = new ArrayList<>();
for (Long seatId : seatIds) {
String key = "lock:show:" + showId + ":seat:" + seatId;
if (locks.tryAcquire(key, token, HOLD_TTL)) {
acquired.add(seatId);
} else {
acquired.forEach(s -> locks.release("lock:show:" + showId + ":seat:" + s, token));
throw new SeatUnavailable(seatId);
}
}
return new HoldToken(token, showId, seatIds, customerId, Instant.now().plus(HOLD_TTL));
}
@Transactional
public Booking confirmBooking(HoldToken hold, PaymentRequest pr) {
if (!locks.validateAll(hold.lockKeys(), hold.token())) {
throw new HoldExpired(hold.token());
}
Booking b = new Booking(hold.customerId(), hold.showId(), hold.seatIds());
b.setState(BookingState.HELD);
Money total = b.quote();
PaymentResult auth = payments.charge(pr, total);
if (!auth.ok()) {
b.setState(BookingState.CANCELLED);
bookings.save(b);
hold.lockKeys().forEach(k -> locks.release(k, hold.token()));
throw new PaymentFailed(auth.code());
}
b.attachPayment(auth.paymentId());
b.setState(BookingState.CONFIRMED);
bookings.save(b);
hold.lockKeys().forEach(k -> locks.release(k, hold.token()));
bus.publish(new BookingConfirmed(b.getId(), b.getCustomerId()));
return b;
}
}
SETNX with a token value (not just 1) lets release verify the holder before deleting, which avoids the classic "TTL expired, another client acquired, now I delete their lock" race. On payment failure the seats are released immediately so the cinema map repopulates without waiting for the TTL.
Design patterns used#
The booking domain layers cleanly onto five behavioural and creational patterns. Each isolates a dimension that varies independently of the booking critical path.
- State (Booking lifecycle):
BookingStatemodels the legal transitionsHELD -> CONFIRMED -> CHECKED_IN -> CANCELLED/REFUNDED. The state object owns the guard logic; you cannot check in aCANCELLEDbooking or refund one stillHELD. Centralising the state machine prevents the conditional sprawl that would otherwise leak into payment, notification, and cancellation services. - Strategy (PricingStrategy):
WeekdayPricing,WeekendPricing,ShowtimeMultiplierPricing, andGroupDiscountPricingall implementPricingStrategy.price(show, seats). The cinema picks the strategy at show creation time; the booking flow never branches on the strategy in use. New strategies slot in without touching the booking service. - Observer (notifications):
BookingConfirmed,ShowReminderDue, andBookingCancelledevents fan out through anEventBusto subscribers likeEmailNotifier,SmsNotifier,PushNotifier, andLoyaltyAccrualService. A reminder one hour before the show is one new subscriber, not an edit to the booking service. - Factory (SeatType subtypes): a
SeatFactory.create(type, row, col)returns a fully configuredSeatwith the right base price and amenity flags (cup holder, recliner motor, premium legroom). Hides the conditional that would otherwise leak into pricing, seat-map rendering, and reporting whenever a new tier (IMAX recliner, lounge pod) is added. - Command (cancellation refund chain): a cancellation is a
RefundCommandqueue: release seats, refund the gateway payment, credit loyalty points, send a cancellation email, update analytics. Each step is a command withexecuteandundo, so a partial failure can roll back the steps already executed and surface an operational alert.
Common variants & follow-ups#
Interviewers usually push beyond the single-cinema baseline to test how the design generalises and which decisions you would defer. The variants below show up in real loops.
- Multi-screen, multi-city chain: introduce a
Brandaggregate aboveCinema; cross-city search lands on a federated inventory index. Loyalty points and corporate offers roll up across cities. Discuss whether bookings are owned by the cinema or the brand. - Group booking with adjacent-seat constraint: a 6-seat booking must land on contiguous seats in the same row (or split across two adjacent rows). Model the seat-map as a 2D grid; run a sliding-window search for
kconsecutiveAVAILABLEseats before offering options to the user. - Food and ticket combo: a
Comboaggregate attaches concession items (popcorn, drinks) to the booking. Combos are priced independently and fulfilled on a different queue (concession counter pickup); failure of food fulfillment must not invalidate the ticket. - Refund-with-credit vs refund-to-card: cancellation policy offers store credit at 100 percent or card refund at 80 percent. Encoded as a
RefundStrategyattached to the rate plan, not hard-coded; loyalty members may get richer terms. - Dynamic pricing (yield management): replace the static
PricingStrategywith aYieldPricingthat reads current occupancy, days-to-show, and competitor data. Cache quotes for 30 to 120 seconds so a search session sees stable prices while the cart is open. - Assigned vs unassigned seats: some cinemas (and most festival screenings) sell first-come-first-served general admission. Model
Auditorium.seatingModeasASSIGNEDvsOPEN; in open mode the booking holds count, not specific seats, and the seat-map UI is skipped. - IMAX, 3D, and format selection:
Show.formatbecomes part of the price key; glasses-deposit handling for 3D is a folio line on the booking; IMAX upgrades are a separatePricingStrategyoverlay applied on top of the day-of-week base.
Edge cases#
These are the failure modes interviewers reliably probe; each one tests a different concurrency or domain invariant.
- Race on the same seat by two users: two clients call
selectSeatsfor the same seat at the same instant. The RedisSETNX(or equivalently a unique constraint on(show_id, seat_id, state=HELD)in Postgres) gives "first writer wins" semantics across replicas; the loser seesSeatUnavailableand the UI repaints the map. Pessimistic distributed locking is the correct call here because contention is high (popular shows) and the cost of a double-book is reputational, not just commercial. - Payment timeout while seats are locked: gateway latency spikes and the 8-minute TTL nearly expires during
confirmBooking. The lock validation right after the gateway returns must check the token is still ours; if the TTL expired and another client grabbed it, the gateway charge is reversed (refund) and the booking lands inCANCELLEDwith a clear error to the customer. Keep the TTL well above p99 gateway latency or refresh the lock once on entry. - Show cancellation policy (auto-refund all): a print fails, projector dies, or the show is pulled. The operator triggers a
ShowCancellationevent; everyCONFIRMEDbooking for that show flips toREFUNDED; refunds queue to the gateway; seats are released; customers receive an SMS plus email with rebooking credit. No human-in-the-loop per booking. - Cinema power failure mid-show: bookings already
CHECKED_INget a 100 percent rebooking credit (no refund-to-card by default; encoded asMidShowFailurePolicy); future shows for the rest of the day are auto-cancelled if the screen cannot resume within 30 minutes. The sameShowCancellationflow runs for the impacted shows. - Fraudulent booking detection: velocity rules flag a customer who books 20 seats across 5 shows in 60 seconds, or who pays with a card that fails a downstream chargeback dispute within the cancellation window. The
FraudCheckruns asynchronously after confirmation; on positive signal the booking flips toFRAUD_HOLD, the ticket QR is invalidated, and the seat goes back to the inventory for the next sale. - Partial cancellation of a multi-seat booking: a 4-seat booking where 2 attendees can no longer come. Refund 2 of the 4 seats per the cancellation policy, release those 2 seats back to inventory, recompute taxes, and keep the booking
CONFIRMEDfor the remaining 2 seats; the QR ticket is reissued with the updated seat list so theatre staff scanning at entry see the right count.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Idempotency & retries | safe re-execution, backoff + jitter | idempotency-retries |
Quick reference#
Functional#
- Browse theatres/movies/shows.
- View seat map; hold seats; pay; book.
- Cancel; refund.
Patterns#
- Saga (booking).
- Strategy (pricing).
- State (seat / booking).
- Repository (DB access).
Trade-offs#
- TTL holds prevent leaks; require sweep.
- Strong consistency on seats is mandatory.
Refs#
- See companion design
ecommerce-marketplaces/ticketmasterfor the full-system view.
FAQ#
How do you prevent two users from booking the same seat?#
Wrap the seat-hold step in a database transaction with SELECT FOR UPDATE on the seat row, or use a unique constraint on (show_id, seat_id) so only one insert can win.
How does the seat-hold-then-pay flow work?#
On select, mark seats as HELD with a short TTL (around 5 to 10 minutes); on payment success, transition to BOOKED; if the TTL expires first, release the hold automatically.
What states does a booking go through?#
PENDING (held), CONFIRMED (paid), CANCELLED (user cancelled), and REFUNDED (post-cancellation refund completed). Each transition is recorded with a timestamp.
How do you model seat categories and pricing?#
Seats belong to a SeatCategory (Standard, Premium, Recliner); Show overrides base prices per category. Booking total sums category price times selected seat count.
How are refunds handled?#
Cancellation triggers a Refund record processed asynchronously via the payment gateway; the seats flip back to AVAILABLE only after refund initiation to avoid duplicate sale.
Related Topics#
- Restaurant Reservation LLD: similar seat-locking design
- Hotel Management: shared inventory & booking patterns
- Calendar Scheduler LLD: time-slot conflict detection
- Ticketmaster (HLD): scale view of the same problem