Skip to main content
Create your own
Lesson illustration

Building a Movie Ticket Booking System

Hello! Welcome to our first LLD case study on booking and entertainment systems. In the previous module, we tackled utility and service systems like the parking lot, library, elevator, and ATM. You've applied key design patterns like State, Strategy, and Chain of Responsibility to build robust, stateful, and extensible systems.

Today, we'll dive into one of the most classic LLD interview questions: designing a movie ticket booking system. While it shares some similarities with other LLD problems, its primary challenge lies in correctly handling seat selection and, most critically, concurrency. This problem is a fantastic test of your ability to design a thread-safe system that prevents common issues like double-booking.

Your learning outcome for this lesson is to design a movie ticket booking system, addressing seat selection and concurrency. We will follow the structured design process we've used before, focusing intensely on the concurrency aspect which is central to this problem.

Step 1: Clarify Requirements and Use Cases

As always, we begin with a vague prompt: "Design a movie ticket booking system." Our first job is to act like a senior engineer and clarify the exact scope and requirements. The goal is to avoid ambiguity and ensure we're solving the right problem.

To see how this dialogue unfolds in an interview, let's consult an excellent guide that uses this very example.

How to Approach LLD Problems

The article 'How to Approach LLD Problems' from codeWithAryan provides a great script for an initial conversation with an interviewer. It shows how to confirm your understanding and ask targeted questions to define the scope.

Please read Section 1, 'Clarify Requirements and Use Cases'. Pay close attention to the dialogue between the Interviewee and Interviewer and the key points that are established.

Based on that structured conversation, let's crystallize our requirements:

  • Core Functionality:
    1. Search: Users can search for movies available in a specific city.
    2. Browse: Users can see a list of theaters and the shows (movie, screen, time) for a selected movie.
    3. Seat Selection: Users can view the layout of available seats for a specific show and select one or more seats.
    4. Booking: The system must lock the selected seats for a limited time to allow the user to complete payment. A successful payment confirms the booking.
  • The Critical Challenge: Concurrency
    • The system must handle multiple users trying to book the same seats for the same show simultaneously. It must prevent race conditions and ensure that a seat is sold to only one user. This is the heart of the problem.
  • Assumptions and Scope:
    • We will focus on the user-facing booking flow. Admin functions like adding movies, theaters, or shows are out of scope but can be discussed as extensions.
    • We'll assume user authentication is already handled.
    • Payment gateway integration will be abstracted behind a simple interface.

Step 2: Identify Core Entities and Relationships

With clear requirements, we can identify our primary domain objects. This involves picking out the key "nouns" from our use cases.

The core entities for a movie booking system are quite intuitive:

  • User: The person booking the ticket.
  • Movie: The film being shown.
  • Theater: The venue, which contains multiple screens.
  • Screen: A specific auditorium within a theater where a movie is shown.
  • Show: A specific screening of a Movie on a Screen at a particular startTime. This is a crucial entity that links everything together.
  • Seat: A physical seat within a Screen. Its availability is specific to a Show.
  • Booking: Represents a confirmed reservation, linking a User, a Show, and the bookedSeats.
  • Payment: Represents the financial transaction for a booking.

The relationships between these entities form the backbone of our design.

UML Class Diagram for a Movie Ticket Booking System
This UML diagram shows the classes and relationships for a movie ticket booking system. A `Cineplex` (Theater) has many `Cinema`s (Screens), each with `Seat`s. A `Movie` is linked to `ShowTime`s, and a `MovieGoer` (User) creates a `Booking` to reserve seats.

Step 3: Designing for Concurrency

This is where the real design challenge begins. Imagine two users, Alice and Bob, both trying to book the last two corner seats (H1 and H2) for the same show at the exact same time.

  1. Both Alice and Bob see that seats H1 and H2 are available.
  2. Alice selects H1 and H2 and proceeds to payment.
  3. Simultaneously, Bob selects H1 and H2 and proceeds to payment.
  4. If not handled correctly, the system might allow both to pay, resulting in a double bookingโ€”a critical business failure.

This is a classic race condition. To solve it, we need a locking mechanism. When a user selects seats, the system must temporarily "lock" them, making them unavailable to others.

Locking Strategies

There are several ways to approach locking. Let's get a high-level overview of the common strategies.

BookMyShow Low level design with code & Concurrency | Movie Ticket Booking System Design #lld #easy

The video 'BookMyShow Low level design' by Mrunmai Dahare provides a concise explanation of three fundamental locking approaches: pessimistic, optimistic, and distributed locking.

Please watch from 27:50 to 32:00. This section explains the difference between these locking types. Focus on understanding the core idea behind each, especially optimistic locking with versioning.

While optimistic locking is powerful, for an interview setting where we need to write concrete code, a pessimistic-style "explicit lock" is often easier to demonstrate. This brings us to the next crucial question: what exactly do we lock?

  • Locking the entire system? Too restrictive. It would mean only one person could book a ticket for any movie, anywhere, at a time.
  • Locking individual Seat objects? This gets very complex. If Alice wants seats [H1, H2] and Bob wants [H2, H3], they contend for the lock on H2. What if Alice gets the lock for H1 but Bob gets the lock for H2? Neither can complete their booking. This leads to deadlocks and complex rollback logic. The booking must be atomic: either all seats are booked, or none are.

A more robust approach is to lock at the level of the shared resource being contended for, which is the Show.

Design [ANY] Booking System ๐ŸŽŸ๏ธ | LLD + Multithreading ๐Ÿงต | Movie Ticket Booking System ๐ŸŽฌ

The video 'Design [ANY] Booking System' by codeWithAryan provides an expert-level breakdown of this 'all or none' problem and presents a clean, effective locking strategy. This is the most important concept in this lesson.

Please watch from 49:01 to 01:03:00. This is a dense but critical section. Focus on these key ideas: The Problem: Why locking individual seats is difficult when a user wants to book multiple seats at once (the 'all or none' requirement). The Solution: Using a temporary, short-lived synchronized block on the Show object (or a map associated with the show). The Logic: Inside this synchronized block, we iterate through the desired seats. If any are already locked or booked, we immediately fail and release the lock. If all are available, we lock them as a single atomic operation and then release the Show lock. The seat locks remain until payment is complete or times out.

This strategy is effective because it ensures that the checking and locking of a group of seats is an atomic operation for any given show, preventing race conditions while still allowing users for different shows to book seats concurrently.

Step 4: Implement Core Logic

Now, let's translate this design into Java code. In an interview, you shouldn't waste time on getters, setters, or admin functionalities. You must focus on the services that implement the core booking and concurrency logic.

We'll design three key components:

  1. SeatLockProvider: An interface and its implementation for managing seat locks. This encapsulates our concurrency strategy.
  2. SeatAvailabilityService: A service to find available seats, taking into account both booked and currently locked seats.
  3. BookingService: The main service that orchestrates the booking flow.

The SeatLockProvider

This component is the heart of our concurrency solution. We'll use the Strategy Pattern here. We define an interface, ISeatLockProvider, and provide a concrete implementation, InMemorySeatLockProvider. This makes our locking mechanism swappable in the future (e.g., to a Redis-based distributed lock provider).

// Interface defining the contract for any lock provider
public interface ISeatLockProvider {
    void lockSeats(Show show, List<Seat> seats, String user);
    void unlockSeats(Show show, List<Seat> seats, String user);
    boolean validateLock(Show show, Seat seat, String user);
    List<Seat> getLockedSeats(Show show);
}

The implementation uses a ConcurrentHashMap to store locks on a per-show basis and synchronizes on a show-specific object to ensure atomicity.

Design [ANY] Booking System ๐ŸŽŸ๏ธ | LLD + Multithreading ๐Ÿงต | Movie Ticket Booking System ๐ŸŽฌ

Let's see the full implementation of this provider in the 'Design [ANY] Booking System' video. This will solidify your understanding of the logic we just discussed.

Rewatch the section from 01:03:00 to 01:07:52. This time, focus on the code for the InMemorySeatLockProvider class itself. Observe how it uses locks.putIfAbsent to create a show-specific lock object and then synchronized on that object to perform the atomic check-and-lock operation.

The BookingService

The BookingService orchestrates the entire process. Its createBooking method is the entry point.

public class BookingService {
    private final ISeatLockProvider seatLockProvider;
    // ... other dependencies

    public Booking createBooking(String userId, Show show, List<Seat> seats) {
        // 1. Check if any requested seats are already permanently booked.
        if (isAnySeatAlreadyBooked(show, seats)) {
            throw new SeatPermanentlyUnavailableException();
        }

        // 2. Attempt to acquire locks on the seats. This is the critical concurrency step.
        seatLockProvider.lockSeats(show, seats, userId);

        // 3. Create a new booking object in a PENDING state.
        final String bookingId = UUID.randomUUID().toString();
        final Booking newBooking = new Booking(bookingId, show, userId, seats);
        
        // In a real system, you'd start a timer here to auto-release locks if payment isn't completed.
        
        return newBooking;
    }

    public void confirmBooking(Booking booking, Payment paymentDetails) {
        // ... process payment
        if (paymentSuccessful) {
            // 4. Mark booking as CONFIRMED.
            booking.confirm();
        } else {
            // 5. If payment fails, release the locks.
            seatLockProvider.unlockSeats(booking.getShow(), booking.getSeatsBooked(), booking.getUser());
        }
    }
    // ... helper methods
}

This structure clearly shows how the BookingService depends on the SeatLockProvider abstraction to handle the complex concurrency logic.

Test your understanding!

Why is the synchronized block in InMemorySeatLockProvider placed on a show-specific object rather than the this keyword (i.e., the InMemorySeatLockProvider instance itself)?

Show answer

If we synchronized on this, it would mean only one thread could attempt to lock seats across the entire system at any given time, regardless of which show they are booking for. This would create a massive performance bottleneck, as a user booking for "Movie A at 7 PM" would block a user booking for "Movie B at 9 PM".

By synchronizing on a show-specific object (retrieved from the ConcurrentHashMap), we achieve a more granular lock. This allows multiple threads to book seats concurrently for different shows, only blocking each other when they contend for the same show. This dramatically improves throughput and scalability.

Step 5: Verification and Extensions

Verifying with a Concurrent Scenario

The best way to validate the design is to trace a concurrent execution flow.

Design [ANY] Booking System ๐ŸŽŸ๏ธ | LLD + Multithreading ๐Ÿงต | Movie Ticket Booking System ๐ŸŽฌ

The codeWithAryan video concludes with a demonstration of exactly this scenario.

Watch from 01:19:12 to 01:21:40. The demo shows two users trying to book overlapping seats (5,6,7 and 7,8,9). Observe how the locking mechanism ensures only one user succeeds and the other's attempt fails cleanly because seat 7 is already locked.

Extension: Designing for High Scale

Our InMemorySeatLockProvider works perfectly for a single application instance. But what if our booking system is a distributed microservice running on multiple servers? The in-memory lock on Server A would be invisible to Server B.

This is where you would introduce a distributed lock manager, like Redis.

Movie Ticket Booking System Design with Concurrency Handling
This diagram illustrates a high-level, distributed architecture. User requests go to an API Gateway. Booking requests are placed in a queue. Multiple 'Booking Workers' can process these requests. To prevent race conditions, each worker must acquire a lock from a centralized, distributed locking service (like Redis) before updating the database.

In an interview, you can mention this as a natural evolution of your design: "My current design uses the Strategy pattern for the ISeatLockProvider. For a single-node deployment, the InMemorySeatLockProvider is sufficient. To scale out, I would create a RedisSeatLockProvider implementation that uses Redis's SETNX command to acquire distributed locks, without changing the BookingService that depends on the interface." This demonstrates foresight and knowledge of HLD concepts.

Conclusion

Excellent work! We've designed a robust movie ticket booking system with a strong focus on its most critical component: concurrency control.

Key Takeaways:

  • Identify the Core Challenge: For any LLD problem, first identify the hardest part. For booking systems, it's almost always concurrency.
  • Atomicity is Key: When users book multiple seats, the operation must be "all or none." This makes simple seat-level locks insufficient and requires a more sophisticated strategy.
  • Granular Locking: The choice of what to lock is crucial for performance. Locking the entire system is too broad; locking individual items can be too complex. Locking a shared resource at the right granularity (like the Show) offers a good balance.
  • Abstract Your Strategy: Use the Strategy pattern (e.g., ISeatLockProvider) to decouple your business logic from the specific concurrency control implementation. This makes your system extensible for future requirements like distributed locking.

In our next lesson, we will continue exploring booking systems by designing a hotel reservation system. It will present similar concurrency challenges but with new twists related to date ranges and room types.

Can't find a good explanation? Sign up and we'll make it for you

Sign up