Movie Ticket Booking
Design a movie ticket booking system: theaters with multiple screens, movies showing at scheduled times, seat selection with temporary holds during checkout, and payment that confirms the booking.
Clarifying questions
- Single theater chain or a marketplace across chains?
- How long is the seat-hold TTL (Time To Live), and what happens if payment is slow?
- Are seats tiered by price (standard, premium, recliner)?
- Do we model concessions in this iteration?
- Are we doing assigned seating only, or also general admission?
Core requirements
- Browse movies and showtimes by city, theater, and date.
- View seat map for a specific show with real-time availability.
- Hold selected seats with a TTL during checkout.
- Charge payment and confirm booking atomically with the hold.
- Release holds on TTL expiry or explicit cancel.
- Issue a confirmed ticket with seats, show, and barcode.
Canonical class breakdown
Movie — Film catalog entry: title, duration, rating, language; independent of any showing.
Theater — Physical venue that owns one or more Screens and a location.
Screen — Auditorium with a fixed seat layout; hosts Shows over time.
Show — A specific Movie on a specific Screen at a specific time; owns the per-seat availability for that showing.
Seat — Position in a Screen's layout with a tier (standard/premium); price varies by Show.
SeatHold — Time-bound reservation against a set of seats on a Show, owned by a user session, with an expiry.
Booking — Confirmed transaction created from a SeatHold after successful payment; immutable.
BookingService — Orchestrates seat-select -> hold -> pay -> confirm; the only path that turns a hold into a Booking.
PaymentGateway (interface) — Pluggable payment provider with idempotent charge for retry safety.
Java sketchjava
// Java skeleton
enum SeatTier { STANDARD, PREMIUM, RECLINER }
enum SeatStatus { AVAILABLE, HELD, BOOKED }
enum BookingStatus { CONFIRMED, CANCELLED, REFUNDED }
class Movie {
private final String movieId;
private final String title;
private final int durationMinutes;
private final String rating;
private final String language;
}
class Seat {
private final String seatId;
private final int row;
private final int col;
private final SeatTier tier;
}
class Screen {
private final String screenId;
private final List<Seat> layout;
}
class Theater {
private final String theaterId;
private final String name;
private final String city;
private final List<Screen> screens;
}
class Show {
private final String showId;
private final Movie movie;
private final Screen screen;
private final Instant startTime;
private final Map<SeatTier, BigDecimal> priceByTier;
private final Map<String, SeatStatus> seatStatus; // contended resource
public boolean tryHold(Set<String> seatIds) { /* atomic CAS over the map */ }
public void release(Set<String> seatIds) { ... }
public void confirm(Set<String> seatIds) { ... }
}
class SeatHold {
private final String holdId;
private final Show show;
private final Set<String> seatIds;
private final String userId;
private final Instant expiresAt;
public boolean isExpired() { ... }
}
class Booking {
private final String bookingId;
private final Show show;
private final Set<String> seatIds;
private final String userId;
private final BigDecimal totalCharged;
private final String barcode;
private BookingStatus status;
}
interface PaymentGateway {
PaymentResult charge(String idempotencyKey, BigDecimal amount, String userId);
}
class BookingService {
private final PaymentGateway gateway;
private final Map<String, SeatHold> activeHolds;
public SeatHold hold(Show show, Set<String> seatIds, String userId, Duration ttl) { ... }
public Booking confirm(SeatHold hold, String paymentToken) { /* charge then convert */ }
public void releaseExpired(Instant now) { ... }
}
Key decisions to defend
- SeatHold lives on the Show (the contended resource), not on the user — that's where the lock must be.
- Hold TTL is the entire concurrency story: short enough to free abandoned carts, long enough to let payment land.
- Use atomic check-and-set on the per-show seat map to create holds; a list of holds with scans does not scale.
- Booking is immutable and created only via BookingService; never let the client mutate hold -> booking directly.
- Pricing lives on (Show, SeatTier), not on Seat — same physical seat costs different amounts on Friday night.
Likely follow-ups
- · Two users tap 'pay' for overlapping seats simultaneously — walk me through the race.
- · Refunds and seat-swap — how do you keep the seat map consistent?
- · Pre-sale a blockbuster with 10k concurrent requests for the same show — what breaks?
Dive deeper
Movie booking is the canonical hold-with-TTL design question. The whole problem hinges on where the lock lives (the Show) and how you make the hold atomic. Candidates who put holds on the user or scan a list of holds fail under any real load. The deeper test is whether you treat the hold-then-confirm flow as a state machine with explicit expiry, rather than a sequence of optimistic operations that hope nothing else happened in between.