← Back to list

Designing BookMyShow: A Deep Dive into Seat Booking, Locking, and Concurrency

Designing a movie ticket booking system like BookMyShow looks simple at first. A user selects a movie, chooses a show, picks seats, makes…

Sakshi Gaur · 2026-05-17 07:34 · 0 claps · 7.9 min read
#system-design-interview #lld #bookmyshow
Open on Medium ↗
Wiki topics: 🎬 · Film & Television

Designing BookMyShow: A Deep Dive into Seat Booking, Locking, and Concurrency

Designing a movie ticket booking system like BookMyShow looks simple at first. A user selects a movie, chooses a show, picks seats, makes payment, and receives a ticket.

But the real complexity starts when thousands of users try to book the same popular show at the same time.

The most important question is:

How do we ensure that the same seat is never booked by two users?

This article explains a low-level design of BookMyShow with a strong focus on concurrency, seat locking, payment handling, and failure scenarios.

1. Understanding the Core Flow

A typical booking flow looks like this:

  1. User searches for a movie.
  2. User selects a city and theatre.
  3. User chooses a show.
  4. User views available seats.
  5. User selects seats.
  6. System temporarily locks those seats.
  7. User completes payment.
  8. On successful payment, booking is confirmed.
  9. If payment fails or times out, seats are released.

At a high level, this is simple.

But internally, the system must carefully manage seat state transitions.

2. Important Entities

The core entities in the system are:

User
Movie
City
Theatre
Screen
Seat
Show
ShowSeat
Booking
Payment

The most important entity is ShowSeat.

A physical seat belongs to a screen.

For example:

Screen 1 -> Seat A1

But the same physical seat can be reused across multiple shows.

So we need a separate entity called ShowSeat.

Example:

Seat A1 for 10 AM show
Seat A1 for 2 PM show
Seat A1 for 7 PM show

Each one has its own availability status.

3. Why ShowSeat Is Important

A common mistake is to store availability directly in the Seat table.

That is incorrect.

A seat is physical.

Availability is show-specific.

So we should model it like this:

Seat = physical seat
ShowSeat = seat availability for a specific show

Example:

Seat A1 can be booked for 10 AM
Seat A1 can still be available for 2 PM
Seat A1 can be locked for 7 PM

This separation keeps the design clean and scalable.

4. Seat State Machine

Every show seat can be in one of these states:

AVAILABLE
LOCKED
BOOKED

The valid flow is:

AVAILABLE -> LOCKED -> BOOKED

If payment fails or user does not pay within time:

LOCKED -> AVAILABLE

A booked seat should not become available again unless there is a cancellation flow.

So the core transition is:

AVAILABLE
   |
   v
LOCKED
   |
   v
BOOKED

And timeout flow:

LOCKED
   |
   v
AVAILABLE

5. Database Design

The most important table is show_seats.

CREATE TABLE show_seats (
    show_seat_id BIGSERIAL PRIMARY KEY,
    show_id BIGINT NOT NULL,
    seat_id BIGINT NOT NULL,
    status VARCHAR(30) NOT NULL,
    locked_by BIGINT,
    lock_id VARCHAR(100),
    locked_until TIMESTAMP,
    price NUMERIC(10,2),
    version INT NOT NULL DEFAULT 0,
    UNIQUE(show_id, seat_id)
);

The unique constraint is very important:

UNIQUE(show_id, seat_id)

It ensures that for a given show, a seat appears only once.

6. Booking Table

CREATE TABLE bookings (
    booking_id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    show_id BIGINT NOT NULL,
    status VARCHAR(30) NOT NULL,
    total_amount NUMERIC(10,2),
    idempotency_key VARCHAR(100) UNIQUE,
    created_at TIMESTAMP DEFAULT now(),
    updated_at TIMESTAMP DEFAULT now()
);

Booking states can be:

INITIATED
PENDING_PAYMENT
CONFIRMED
FAILED
EXPIRED
CANCELLED

7. Why Seat Locking Is Needed

Suppose two users select the same seat at the same time.

User A selects A1
User B selects A1

Without proper locking, both may see the seat as available and both may try to book it.

This leads to double booking.

So before payment, the system should temporarily lock the seat.

The lock gives the user a short time window, usually 5 minutes, to complete payment.

8. Seat Locking Flow

When a user selects seats and clicks continue:

  1. System receives seat lock request.
  2. System checks idempotency key.
  3. System creates booking in INITIATED state.
  4. System tries to lock all selected seats.
  5. If all seats are locked successfully, booking moves to PENDING_PAYMENT.
  6. If any seat is unavailable, entire transaction rolls back.
  7. User is redirected to payment page.

9. Atomic Conditional Update

The safest way to lock a seat is using an atomic database update.

UPDATE show_seats
SET status = 'LOCKED',
    locked_by = :userId,
    lock_id = :lockId,
    locked_until = :expiryTime,
    version = version + 1
WHERE show_id = :showId
  AND seat_id = :seatId
  AND status = 'AVAILABLE';

This query succeeds only if the seat is currently available.

If two users run this query at the same time, only one will update the row successfully.

The other request will get update count as 0.

That means the seat is already locked or booked.

This is the heart of the concurrency design.

10. Locking Multiple Seats

Users usually book multiple seats together.

Example:

A1, A2, A3

The requirement is:

Either all seats should be locked, or none should be locked.

This should be done inside a database transaction.

If locking any seat fails, the transaction should roll back.

@Transactional
public Booking lockSeats(Long userId, Long showId, List<Long> seatIds, String idempotencyKey) {
    Optional<Booking> existing = bookingRepo.findByIdempotencyKey(idempotencyKey);
    if (existing.isPresent()) {
        return existing.get();
    }
    Booking booking = bookingRepo.create(userId, showId, "INITIATED", idempotencyKey);
    String lockId = UUID.randomUUID().toString();
    LocalDateTime expiry = LocalDateTime.now().plusMinutes(5);
    for (Long seatId : seatIds) {
        int updated = showSeatRepo.lockSeat(showId, seatId, userId, lockId, expiry);
        if (updated == 0) {
            throw new SeatAlreadyLockedException();
        }
    }
    bookingRepo.updateStatus(booking.getBookingId(), "PENDING_PAYMENT");
    return booking;
}

If any seat fails, the exception rolls back the whole transaction.

11. Why Java Locks Are Not Enough

We should not rely only on:

synchronized
ReentrantLock
ConcurrentHashMap

These work only inside one JVM.

But in production, there will be multiple application servers.

App Server 1
App Server 2
App Server 3

A Java lock on App Server 1 does not protect App Server 2.

So the final concurrency control must happen at a shared layer like:

Database
Redis
Distributed lock

For this design, the database atomic update is the source of truth.

12. Optimistic Locking

Another option is optimistic locking.

We keep a version column in show_seats.

UPDATE show_seats
SET status = 'LOCKED',
    version = version + 1
WHERE show_id = :showId
  AND seat_id = :seatId
  AND status = 'AVAILABLE'
  AND version = :oldVersion;

If another transaction changed the row, the version will not match and update will fail.

Optimistic locking is useful when:

Reads are high
Contention is moderate
We want non-blocking behavior
Retries are acceptable

13. Pessimistic Locking

Pessimistic locking means we explicitly lock rows before updating.

SELECT *
FROM show_seats
WHERE show_id = :showId
  AND seat_id IN (:seatIds)
FOR UPDATE;

This blocks other transactions from modifying those rows.

It is useful under high contention, but it has disadvantages:

More waiting
Lower throughput
Possible deadlocks

For BookMyShow, atomic conditional update is usually cleaner.

14. Redis Lock: Do We Need It?

Redis can be used as a fast first layer.

Example:

SET seat_lock:5001:A1 lock_123 NX EX 300

This means:

Lock this seat only if key does not already exist.
Expire the lock after 5 minutes.

But Redis should not be the only source of truth.

The final correctness should still be protected by the database.

A good interview answer is:

Redis can reduce database load during high traffic, but the database conditional update remains the final guard against double booking.

15. Payment Flow

Once seats are locked, the user is redirected to payment.

Payment can succeed, fail, timeout, or send duplicate callbacks.

So payment confirmation must also be idempotent.

On payment success:

UPDATE show_seats
SET status = 'BOOKED',
    version = version + 1
WHERE lock_id = :lockId
  AND locked_by = :userId
  AND status = 'LOCKED'
  AND locked_until > now();

This confirms only seats that are:

Currently locked
Locked by the same user
Not expired

16. What If Payment Succeeds After Lock Expiry?

This is a very common interview follow-up.

Suppose the lock expires after 5 minutes.

At 5 minutes 5 seconds, payment success callback comes.

Should we confirm the booking?

No.

Before confirming, we check:

AND locked_until > now()

If the lock expired, booking should not be confirmed.

The system can then trigger refund or mark payment as refund pending.

17. Expiry Worker

If user does not complete payment within 5 minutes, locked seats should be released.

A background worker can run every few seconds.

UPDATE show_seats
SET status = 'AVAILABLE',
    locked_by = NULL,
    lock_id = NULL,
    locked_until = NULL,
    version = version + 1
WHERE status = 'LOCKED'
  AND locked_until < now();

It also updates booking status:

UPDATE bookings
SET status = 'EXPIRED'
WHERE status = 'PENDING_PAYMENT'
  AND created_at < now() - interval '5 minutes';

This ensures locked seats do not remain blocked forever.

18. Race Condition: Payment vs Expiry Worker

One difficult case is:

Payment success callback arrives
Expiry worker also runs at the same time

Both may try to update the same booking.

The solution is to use conditional state updates.

Payment confirmation:

UPDATE show_seats
SET status = 'BOOKED'
WHERE lock_id = :lockId
  AND status = 'LOCKED'
  AND locked_until > now();

Expiry worker:

UPDATE show_seats
SET status = 'AVAILABLE'
WHERE lock_id = :lockId
  AND status = 'LOCKED'
  AND locked_until < now();

Only one can succeed.

This prevents inconsistent state.

19. Idempotency

Idempotency is extremely important in this system.

The user may double-click.

The browser may retry.

The payment gateway may send duplicate callbacks.

So we use:

X-Idempotency-Key

For booking:

idempotency_key VARCHAR(100) UNIQUE

For payment:

gateway_reference_id VARCHAR(100) UNIQUE

If the same request comes again, we return the already created booking or payment result.

We do not create duplicate bookings.

20. Full Booking Flow

The complete flow is:

1. User selects movie.
2. User selects show.
3. User opens seat layout.
4. User selects seats.
5. Booking service receives lock request.
6. Booking service checks idempotency key.
7. Booking service creates booking in INITIATED state.
8. Booking service locks selected seats using atomic DB update.
9. If any seat fails, transaction rolls back.
10. If all seats lock, booking becomes PENDING_PAYMENT.
11. User is redirected to payment.
12. Payment gateway processes payment.
13. Gateway sends callback.
14. Payment service verifies callback.
15. Booking service checks booking state and lock expiry.
16. If valid, seats move from LOCKED to BOOKED.
17. Booking becomes CONFIRMED.
18. Notification is sent to user.
19. If payment fails or timeout happens, seats are released.

21. High-Level Flow Diagram

User selects seats
        |
        v
Booking Service
        |
        v
Check idempotency key
        |
        v
Create booking INITIATED
        |
        v
Lock seats using atomic DB update
        |
        v
All seats locked?
   /             \
 No               Yes
 |                 |
Rollback        Booking PENDING_PAYMENT
 |                 |
Return error     Redirect to payment
                   |
                   v
             Payment callback
                   |
                   v
           Payment success?
             /          \
           No            Yes
           |              |
      Release seats    Confirm seats
           |              |
      Booking failed   Booking confirmed

22. Important Design Patterns

State Pattern

Booking and seat status transitions should be controlled.

AVAILABLE -> LOCKED -> BOOKED
PENDING_PAYMENT -> CONFIRMED
PENDING_PAYMENT -> EXPIRED

Strategy Pattern

Payment providers can be implemented using strategy pattern.

interface PaymentStrategy {
    PaymentResponse pay(PaymentRequest request);
}

Implementations:

RazorpayPaymentStrategy
StripePaymentStrategy
PaytmPaymentStrategy

Repository Pattern

Repositories hide database logic.

BookingRepository
ShowSeatRepository
PaymentRepository

Observer Pattern

Once booking is confirmed, notification can be sent asynchronously.

BookingConfirmedEvent -> Email/SMS/WhatsApp

23. Common Interview Follow-Ups

How do you prevent double booking?

Use atomic conditional update:

UPDATE show_seats
SET status = 'LOCKED'
WHERE show_id = ?
  AND seat_id = ?
  AND status = 'AVAILABLE';

Only one request can update successfully.

What if two users select the same seat?

Both may try, but only one update succeeds.

The second user gets seat unavailable.

What if payment succeeds after expiry?

Do not confirm booking.

Check lock expiry before confirming.

If expired, trigger refund.

What if payment callback comes twice?

Use unique gateway reference ID or payment idempotency key.

Return already processed result.

What if app crashes after locking seats?

Expiry worker releases locked seats after timeout.

What isolation level is needed?

Usually READ COMMITTED is enough with atomic conditional updates.

SERIALIZABLE is safer but often overkill and reduces throughput.

How do you avoid deadlocks?

Always process selected seat IDs in sorted order.

Collections.sort(seatIds);

This prevents circular waits.

25. Final Thoughts

BookMyShow is not difficult because of entities like movie, theatre, and screen.

The real difficulty is seat consistency.

The interviewer is mainly checking whether we can handle:

Double booking
Concurrent seat selection
Payment timeout
Payment callback duplication
Lock expiry
Race between payment and expiry worker
Idempotency
Distributed app servers

A good design should keep the database as the final source of truth and use conditional updates to protect seat state transitions.

Redis can improve performance, but correctness should not depend only on Redis.

The key principle is:

A seat should move from AVAILABLE to LOCKED to BOOKED using controlled, atomic, and idempotent state transitions.

That is the foundation of a reliable ticket booking system.


메타데이터
post_id
dfc64f39bae9
slug
designing-bookmyshow-a-deep-dive-into-seat-booking-locking-and-concurrency-dfc64f39bae9
url
https://medium.com/@sakshigaur_74312/designing-bookmyshow-a-deep-dive-into-seat-booking-locking-and-concurrency-dfc64f39bae9
canonical_url
https://medium.com/@sakshigaur_74312/designing-bookmyshow-a-deep-dive-into-seat-booking-locking-and-concurrency-dfc64f39bae9
author_url
https://medium.com/@sakshigaur_74312
status
ok
fetched_at
2026-06-21 15:33:18