Problem 11: Design BookMyShow / Movie Booking
The setup
"Design a movie booking system like BookMyShow. Users search shows by city, theater, movie, time. Book seats for a specific show. Handle concurrent booking — two users can't book the same seat. Process payment."
This problem tests concurrency around seat selection (the classic race condition: two users try to book the same seat) plus rich domain modelling (City → Theater → Screen → Show → Seat).
Clarifying questions
| Question | Story answer |
|---|---|
| Seat selection — assigned or general admission? | Assigned seats; user picks specific ones. |
| Number of users? | Tens of thousands concurrent; very popular shows. |
| Payment integration? | External payment gateway; assume API. |
| How long can a user "hold" seats before paying? | 10 minutes; after that, seats released. |
| Refund / cancel policy? | Allowed up to 30 min before show start; processing fee deducted. |
Core entities
class City { id, name }
class Theater { id, name, city, screens }
class Screen { id, theater, totalSeats, seatLayout }
class Movie { id, title, duration, genre, language, releaseDate }
class Show { id, screen, movie, startTime, endTime, basePrice }
class Seat { id, screen, row, number, type } // PREMIUM, NORMAL, RECLINER
class ShowSeat {
showId, seatId, status: AVAILABLE/BOOKED/HELD,
heldByUserId, heldUntil
}
class Booking { id, user, show, seats, totalPrice, status, createdAt }
class Payment { booking, amount, status, gateway, transactionId }
Design Decision 1: handling the concurrent-seat-selection problem
Two users see seat A1 available; both try to book it. One must succeed; the other must fail gracefully.
| Approach | How | Tradeoff |
|---|---|---|
| Optimistic locking (version-based) | Each ShowSeat has version; UPDATE specifies version; failure = race lost | Cheap; user has to retry on conflict |
| Pessimistic locking (SELECT FOR UPDATE) | Lock the seat row during the booking transaction | Strict; can cause lock contention on popular shows |
| Two-phase: hold then book | HELD state for 10 min; only HELD users can finalize | Best UX; users have time to pay; clear conflict semantics |
Production: two-phase. Hold seats with a TTL, finalize on payment.
class BookingService {
public Booking holdSeats(User user, Show show, List seats) {
Booking booking = new Booking(user, show, seats);
Instant heldUntil = Instant.now().plusSeconds(600); // 10 min hold
// Atomically update ShowSeat rows from AVAILABLE → HELD
for (Seat seat : seats) {
int updated = db.executeUpdate(
"UPDATE show_seats SET status='HELD', held_by=?, held_until=? " +
"WHERE show_id=? AND seat_id=? AND status='AVAILABLE'",
user.id, heldUntil, show.id, seat.id);
if (updated == 0) {
// Seat no longer available
rollbackHolds(booking);
throw new SeatUnavailableException(seat);
}
}
booking.status = BookingStatus.HELD;
bookingRepo.save(booking);
return booking;
}
public Booking finalizeBooking(Booking booking, PaymentMethod paymentMethod) {
if (booking.status != BookingStatus.HELD) {
throw new InvalidBookingException();
}
if (Instant.now().isAfter(booking.heldUntil)) {
throw new BookingExpiredException();
}
// Process payment
PaymentResult result = paymentProcessor.process(paymentMethod, booking.totalPrice);
if (!result.isSuccess()) {
rollbackHolds(booking);
throw new PaymentFailedException();
}
// Confirm seats
for (Seat seat : booking.seats) {
db.executeUpdate(
"UPDATE show_seats SET status='BOOKED', held_by=NULL, held_until=NULL " +
"WHERE show_id=? AND seat_id=?",
booking.show.id, seat.id);
}
booking.status = BookingStatus.CONFIRMED;
bookingRepo.save(booking);
return booking;
}
}
Design Decision 2: cleanup expired holds
If a user holds seats but doesn't pay within 10 min, the holds must expire and the seats become available again.
Two approaches:
- Lazy: when checking seat availability, also include "HELD seats whose held_until < now" as available. Doesn't physically clean up, but logically correct.
- Active: background job runs every minute, finds expired holds, resets them to AVAILABLE.
Production uses both: lazy check in the hot path, active cleanup for housekeeping.
Cross-examination round 1: peak load
Takeaway
BookMyShow combines domain modelling with concurrency. The two-phase hold-then-book pattern is the canonical solution to the seat-selection race. The interview test is whether you can identify the race condition unprompted, propose the right solution, and reason about its tradeoffs.