Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,106visitors

Built for engineers who want to go deep.


← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
Chaturmind
← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
HomeLearnSystem DesignLow-Level Design & Design PatternsMachine Coding Exercises
✓ FreeAdvanced· 8 min read

Movie Ticket Booking System

Theater/Screen/Show/Seat/Booking classes, temporary seat holds with expiry vs immediate commit, and preventing the double-booking race condition.

Published September 23, 2026


Movie Ticket Booking System

Core classes

class Theater { List<Screen> screens; }
class Screen { String id; List<Seat> seats; }
class Seat { String id; SeatType type; }
class Show { Screen screen; Movie movie; Instant startTime; Map<String, SeatStatus> seatStatuses; } // per-show, not per-seat globally
class Booking { Show show; List<Seat> seats; User user; BookingStatus status; Instant expiresAt; }

Seat availability is tracked per Show, not as a permanent property of Seat — the same physical seat is available for a 2pm show and separately bookable for a 6pm show on the same screen. This is the detail that trips people up first: Seat itself has no booking state at all; Show.seatStatuses does.

Advertisement

Temporary hold with expiry, not immediate commit

enum SeatStatus { AVAILABLE, HELD, BOOKED }

class BookingService {
    Optional<Booking> holdSeats(Show show, List<String> seatIds, User user) {
        synchronized (show) { // see below for why per-show locking, not a global lock
            for (String id : seatIds) {
                if (show.getSeatStatus(id) != SeatStatus.AVAILABLE) return Optional.empty();
            }
            seatIds.forEach(id -> show.setSeatStatus(id, SeatStatus.HELD));
            Booking booking = new Booking(show, seatIds, user, BookingStatus.PENDING, Instant.now().plus(Duration.ofMinutes(10)));
            scheduleExpiry(booking); // if not confirmed within 10 minutes, seats auto-release back to AVAILABLE
            return Optional.of(booking);
        }
    }

    boolean confirmBooking(Booking booking) {
        if (Instant.now().isAfter(booking.getExpiresAt())) return false; // hold already expired
        booking.getSeats().forEach(seat -> booking.getShow().setSeatStatus(seat, SeatStatus.BOOKED));
        booking.setStatus(BookingStatus.CONFIRMED);
        return true;
    }
}

Going straight to BOOKED on seat selection (immediate commit) would let a user select seats, abandon the checkout flow (browser closed, payment never completed), and permanently lock those seats away from every other user. A temporary HELD state with expiry — the seat is reserved just long enough to complete payment, then automatically released if the flow doesn't finish — is the standard fix, directly analogous to Parking Lot's spot-claiming problem but with a time-bounded hold instead of a permanent claim.

Preventing the double-booking race

Two users selecting the same seat for the same show simultaneously is the exact same check-then-act race covered throughout this course (HashMap Concurrency Variants, Parking Lot — Implementation). The synchronized (show) block above locks per show, not globally across the whole theater chain — two users booking seats for different shows never contend with each other, only users targeting the same show's seat map do. At real scale, this per-show lock would typically be replaced by a database-level constraint (a unique index on (show_id, seat_id, status=BOOKED), or the optimistic-locking @Version pattern from Locking Strategies) rather than an in-process lock, since booking requests for a popular show likely arrive across multiple application server instances, not one process holding one JVM monitor.

Seat holds that survive multiple servers

An in-process lock and timer work for one server. With several application instances behind a load balancer, the hold has to live in shared storage. Two common implementations:

Option 1: the database as the source of truth

CREATE TABLE show_seat (
    show_id     BIGINT      NOT NULL,
    seat_id     VARCHAR(8)  NOT NULL,
    status      VARCHAR(10) NOT NULL DEFAULT 'AVAILABLE',   -- AVAILABLE | HELD | BOOKED
    hold_id     UUID,
    held_until  TIMESTAMPTZ,
    booking_id  BIGINT,
    PRIMARY KEY (show_id, seat_id)
);

Holding seats is a single conditional update. Expired holds count as available, so no background job is needed for correctness:

UPDATE show_seat
   SET status = 'HELD', hold_id = :holdId, held_until = now() + interval '10 minutes'
 WHERE show_id = :showId
   AND seat_id = ANY(:seatIds)
   AND (status = 'AVAILABLE' OR (status = 'HELD' AND held_until < now()));
@Transactional
public Optional<UUID> holdSeats(long showId, List<String> seatIds) {
    UUID holdId = UUID.randomUUID();
    int updated = seatDao.holdIfFree(showId, seatIds, holdId);
    if (updated != seatIds.size()) {                      // someone got at least one seat first
        throw new SeatsUnavailableException(seatIds);     // exception → rollback releases the partial hold
    }
    return Optional.of(holdId);
}
  • All-or-nothing: the user wants seats together, so if fewer rows were updated than requested, the transaction rolls back and nothing stays held.
  • No race: the row-level write and its WHERE condition are evaluated atomically, so two transactions can't both win the same seat.
  • Confirmation checks the hold ID and expiry in the same way: UPDATE … SET status='BOOKED' WHERE hold_id = :holdId AND held_until > now(). If the result is 0 rows, the hold expired and the payment must be refunded or the booking retried.
  • A periodic job can reset long-expired rows to AVAILABLE for tidier data, but correctness doesn't depend on it.

Option 2: Redis for holds, the database for bookings

For very hot shows (a blockbuster's opening night), holds can live in Redis: SET hold:{show}:{seat} {holdId} NX PX 600000 for each seat, reserved with a Lua script so that multiple seats are taken atomically, or not at all. The TTL expires abandoned holds automatically. Confirmed bookings are still written to the database as the durable record, and the confirmation re-checks the Redis hold before writing. This relieves the database of high-churn hold traffic, at the cost of an extra system that must be kept consistent.

Payment and the hold timer

The flow that ties it together:

  1. Hold the seats, and return holdId with an expiry time. The UI shows a countdown.
  2. Create a payment intent with the booking reference as an idempotency key, so a retried request never charges twice.
  3. When the payment provider's webhook (or the redirect) reports success, confirm the seats with the hold ID.
  4. If confirmation fails because the hold expired: automatically refund, and tell the user. This race is rare, but it must be handled, not ignored.
  5. Make the hold slightly longer than the payment page's timeout, so a payment that completes in time can always confirm.

Showing the seat map

The seat map is read far more often than it's written: thousands of people look, and few book. Serve it from a short-lived cache (a few seconds), or push updates over WebSockets for popular shows. A stale map only means a user occasionally clicks a seat that was just taken, and the hold request, being authoritative, returns a clear "seat no longer available".

Follow-up questions this topic invites — and their answers

Q: How would you scale seat-hold expiry across multiple application instances, given scheduleExpiry() here implies a single-process timer? A: A single in-process scheduled task doesn't survive that instance restarting or scale across multiple instances — production systems typically use a durable, distributed mechanism instead (a delayed job in a queue, or a database row with an expires_at column that a periodic sweep job checks), rather than relying on an in-memory timer tied to one process's lifetime.

Q: What happens if payment succeeds but confirmBooking() is called after the hold already expired? A: This is exactly the failure scenario worth designing for explicitly: the seats may have already been released and rebooked by someone else — the system needs to detect this (confirmBooking returning false as shown) and trigger a refund/retry flow, rather than silently confirming a booking for seats that are no longer actually held.

Q: Why lock per-show rather than per-seat, given seats are the actual contended resource? A: Per-seat locking would technically be finer-grained and allow slightly more parallelism, but checking availability for MULTIPLE seats atomically (a user typically selects several seats in one booking) requires holding all their locks together anyway to avoid a partial-success race — per-show locking is simpler to reason about correctly for this multi-seat-atomicity requirement, at the cost of some contention between users booking different seats on the same popular show.

Q: How do you hold several seats atomically so a group isn't split? A: Hold them in one conditional update (or one transaction) and check that the number of rows changed equals the number requested. If not, roll back. With Redis, use a Lua script that checks all seats, then sets them all, as one atomic operation.

Q: Why treat expired holds as available in the query instead of relying on a cleanup job? A: Then correctness doesn't depend on a background job's timing or health. A seat whose hold time has passed is immediately bookable. The cleanup job only tidies up data.

Q: What if the payment succeeds but the hold has already expired? A: The confirm step finds no valid hold and fails. The system should then attempt to re-hold the same seats if they're still free, and otherwise refund automatically and notify the user. Using idempotency keys ensures that retries never double-charge or double-refund.

Advertisement

Previous

Producer-Consumer Class Design

Next

Hotel Reservation System

AI Tutor

Lesson: Movie Ticket Booking System

Quick actions

AI responses can be inaccurate. Verify critical information.