LLD for Movie Booking System: Designing BookMyShow
Designing a movie booking system involves handling concurrency (no double bookings!), managing complex hierarchies, and ensuring a smooth user experie
Abstract AlgorithmsTLDR
TLDR: A Movie Booking System (like BookMyShow) is an inventory management problem with an expiry: seats expire when the show starts. The core engineering challenge is preventing double-booking under concurrent user load with a 3-state seat model (AVAILABLE โ LOCKED โ BOOKED).
๐ The Cinema Inventory Problem: What Makes It Hard
You run a cinema. A Friday night show has 200 seats. Two scenarios break a naive implementation:
- Double-booking: User A and User B click "Book Seat A1" at the same millisecond. Only one should succeed.
- Abandoned locks: User A locks Seat A1 but never completes payment. The seat should become available again after 10 minutes.
This needs a 3-state model + distributed lock strategy.
๐ Key Entities in a Movie Booking System
Before writing a single line of concurrency code, you need a clear domain model. A movie booking platform has six core entities:
| Entity | What It Represents | Key Attributes |
| Movie | The film being screened | title, duration, genre |
| Cinema | The physical venue | name, city, list of screens |
| Screen | A single auditorium | id, total seats, seat layout |
| Show | One screening of a movie on a screen | movie, screen, startTime |
| Seat | An individual seat in a screen | row, column, SeatType, SeatStatus |
| Booking | A confirmed reservation | user, show, seats, paymentStatus |
| User | The person making the reservation | name, email, booking history |
SeatType classifies seats by price tier:
PLATINUMโ premium recliner seats (front/center)GOLDโ standard premium tierSILVERโ economy tier
SeatStatus is the concurrency-critical state that drives the entire booking engine:
AVAILABLEโ free to book by any userLOCKEDโ temporarily held during checkout (10-minute TTL by default)BOOKEDโ payment confirmed, seat belongs to a specific user
The Show entity is the critical aggregation point: it joins a Movie, a Screen, and a specific start time. Every Booking references a Show, not the Movie directly โ this is why "the 7 PM showing of a film" is completely separate inventory from "the 9 PM showing" of the same film. This hierarchy shapes every query, index, and cache partition in the real system.
๐ข The Domain Hierarchy: City โ Cinema โ Screen โ Show โ Seat
classDiagram
class City { +String name; +List~Cinema~ cinemas }
class Cinema { +String name; +List~Screen~ screens }
class Screen { +int id; +List~Seat~ seats }
class Movie { +String title; +int durationMinutes }
class Show {
+Movie movie
+Screen screen
+LocalDateTime startTime
+synchronized boolean bookSeats(List seats)
}
class Seat {
-SeatStatus status
+String row
+int col
+SeatType type
+reserve(bookingId)
+release()
+confirm()
}
class PremiumSeat {
+boolean hasRecline
+boolean hasFootrest
}
class GroupSeat {
+String groupReservationId
}
class Booking {
+int id
+Show show
+List~Seat~ seats
+PaymentStatus payment
}
class BookingService {
+reserveSeats(User, Show, List~Seat~)
+confirmBooking(Booking)
}
class PricingStrategy {
<>
+calculatePrice(Seat, Show, Customer) Money
}
class PaymentGateway {
<>
+charge(Customer, Money, String) PaymentResult
}
City *-- Cinema
Cinema *-- Screen
Screen *-- Seat
Show "1" *-- "many" Seat : contains
Show --> Screen
Booking --> Show
Booking --> Seat
PremiumSeat --|> Seat : extends
GroupSeat --|> Seat : extends
BookingService o-- PricingStrategy : uses (injected)
BookingService o-- PaymentGateway : uses (injected)
- SeatType:
GOLD,SILVER,PLATINUMโ affects pricing. - SeatStatus:
AVAILABLE,LOCKED,BOOKEDโ the concurrency core. - Show is the intersection of Movie + Screen + StartTime.
โ๏ธ The 3-State Seat Model: Preventing Double Booking
The key insight: don't go directly from AVAILABLE to BOOKED. Insert a temporary LOCKED state.
State machine:
stateDiagram-v2
[*] --> AVAILABLE
AVAILABLE --> LOCKED : User selects seat (10 min TTL)
LOCKED --> BOOKED : Payment success
LOCKED --> AVAILABLE : Payment timeout / failure
BOOKED --> [*] : Show complete
The booking flow:
| Time | User A | User B | Seat A1 Status |
| 10:00:00 | Selects A1 | โ | AVAILABLE |
| 10:00:01 | System locks A1 | โ | LOCKED (expires 10:10) |
| 10:00:02 | Directed to payment | Selects A1 | โ |
| 10:00:02 | โ | System: seat unavailable | LOCKED (no change) |
| 10:05:00 | Payment success | โ | BOOKED |
User B gets a graceful "This seat is currently being held by another user" message โ not a race condition with undefined behavior.
๐ Booking Flow: From Seat Selection to Confirmation
Here is how a booking request moves through the system end-to-end โ including both failure paths that keep inventory clean:
flowchart TD
A([User Selects Seats]) --> B{All seats AVAILABLE?}
B -- No --> C([Error: Seat Already Held by Another User])
B -- Yes --> D[Lock Seats โ 10 min TTL applied]
D --> E([User Redirected to Payment Page])
E --> F{Payment Successful?}
F -- Timeout or Failure --> G[Release Lock โ Seats set back to AVAILABLE]
F -- Yes --> H[Set Seats to BOOKED]
H --> I([Confirmation Email Sent to User])
G --> J([Error: Payment Failed โ Seats Released])
Two distinct failure paths keep inventory accurate:
- Pre-lock failure โ seats are already LOCKED by a concurrent user. The second user receives a graceful "unavailable" response immediately, before any lock is created on their behalf.
- Post-lock failure โ the user's payment times out or fails. The LOCKED seats revert to AVAILABLE either via TTL expiry (handled by a background cleanup job) or an explicit release call in the payment failure handler.
The 10-minute lock TTL is the safety net for abandoned sessions: a user who closes the browser mid-checkout will not freeze a seat for the rest of the evening.
๐ง Deep Dive: Synchronized Booking Implementation
public class Show {
private final int id;
private final List<Seat> seats;
public synchronized BookingResult reserveSeats(User user, List<Integer> seatIds) {
// Phase 1: Check all requested seats are available
List<Seat> selected = new ArrayList<>();
for (int id : seatIds) {
Seat s = findSeat(id);
if (s.getStatus() != SeatStatus.AVAILABLE) {
return BookingResult.failure("Seat " + id + " is not available");
}
selected.add(s);
}
// Phase 2: Lock all seats together (atomic within synchronized block)
Instant lockExpiry = Instant.now().plusSeconds(600);
for (Seat s : selected) {
s.setStatus(SeatStatus.LOCKED);
s.setLockExpiry(lockExpiry);
}
return BookingResult.success(new Booking(user, this, selected));
}
}
synchronized on the Show method ensures that the check-and-lock is atomic โ no two threads can interleave between the check (AVAILABLE) and the lock (LOCKED).
โ๏ธ Pricing Strategy Pattern
Different shows and seat types need different prices:
public interface PricingStrategy {
Money calculatePrice(Seat seat, Show show, Customer customer);
}
public class HourlyPricing implements PricingStrategy {
public Money calculatePrice(Seat seat, Show show, Customer customer) {
double base = seat.getType() == SeatType.GOLD ? 15.0 : 10.0;
double multiplier = show.isWeekend() ? 1.2 : 1.0; // weekend surcharge
return Money.of(base * multiplier);
}
}
public class DynamicPricing implements PricingStrategy {
public Money calculatePrice(Seat seat, Show show, Customer customer) {
double occupancy = show.getBookedPercent();
return Money.of(BASE_PRICE * (1 + occupancy)); // price rises with demand
}
}
๐ Interface Contracts: The Three Boundaries That Keep Booking Decoupled
Every cross-module dependency in the booking system is expressed as a Java interface. This locks in the contract while leaving each implementation free to change independently.
// Abstracts price computation โ BookingService never hardcodes a fee rule
interface PricingStrategy {
Money calculatePrice(Seat seat, Show show, Customer customer);
}
// Abstracts seat discovery โ BookingService never knows the screen layout algorithm
interface SeatSelector {
List<Seat> findAvailable(Show show, SeatType type, int count);
}
// Abstracts the payment provider โ BookingService never touches a provider SDK
interface PaymentGateway {
PaymentResult charge(Customer customer, Money amount, String idempotencyKey);
}
| Interface | What It Abstracts | Implementors | Consumer |
PricingStrategy | Fee calculation rules | HourlyPricing, DynamicPricing, GroupRatePricing | BookingService |
SeatSelector | Seat availability search and ranking | DefaultSeatSelector, AdjacentSeatSelector | BookingService |
PaymentGateway | Payment provider API calls | StripeGateway, RazorpayGateway | BookingService |
The idempotencyKey in PaymentGateway.charge() (typically bookingId + "-" + attemptNumber) lets the payment provider safely deduplicate retried charges โ critical when a network timeout causes BookingService to retry after the first charge already succeeded.
๐งฑ OOP Pillars Applied to the Booking Domain
Each of the four OOP pillars has a concrete engineering role in this design โ not as theoretical labels but as decisions that keep the system correct and extensible.
Encapsulation โ Seat Owns Its Own State Machine
SeatStatus is a private field. No class outside Seat can call setStatus(SeatStatus.BOOKED) directly. External code uses the explicit transition methods, and Seat enforces invariants at every step:
public class Seat {
private SeatStatus status = SeatStatus.AVAILABLE; // hidden โ no direct setter
public synchronized void reserve(String bookingId) {
if (status != SeatStatus.AVAILABLE)
throw new IllegalStateException("Seat already held");
this.status = SeatStatus.LOCKED;
this.bookingId = bookingId;
}
public synchronized void release() {
if (status == SeatStatus.LOCKED) {
this.status = SeatStatus.AVAILABLE;
this.bookingId = null;
}
}
public synchronized void confirm() {
if (status != SeatStatus.LOCKED)
throw new IllegalStateException("Cannot confirm an unlocked seat");
this.status = SeatStatus.BOOKED;
}
// No public setStatus() โ external code cannot bypass the state machine
}
synchronized on each method means the state machine is also thread-safe: a LOCKED seat cannot be accidentally set back to AVAILABLE by one thread while another thread is confirming it.
Abstraction โ PricingStrategy Hides the Fee Rules
BookingService calls one method and gets a price. Whether the active strategy applies a peak-hour surcharge, a loyalty discount, or a group rate is invisible to the caller:
// BookingService only knows this interface โ not which implementation is active
Money price = pricingStrategy.calculatePrice(seat, show, customer);
The abstraction also insulates the service from future business-logic changes: adding a holiday-surcharge rule never requires touching BookingService โ it only requires a new HolidayPricing implements PricingStrategy.
Inheritance โ PremiumSeat and GroupSeat Extend the Base Contract
The physical seat hierarchy maps directly to Java inheritance. Both subclasses inherit reserve(), release(), and confirm() without duplicating concurrency logic:
public class PremiumSeat extends Seat {
private boolean hasRecline;
private boolean hasFootrest;
// inherits the full SeatStatus state machine โ no duplication
}
public class GroupSeat extends Seat {
private String groupReservationId;
@Override
public synchronized void reserve(String bookingId) {
super.reserve(bookingId); // delegate to parent state machine
this.groupReservationId = bookingId; // add group-level metadata
}
}
The hierarchy models physical auditoriums: every premium recliner and group-linked seat is still fundamentally a Seat with the same lifecycle.
Polymorphism โ One Loop, All Seat Types
Show holds a List<Seat>. At runtime the list may contain Seat, PremiumSeat, and GroupSeat objects. Pricing and state transitions work uniformly across all of them:
List<Seat> seats = show.getSeats(); // may be StandardSeat, PremiumSeat, GroupSeat
for (Seat seat : seats) {
// runtime dispatch: correct pricing rule chosen based on actual type + strategy
Money price = pricingStrategy.calculatePrice(seat, show, customer);
}
PricingStrategy polymorphism complements this: DynamicPricing, HourlyPricing, and GroupRatePricing each implement calculatePrice differently, but BookingService uses a single call site regardless of which implementation is injected.
โ SOLID Principles in the Booking Design
| Principle | How It Appears in the Design |
| SRP | Seat manages its own state transitions; Show manages seat inventory for a screening; BookingService orchestrates a single booking transaction โ each class has exactly one reason to change |
| OCP | Adding a VIP pod seat type = VipSeat extends Seat + VipPricingStrategy implements PricingStrategy. No existing class is modified; the system is open for extension, closed for modification |
| LSP | PremiumSeat and StandardSeat both honour Seat's reserve()/release()/confirm() contract โ they are substitutable anywhere a Seat is expected, and a List<Seat> works correctly regardless of the concrete type it holds |
| ISP | PricingStrategy is a single-method interface; SeatSelector is a separate interface. A class that only discovers seats never has to implement calculatePrice, and vice versa |
| DIP | BookingService depends on PricingStrategy and PaymentGateway abstractions injected via constructor โ it never references HourlyPricing or StripeGateway directly |
DIP in practice โ constructor injection keeps BookingService provider-agnostic:
@Service
public class BookingService {
private final PricingStrategy pricingStrategy; // abstraction, not a concrete class
private final PaymentGateway paymentGateway; // abstraction, not a concrete class
public BookingService(PricingStrategy pricingStrategy, PaymentGateway paymentGateway) {
this.pricingStrategy = pricingStrategy;
this.paymentGateway = paymentGateway;
}
// Swapping DynamicPricing for HourlyPricing is a Spring config change, not a code change
}
In Spring Boot the active PricingStrategy bean (e.g., DynamicPricing) is injected by the container at startup. Switching strategies for a feature flag or A/B test requires only a configuration change.
โ๏ธ OOP Design Trade-offs and Failure Modes in the Booking System
| Design Decision | Trade-off |
| 3-state model vs. 2-state | Three states (AVAILABLE โ LOCKED โ BOOKED) prevent double-booking but add a cleanup responsibility: LOCKED seats must expire. A 2-state model is simpler but makes race conditions inexpressible in the design |
| Encapsulated state machine vs. direct field access | Seat.reserve() / release() / confirm() enforce invariants at every transition. Exposing setStatus() is simpler to write but shifts correctness responsibility to every caller |
Interface injection vs. new inside the method | Injecting PricingStrategy makes BookingService independently testable with any fake strategy. Using new HourlyPricing() inside the method couples the service to one rule and makes substitution impossible without modifying the class |
| Inheritance depth | One level (PremiumSeat extends Seat) is safe โ the state machine is inherited cleanly. Deeper hierarchies (e.g., VipReclinerSeat extends PremiumSeat extends Seat) make reserve() override chains hard to trace. Prefer composition for third-level specialisation |
| Single-method interfaces vs. fat interfaces | PricingStrategy with one method is easy to implement as a lambda and easy to test in isolation. A fat BookingOperations interface forces implementors to provide methods they don't use and violates ISP |
Failure mode โ skipping the LOCKED state:
If code transitions directly from AVAILABLE to BOOKED in a single step, any failure mid-payment (timeout, gateway error, network drop) leaves the seat permanently BOOKED with no completed payment. The LOCKED intermediate state isolates the inventory hold from the payment outcome, making reversal explicit, TTL-bounded, and safe.
๐ Real-World Applications of the Movie Booking Pattern
The AVAILABLE โ LOCKED โ BOOKED pattern is not unique to cinemas. Any system where a limited, enumerable resource must be reserved under concurrent demand faces the same engineering challenge:
| Domain | Resource Being Reserved | Typical Lock TTL |
| Flight booking | Aircraft seat | 15โ30 min (complex international payment) |
| Concert tickets | Venue seat or GA slot | 5โ10 min (short to limit bot abuse) |
| Hotel rooms | Room-night inventory | 15 min |
| Sports events | Stadium seat | 8โ12 min |
| Exam slot booking | Test center seat + timeslot | 20 min |
| Parking reservations | Parking space | Variable |
The key differentiator across domains is TTL tuning: flight bookings allow longer locks because international payment flows involve more redirect steps. High-demand concert systems use shorter TTLs to prevent automated bots from freezing large seat blocks and releasing them at the last moment.
The BookMyShow design is the canonical LLD interview question because it is scope-limited enough to whiteboard in 45 minutes while covering every essential OOD concept: domain hierarchy, state machines, concurrency control, and behavioral patterns like Strategy.
๐งช Exercises: Extend the Booking System Yourself
Work through these exercises to move from understanding the design to being able to implement it under interview conditions.
Exercise 1 โ Add a Cancellation Flow
Implement a cancelBooking(Booking b) method on Show. What state transition should the seat undergo: directly back to AVAILABLE, or through an intermediate CANCELLATION_PENDING state? Handle the edge case of a partial cancellation โ the user booked 3 seats and wants to cancel only 1. Should the remaining 2 seats stay BOOKED?
Exercise 2 โ Expired Lock Cleanup Job
Write a ScheduledJob that runs every 60 seconds and reverts all LOCKED seats where lockExpiry < Instant.now() back to AVAILABLE. Now consider: what happens if two instances of this job run concurrently on a two-node deployment? Design a guard (using DB-level CAS or a distributed lock) to prevent the same seat from being released twice.
Exercise 3 โ Adjacent Seat Recommendation
Given a user who requests n adjacent seats, write a method List<Seat> findBestAdjacentSeats(Screen screen, int n). Return the best available adjacent block ranked by SeatType, preferring center rows first. Define "best" explicitly, and handle the edge case where no block of n adjacent seats exists in any row.
๐ Design Lessons from the Booking System
- Never go directly from check to commit. The AVAILABLE โ LOCKED โ BOOKED progression prevents the classic TOCTOU (Time-of-Check to Time-of-Use) race condition. Any two-step "check then act" operation needs an atomic transition protecting it.
- TTLs are a correctness requirement, not an optimization. Without lock expiry, one abandoned checkout session can freeze a seat until the show starts. Design every temporary lock with a TTL from day one.
synchronizedis a valid starting point. Start simple withsynchronizedon the Show method for single-server deployments. Migrate to Redis SETNX or DB optimistic locking only when operational scale demands it โ don't over-engineer upfront.- Strategy Pattern keeps pricing logic decoupled. Hardcoding seat prices inside the booking method creates a maintenance trap. Pricing rules change seasonally and by event; the booking engine should never change with them.
- The domain hierarchy shapes your database indexes. City โ Cinema โ Screen โ Show โ Seat is not just an OOP diagram โ it determines how you index, partition, and cache your data at scale.
๐งญ OOP Decision Guide: Patterns and Extension Points
| Decision Point | Recommendation |
| New seat category (VIP pod, wheelchair) | VipSeat extends Seat โ inherit the full state machine, add category-specific fields. Only override reserve() if the locking behaviour genuinely differs for that category |
| New pricing rule | HolidayPricing implements PricingStrategy โ never add an if (isHoliday) branch inside BookingService. The Strategy pattern exists to absorb exactly this kind of change |
| New seat selection algorithm | AdjacentSeatSelector implements SeatSelector โ the interface isolates layout logic from booking orchestration. Swap selectors via constructor injection without touching BookingService |
| Payment provider switch | RazorpayGateway implements PaymentGateway โ the gateway interface abstracts the entire provider SDK. BookingService needs zero changes |
synchronized vs. distributed locking | synchronized on Seat methods is correct for object-level thread safety within one JVM. Move to Redisson RLock only when the locking boundary crosses a JVM. Keep the method-level interface contract identical in both cases |
Abstract class vs. interface for Seat | Seat is a concrete class (not abstract) because the base seat type is valid on its own. PricingStrategy and PaymentGateway are interfaces because they have no shared state โ each implementation is completely independent |
Start with synchronized on Seat methods and constructor-injected strategies. Extend the design by adding new subclasses and strategy implementations โ never by adding conditional branches inside existing classes.
๐ TLDR: Summary & Key Takeaways
- Encapsulation:
Seatowns itsSeatStatusstate machine โ external code callsreserve()/release()/confirm(), never sets status directly.synchronizedmethods enforce both the contract and thread safety. - 3-state seat model (AVAILABLE โ LOCKED โ BOOKED) prevents double-booking and handles abandoned checkouts via TTL expiry.
synchronized bookSeats()makes the check-and-lock atomic; no two threads can hold the same seat simultaneously.- Abstraction + Strategy Pattern:
PricingStrategydecouples fee rules fromBookingServiceโ addHolidayPricingwithout touching the service. - SOLID OCP: every extension point (
VipSeat,RazorpayGateway,AdjacentSeatSelector) is a new class, never a modification to an existing one. - DIP:
BookingServicedepends onPricingStrategyandPaymentGatewayabstractions injected via constructor โ concrete providers are swappable at configuration time.
๐ Practice Quiz
Why is a 3-state model (AVAILABLE โ LOCKED โ BOOKED) better than a 2-state model (AVAILABLE โ BOOKED)?
- A) It reduces database writes.
- B) The LOCKED state holds the seat for the user during payment without permanently booking it, allowing auto-release on timeout.
- C) It's required by cinema software standards.
Correct Answer: B โ The LOCKED state holds the seat during payment without permanently booking it, allowing automatic release on timeout or failure.
In the single-server Java implementation, what prevents two threads from both seeing a seat as AVAILABLE and both locking it?
- A)
volatileon the seat status field. - B)
synchronizedonbookSeats()โ only one thread executes the check-and-lock block at a time. - C) A database constraint.
Correct Answer: B โsynchronizedonbookSeats()ensures only one thread executes the check-and-lock block at a time, making the check-then-lock atomic.
- A)
At multi-server scale, which mechanism replaces Java's
synchronizedkeyword for preventing double booking?- A) Thread pools with fixed queue size.
- B) Distributed lock via Redis SETNX (or DB optimistic locking with CAS).
- C) Read replicas for the seat table.
Correct Answer: B โ Redis SETNX with a TTL-keyed lock (or DB CAS) replaces the JVMsynchronizedblock when booking requests span multiple server instances.
๐ ๏ธ Spring Data JPA and Redisson: Transactional Booking with a Distributed Lock
Spring Data JPA maps the Seat entity and its SeatStatus enum to a relational table and provides @Transactional isolation so concurrent threads cannot both read AVAILABLE and commit a BOOKED update for the same seat. Redisson is a Redis-based Java client that provides RLock โ a distributed reentrant lock with automatic expiry โ to prevent double-booking across multiple JVM instances (the multi-server scenario synchronized cannot solve).
How they solve the problem in this post: The 3-state model (AVAILABLE โ LOCKED โ BOOKED) maps directly to JPA's optimistic locking via @Version. Redisson's RLock acquires a per-seat Redis lock for the 10-minute payment window, releasing it on payment or expiry โ handling the abandoned-lock scenario automatically.
// โโโ Seat entity with optimistic locking via @Version โโโโโโโโโโโโโโโโโโโโโโโโโ
import jakarta.persistence.*;
@Entity
@Table(name = "seats")
public class Seat {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int row;
private int column;
@Enumerated(EnumType.STRING)
private SeatStatus status = SeatStatus.AVAILABLE;
@Version // optimistic lock: incremented on each update
private int version; // concurrent update โ OptimisticLockException โ retry
// getters / setters (or use Lombok @Data)
}
public enum SeatStatus { AVAILABLE, LOCKED, BOOKED }
// โโโ Repository: Spring Data JPA โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
public interface SeatRepository extends JpaRepository<Seat, Long> {
// Pessimistic write lock at DB level: SELECT ... FOR UPDATE
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT s FROM Seat s WHERE s.id = :id")
java.util.Optional<Seat> findByIdForUpdate(Long id);
}
// โโโ Booking service: Redisson distributed lock + @Transactional โโโโโโโโโโโโโโ
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
@Service
public class BookingService {
private final SeatRepository seatRepo;
private final RedissonClient redisson; // injected via Spring Boot auto-config
public BookingService(SeatRepository seatRepo, RedissonClient redisson) {
this.seatRepo = seatRepo;
this.redisson = redisson;
}
/**
* Lock the seat for 10 minutes (payment window).
* Redisson RLock prevents two JVM nodes booking the same seat concurrently.
*/
@Transactional
public String lockSeat(Long seatId, String userId) throws InterruptedException {
String lockKey = "seat:lock:" + seatId;
RLock lock = redisson.getLock(lockKey);
// Try to acquire; fail fast if another user already holds it (0 wait)
boolean acquired = lock.tryLock(0, 10, TimeUnit.MINUTES);
if (!acquired) {
return "SEAT_ALREADY_LOCKED";
}
Seat seat = seatRepo.findByIdForUpdate(seatId)
.orElseThrow(() -> new IllegalArgumentException("Seat not found: " + seatId));
if (seat.getStatus() != SeatStatus.AVAILABLE) {
lock.unlock();
return "SEAT_NOT_AVAILABLE";
}
seat.setStatus(SeatStatus.LOCKED);
seatRepo.save(seat); // @Transactional ensures DB + Redisson are in sync
return "LOCKED"; // caller proceeds to payment flow
}
/** Confirm booking after successful payment */
@Transactional
public void confirmBooking(Long seatId) {
Seat seat = seatRepo.findByIdForUpdate(seatId).orElseThrow();
if (seat.getStatus() != SeatStatus.LOCKED) {
throw new IllegalStateException("Seat " + seatId + " is not in LOCKED state");
}
seat.setStatus(SeatStatus.BOOKED);
seatRepo.save(seat);
// Redisson lock auto-expires after 10 min โ no explicit unlock needed on confirm
}
}
@Version handles single-JVM concurrency: if two threads read the same version=5 and both try to UPDATE, the second update throws OptimisticLockException โ the caller retries. Redisson's RLock handles multi-JVM concurrency: only one node across the entire cluster can hold seat:lock:42 at a time, and the 10-minute TTL automatically releases abandoned locks without a background job.
For a full deep-dive on Spring Data JPA pessimistic/optimistic locking and Redisson distributed patterns, a dedicated follow-up post is planned.
๐ Related Posts
- LLD for Parking Lot System โ another bounded-inventory OOD problem; slots use the same AVAILABLE/OCCUPIED state model
- LLD for Elevator System โ state machine design applied to request scheduling and direction logic
- LLD for URL Shortener โ LLD with hashing, collision handling, and storage trade-offs
- LLD for LRU Cache โ concurrency-aware caching with eviction policies; pairs well with the seat-lock TTL pattern
- Single Responsibility Principle โ the OOD principle driving class separation in the domain hierarchy above
- Strategy Design Pattern โ the pattern powering the pluggable pricing engine

Written by
Abstract Algorithms
@abstractalgorithms
More Posts

Types of LLM Quantization: By Timing, Scope, and Mapping
TLDR: There is no single "best" LLM quantization. You classify and choose quantization along three axes: when you quantize (timing), what you quantize (scope), and how values are encoded (mapping). In
Stream Processing Pipeline Pattern: Stateful Real-Time Data Products
TLDR: Stream pipelines succeed when event-time semantics, state management, and replay strategy are designed together โ and Kafka Streams lets you build all three directly inside your Spring Boot service. Stripe's real-time fraud detection processes...
Service Mesh Pattern: Control Plane, Data Plane, and Zero-Trust Traffic
TLDR: A service mesh intercepts all service-to-service traffic via injected Envoy sidecar proxies, letting a platform team enforce mTLS, retries, timeouts, and circuit breaking centrally โ without changing application code. Reach for it when cross-te...
Serverless Architecture Pattern: Event-Driven Scale with Operational Guardrails
TLDR: Serverless is strongest for spiky asynchronous workloads when cold-start, observability, and state boundaries are intentionally designed. TLDR: Serverless works best for spiky, event-driven workloads when you design for idempotency, observabili...
