Hirestack
Hirestack
Chapter 15

Problem 11: Design BookMyShow / Movie Booking

📖 3 min read · 7 sections · May 2026
Asked at: BookMyShow Flipkart Swiggy Razorpay

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

QuestionStory 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.

ApproachHowTradeoff
Optimistic locking (version-based)Each ShowSeat has version; UPDATE specifies version; failure = race lostCheap; user has to retry on conflict
Pessimistic locking (SELECT FOR UPDATE)Lock the seat row during the booking transactionStrict; can cause lock contention on popular shows
Two-phase: hold then bookHELD state for 10 min; only HELD users can finalizeBest 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:

  1. Lazy: when checking seat availability, also include "HELD seats whose held_until < now" as available. Doesn't physically clean up, but logically correct.
  2. 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

Interviewer: Avengers premiere; 10K users hammer the seat selection at the same moment. What happens?
Database becomes the bottleneck. Each hold attempt is one update with a WHERE clause. 10K simultaneous updates on the show_seats table create lock contention. Mitigations: (1) partition show_seats by show_id; popular shows on dedicated DB resources. (2) Use Redis as a "hold registry" — atomic SET if absent (NX) operations are faster than DB row updates. (3) Queue user requests through a per-show ordered queue; serialise within a show.

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.