Problem 4.9 — BookMyShow (Ticket Booking)
In one line
Seat locking, show/seat modeling, pricing by category, and the invalidation of stale holds — a Level-3 problem that combines LLD and concurrency.
Think of it like this
Booking a movie ticket online. When you pick seats, they turn orange (held) for a few minutes while you pay; if you don't pay in time, they go back to grey (available) for someone else to grab. The hard part is making sure two people can never both hold the same seat.
Key ideas
- 01
Entities: Movie, Show (movie × screen × time), Seat, Booking, Payment, Screen, City/Theatre.
- 02
Seat states: AVAILABLE / HELD (locked for user) / BOOKED / BLOCKED (aisle, maintenance).
- 03
Two-phase flow: hold seats (TTL lock) → pay → confirm. Expired holds release back to AVAILABLE.
- 04
Concurrency: two users choosing the same seat — the hold must be atomic; DB row-lock or optimistic version.
- 05
Pricing: base × category (gold/silver/platinum) × day/showtime — strategy/decorator.
- 06
Booking expiry: ScheduledExecutorService or DB expiry job releases stale holds.
- 07
Design the flow: BrowseShowtimes → SelectSeats → Hold → Payment → Ticket+Confirmation. Every step has a failure mode.
Java / Spring map
- →
SeatHold with TTL; SELECT ... FOR UPDATE on the seat row in a @Transactional hold service.
Code & diagrams
The seat's own state diagram — the entire concurrency story in one picture.
The atomic hold: exactly one of two concurrent users gets the seat, and holds expire automatically.
public enum SeatStatus { AVAILABLE, HELD, BOOKED, BLOCKED }
public final class Seat {
private final String id;
private volatile SeatStatus status = SeatStatus.AVAILABLE;
private String heldBy;
private Instant holdExpiresAt;
public Seat(String id) { this.id = id; }
// Atomic claim — the same shape as ParkingSpot.tryOccupy() and BookCopy.checkout().
public synchronized boolean tryHold(String userId, Duration ttl, Instant now) {
releaseIfExpired(now);
if (status != SeatStatus.AVAILABLE) return false;
status = SeatStatus.HELD;
heldBy = userId;
holdExpiresAt = now.plus(ttl);
return true;
}
public synchronized boolean confirm(String userId, Instant now) {
releaseIfExpired(now);
if (status != SeatStatus.HELD || !heldBy.equals(userId)) return false;
status = SeatStatus.BOOKED;
return true;
}
private void releaseIfExpired(Instant now) {
if (status == SeatStatus.HELD && now.isAfter(holdExpiresAt)) {
status = SeatStatus.AVAILABLE; // stale hold self-heals — no sweeper needed to be CORRECT,
heldBy = null; // though a sweeper job frees memory/rows sooner
}
}
}
// Equivalent DB-level guarantee for a multi-instance deployment (no shared JVM memory):
// UPDATE seats SET status='HELD', held_by=?, hold_expires_at=?
// WHERE id = ? AND (status = 'AVAILABLE' OR (status = 'HELD' AND hold_expires_at < NOW()));
// -- if rowsAffected == 0, someone else holds it or it's already booked.Explain without notes
What happens to the seat when the user abandons mid-payment — who releases it and after how long?
Practice
Model holds with expiry timestamps + a sweeper job, and write the SQL you'd use for atomic seat claim.
Trade-offs
- ↔
Short holds = lost bookings under slow payers; long holds = empty theatre risk. 5–10 minutes is the industry answer.
Completion checklist
I can describe the seat lifecycle with its timers and concurrency guard.