16. Hotel Reservation System Intermediate → Advanced (Concurrency)

A. Realistic Interview Prompt

"Design a hotel reservation system. Guests can search for available rooms over a date range, book a room, and cancel a reservation. Room prices vary by room type and season."

B. Difficulty

Intermediate, climbing to Advanced on the concurrency follow-up. The entity model itself is approachable; the real signal is whether the candidate notices that "room available for these dates" is a date-range query, not a boolean flag, and handles two guests racing to book overlapping dates on the same room.

C. What This Problem Tests

D. Requirements

Functional

  1. Search for rooms available across a given check-in/check-out date range, optionally filtered by room type
  2. Book a room for a date range, producing a reservation with a computed total price
  3. Cancel a reservation, freeing the room's availability for that date range
  4. Price a room based on room type, season, and current demand
  5. Notify the guest (and hotel staff) on booking confirmation and cancellation

Non-Functional

E. Clarifying Questions

#QuestionWhy It MattersDesign Decision Affected
1Is availability tracked per physical room, or per room type with an undifferentiated pool (any "Deluxe" room, not a specific numbered one)?Changes whether overlap-checking happens against one specific room's calendar or against a type-level countDesign of the availability/inventory structure
2Does pricing vary by season, day-of-week, and/or real-time demand, or is it a fixed rate per room type?Determines whether pricing needs to be a Strategy or can be a static lookupDesign of PricingStrategy
3Is overbooking ever intentionally allowed (a common real hotel practice, betting on no-shows)?If yes, availability becomes probabilistic rather than a hard overlap checkWhether the overlap check is a hard invariant or a soft one with an overbooking margin
4What's the cancellation policy — free cancellation window, partial refund, no refund?Determines whether cancellation needs its own Strategy alongside pricingScope of CancellationPolicy

F. Assumptions

G. Actors

ActorResponsibility
GuestSearches, books, cancels reservations
Hotel Staff / Front DeskManages room inventory, checks guests in/out
Hotel (facade)Top-level API coordinating search, booking, and cancellation

H. Core Domain Entities

ClassResponsibilityKey AttributesKey MethodsDoes NOT Handle
RoomOne physical room and its static attributesroomNumber, roomType, floor—Its own availability calendar — delegates to RoomInventory
RoomInventoryPer-room calendar of booked date-range intervals; the overlap-checking authoritybookedIntervalsisAvailable(range), book(range), release(range)Pricing or guest identity
ReservationOne confirmed booking recordguest, room, checkIn, checkOut, status, totalPricecancel()Its own price calculation — delegates to PricingStrategy at booking time
ReservationRequest (built via Builder)An in-progress, not-yet-committed booking intent with optional fieldsdates, roomType, guestCount, addOns, specialRequests—Persistence or availability checking itself
ReservationRequestBuilderFluent, step-by-step construction of a ReservationRequest—withDates(), withRoomType(), withAddOn(), build()—
PricingStrategy (interface)One price-calculation rule—calculatePrice(room, checkIn, checkOut)—
BookingNotifierObserver subject for booking lifecycle events—notifyConfirmed(), notifyCancelled()Deciding booking eligibility itself
Hotel (facade)Top-level orchestrationrooms, inventorysearchAvailability(), bookRoom(), cancelReservation()—

I. Class Relationships

J. Class Diagram

1

1

Room

roomNumber

roomType

floor

RoomInventory

+isAvailable(range)

+book(range)

+release(range)

ReservationRequestBuilder

+withDates()

+withRoomType()

+withAddOn()

+build()

Reservation

checkIn

checkOut

status

totalPrice

+cancel()

«interface»

PricingStrategy

+calculatePrice(room, in, out)

BookingNotifier

+notifyConfirmed()

+notifyCancelled()

Hotel

+searchAvailability()

+bookRoom()

+cancelReservation()

View Mermaid source
classDiagram
  class Room {
    roomNumber
    roomType
    floor
  }
  class RoomInventory {
    +isAvailable(range)
    +book(range)
    +release(range)
  }
  class ReservationRequestBuilder {
    +withDates()
    +withRoomType()
    +withAddOn()
    +build()
  }
  class Reservation {
    checkIn
    checkOut
    status
    totalPrice
    +cancel()
  }
  class PricingStrategy {
    <<interface>>
    +calculatePrice(room, in, out)
  }
  class BookingNotifier {
    +notifyConfirmed()
    +notifyCancelled()
  }
  class Hotel {
    +searchAvailability()
    +bookRoom()
    +cancelReservation()
  }
  Room "1" --> "1" RoomInventory
  ReservationRequestBuilder --> Reservation
  Reservation --> PricingStrategy
  Reservation --> BookingNotifier
  Hotel --> RoomInventory
  Hotel --> Reservation

K. Responsibility Mapping

ClassResponsibilityDoes NOT Handle
RoomInventoryPer-room interval overlap checking and booking/releasePricing, guest identity
ReservationRequestBuilderStep-by-step, validated request constructionAvailability checking, persistence
PricingStrategy implementationsOne price formulaBooking commitment itself
BookingNotifierFan-out of booking lifecycle events to interested partiesBooking eligibility decisions
HotelTop-level orchestrationInterval math, price math

L. SOLID Analysis

PrincipleHow It's AppliedNaive Violation
SRPRoomInventory owns overlap logic; PricingStrategy owns pricing; BookingNotifier owns notification fan-outOne Hotel god-class checking overlap, computing price, and sending emails all inline
OCPA new pricing rule is a new PricingStrategy; a new request field is a new Builder stepA pricing if/else chain keyed on season, edited every time a new promotional period is added
LSPEvery PricingStrategy implementation honestly returns a price for the same method signature regardless of internal complexityA "premium" strategy that also silently mutates the reservation's dates as a side effect
ISPPricingStrategy stays a single-method interfaceA combined "BookingPolicy" interface mixing pricing with cancellation-refund and overbooking rules that don't always vary together
DIPReservation depends on the PricingStrategy abstraction, not a hardcoded rate tableReservation.calculatePrice() embedding a season lookup table directly on the entity

M. Design Patterns

PatternProblem It SolvesWhere It AppearsWhen NOT to Use
BuilderA reservation request has several optional parts that don't all apply to every bookingReservationRequestBuilderSkip if a booking only ever needs dates and a room type — a plain constructor is enough
StrategyPrice varies by room type, season, and demand, and changes more often than booking logicPricingStrategySkip if the round only ever needs one flat rate per room type
ObserverBooking confirmation/cancellation shouldn't require the booking code path to know who needs tellingBookingNotifierSkip if notifications are entirely out of scope for the round

Explicitly avoided: a single boolean isAvailable flag on Room — see I for why this cannot represent partial-date-range availability, which is the defining shape of this problem.

N. What Is Likely to Change?

BehaviorCurrent DesignAnticipated ChangeProtecting Abstraction
Pricing rulesRoom-type-and-season-basedReal-time demand pricing, loyalty discounts, promotional codesPricingStrategy interface
Reservation request fieldsDates, room type, guest countAdd-ons, special requests, group bookingsNew Builder steps, additive to ReservationRequest
Cancellation termsSimple cancel-and-releaseTiered refund windows, non-refundable rate plansA future CancellationPolicy strategy, same shape as PricingStrategy

O. Design Evolution

What stays stable: Hotel's public search/book/cancel API. New pricing rules and request fields are additive.

P. API / Interface Design

interface PricingStrategy {
  Money calculatePrice(Room room, Date checkIn, Date checkOut);
}

class ReservationRequestBuilder {
  ReservationRequestBuilder withDates(Date checkIn, Date checkOut);
  ReservationRequestBuilder withRoomType(RoomType type);
  ReservationRequestBuilder withAddOn(AddOn addOn);
  ReservationRequest build();
}

class Hotel {
  List<Room> searchAvailability(Date checkIn, Date checkOut, RoomType type);
  Reservation bookRoom(Guest guest, ReservationRequest request);
  void cancelReservation(String reservationId);
}

Ownership/validation notes: bookRoom() locates a specific Room whose RoomInventory reports the requested range free, atomically reserves that interval, and only then constructs the Reservation and prices it — availability is never assumed to still hold between the search call and the book call.

Q. Pseudocode — Critical Logic

Hotel.bookRoom(guest, request):
  candidateRoom = pickCandidate(request.roomType)
  lock(candidateRoom.inventory)
  try:
    if not candidateRoom.inventory.isAvailable(request.range): reject
    candidateRoom.inventory.book(request.range)
    price = pricingStrategyFor(candidateRoom).calculatePrice(candidateRoom, request.range)
    reservation = new Reservation(guest, candidateRoom, request.range, price)
    bookingNotifier.notifyConfirmed(reservation)
    return reservation
  finally:
    unlock(candidateRoom.inventory)

RoomInventory.isAvailable(range):
  return bookedIntervals.none(existing => existing.overlaps(range))

R. End-to-End Use Case

guestA.searchAvailability(Mar 1–5, Deluxe) → Room 204 returned as free
  │
  ▼
guestA.bookRoom(Room 204, Mar 1–5)
  │  lock(Room 204.inventory) → re-check still free → book interval → unlock
  │  price computed via SeasonalPricingStrategy → Reservation created
  │  BookingNotifier.notifyConfirmed() → guestA emailed, front desk updated
  ▼
guestA.cancelReservation() before Mar 1 → interval released, BookingNotifier.notifyCancelled()

S. Data Structures

StructureUsed ForWhyAlternative & Trade-off
Sorted list / interval tree of booked ranges per room in RoomInventoryOverlap checkingSupports O(log n) overlap queries as a room accumulates many bookings over time, instead of scanning every bookingA plain unsorted list is fine for a small demo but degrades to O(n) per check
Index of rooms by typeType-filtered availability searchAvoids scanning every room in the hotel for every searchFull scan is acceptable for a small single-property hotel

T. Complexity Analysis

OperationTimeSpace
isAvailable(range) (interval tree)O(log n + k) where k = overlapping candidatesO(n) bookings per room
bookRoom()O(log n) for the overlap check + O(1) commitO(1)
searchAvailability() across a hotelO(rooms of matching type × log n)O(1) beyond the inventory structures

U. Testing Strategy

V. Interviewer Follow-Ups

Follow-UpTestingDesign ChangeClasses ChangedPattern/Trade-off
"Add demand-based dynamic pricing."Whether pricing was ever hardcoded inlineNew DemandBasedPricingStrategy reading current occupancyNew strategy onlyValidates the Strategy boundary from I
"Support group bookings of multiple rooms in one reservation."Whether Reservation assumed exactly one roomReservation holds a list of rooms; booking must lock and verify all rooms together, in a fixed orderReservation, booking flowDirectly reuses the canonical-ordering deadlock-avoidance lesson from File System (Problem 9)
"Allow intentional overbooking with a no-show margin."Whether availability was a hard boolean invariant baked in everywhereOverlap check becomes probabilistic/threshold-based rather than a hard exclusionRoomInventory onlyTests whether the hard-invariant assumption from F was actually isolated behind one class

W. Concurrency

Shared mutable state: each room's booked-interval collection in RoomInventory, particularly for popular rooms/dates.

X. Persistence

Reservations and booked intervals are durable by nature — a booking that disappears on restart is a correctness failure, not a caching inconvenience. Each room's booked intervals are naturally repository-backed, and the exclusion-constraint approach from W is specifically a persistence-layer technique, not an in-memory one.

Y. Scaling Boundary

LayerWhat Belongs Here
LLDRoom/RoomInventory model, interval overlap checking, pricing strategy, per-room locking
HLDMulti-property search federation, distributed inventory across data centers, third-party OTA (Expedia/Booking.com-style) integration
Explicitly out of scopeCross-property or multi-tenant systems unless the round pivots there

Z. Failure Scenarios

AA. Common Bad Designs

AB. Design Trade-Offs

DecisionOption AOption BRecommendation
Overlap enforcementApplication-level lock + re-checkDatabase exclusion constraint on a range typeExclusion constraint in production (removes an entire class of application bugs); application lock is the right level of detail to describe in an interview if the schema/DB isn't the focus
Inventory granularityPer specific physical roomPer room type (undifferentiated pool)Per specific room when a guest needs a specific room; per type is simpler and sufficient if any room of a type is interchangeable

AC. Interviewer Evaluation

Strong signal: modeling availability as intervals unprompted; isolating pricing behind a Strategy before being asked for a second pricing rule; catching the overlapping-booking race and proposing either a lock or an exclusion constraint without being walked into it.

Red flag: a boolean availability flag; checking-then-booking as two unlocked steps; hardcoded seasonal price constants; a god-object Hotel class.

AD. Interview Simulation

Interviewer: Design a hotel reservation system.
Candidate: The key modeling decision is that a room's availability isn't a single flag — it's a set of booked date intervals. I'd give each Room a RoomInventory that answers "is this range free" via overlap checks against its booked intervals, not a boolean that can't represent partial-date availability.
Interviewer: How would you handle a booking request with optional extras like breakfast or late checkout?
Candidate: I'd use a Builder for the reservation request — ReservationRequestBuilder — since the request has several optional parts that don't all apply to every booking. It validates once at build() instead of forcing every caller through a huge constructor or leaving partial state to be validated ad hoc.
Interviewer: Two guests try to book the same room for overlapping dates at the same instant.
Candidate: Same fix as the last-copy race in the Library problem or the balance race in the ATM problem — lock that room's inventory, re-verify the range is still free, then commit. In production I'd actually push this to the database with an exclusion constraint on a date-range column, so the non-overlap invariant is enforced at the storage layer instead of relying purely on application-level locking.
Interviewer: How does pricing work if it varies by season?
Candidate: Behind a PricingStrategy interface, keyed by room type and the date range's season — a new promotional rule or demand-based rule becomes a new strategy, not a change to the booking flow itself.

AE. Online Solution Comparison

SourceApproachStrengthWeaknessWorth Learning
Medium — Designing a Hotel Booking System (Low Level Design)Entity-and-class walkthrough covering rooms, bookings, and a basic pricing notionApproachable introduction to the room/reservation entity splitAvailability is generally presented closer to a status flag than an explicit interval-overlap structure, and concurrency on overlapping bookings isn't the central focusUseful as an entity-modeling starting point; this document's interval-based RoomInventory and the overlapping-booking race are the parts worth adding on top
ashishps1/awesome-low-level-design — Hotel Management SystemA curated, runnable reference solution with room, reservation, and hotel classesReal, buildable code to cross-check class shapes against an independent, community-vetted sourceBreadth-over-depth — a working solution without the same depth of concurrency/trade-off analysis as a dedicated write-upGood for spot-checking that this document's entity boundaries match a widely-referenced independent implementation

AF. Interview Cheat Sheet

ProblemHotel Reservation System
DifficultyIntermediate → Advanced (concurrency follow-up)
Core requirementsDate-range search, booking, cancellation, room-type/season-aware pricing, booking notifications
Key clarificationsPer-room or per-type inventory? Pricing variation? Overbooking allowed? Cancellation/refund policy?
Core entitiesRoom, RoomInventory, Reservation, ReservationRequest, ReservationRequestBuilder, PricingStrategy, BookingNotifier, Hotel
Key relationshipsAvailability modeled as booked intervals, not a flag; Builder for requests; Strategy for pricing; Observer for notifications
Key patternsBuilder, Strategy, Observer
Most important decisionModeling availability as a date-range interval structure instead of a boolean flag
Likely-changing behaviorPricing rules, reservation request fields, cancellation/refund terms
Concurrency concernCheck-then-act race on overlapping date-range bookings for the same room
Top follow-upsDemand-based pricing, multi-room group bookings, intentional overbooking
Common mistakesBoolean availability flag; unlocked check-then-book from the caller; hardcoded seasonal constants; god-object Hotel class
What NOT to over-engineerDon't design multi-property/OTA federation unless asked

AG. Full Implementation

A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Problem 16: Hotel Reservation System
 *
 * Key design decisions faithfully reproduced from the class diagram:
 *  - Room (static attrs) vs RoomInventory (per-room booked-interval calendar) split.
 *  - ReservationRequestBuilder assembles an in-progress booking intent step by step.
 *  - PricingStrategy is injected (Strategy) so pricing rules can change independently.
 *  - BookingNotifier is a thin Observer subject for booking lifecycle events.
 *  - Hotel is the facade orchestrating search/book/cancel.
 */
public class p16 {

    // ---------- Shared value types ----------

    enum RoomType { SINGLE, DOUBLE, SUITE, DELUXE }

    enum AddOn { BREAKFAST, LATE_CHECKOUT, AIRPORT_PICKUP, EXTRA_BED }

    enum ReservationStatus { CONFIRMED, CANCELLED }

    /** Half-open [checkIn, checkOut) interval. Overlap math lives here, not on Room. */
    static final class DateRange {
        final LocalDate checkIn;
        final LocalDate checkOut;

        DateRange(LocalDate checkIn, LocalDate checkOut) {
            if (!checkIn.isBefore(checkOut)) throw new IllegalArgumentException("checkIn must be before checkOut");
            this.checkIn = checkIn;
            this.checkOut = checkOut;
        }

        boolean overlaps(DateRange other) {
            return this.checkIn.isBefore(other.checkOut) && other.checkIn.isBefore(this.checkOut);
        }

        long nights() {
            return checkOut.toEpochDay() - checkIn.toEpochDay();
        }

        @Override public String toString() { return checkIn + " -> " + checkOut; }
    }

    static final class Guest {
        final String id, name, email;
        Guest(String id, String name, String email) { this.id = id; this.name = name; this.email = email; }
    }

    // ---------- Room / RoomInventory ----------

    /** Per-room calendar of booked intervals; the sole overlap-checking authority. */
    static final class RoomInventory {
        private final List<DateRange> bookedIntervals = new ArrayList<>();
        private final Lock lock = new ReentrantLock();

        boolean isAvailable(DateRange range) {
            lock.lock();
            try {
                return isAvailableInternal(range);
            } finally {
                lock.unlock();
            }
        }

        private boolean isAvailableInternal(DateRange range) {
            for (DateRange existing : bookedIntervals) {
                if (existing.overlaps(range)) return false;
            }
            return true;
        }

        /** Atomic check-then-book under the same lock, preventing double booking. */
        boolean book(DateRange range) {
            lock.lock();
            try {
                if (!isAvailableInternal(range)) return false;
                bookedIntervals.add(range);
                return true;
            } finally {
                lock.unlock();
            }
        }

        void release(DateRange range) {
            lock.lock();
            try {
                bookedIntervals.removeIf(r -> r.checkIn.equals(range.checkIn) && r.checkOut.equals(range.checkOut));
            } finally {
                lock.unlock();
            }
        }
    }

    static final class Room {
        final String roomNumber;
        final RoomType roomType;
        final int floor;
        private final RoomInventory inventory = new RoomInventory(); // composition: Room owns its inventory 1:1

        Room(String roomNumber, RoomType roomType, int floor) {
            this.roomNumber = roomNumber;
            this.roomType = roomType;
            this.floor = floor;
        }

        RoomInventory getInventory() { return inventory; }
    }

    // ---------- Reservation request + builder ----------

    static final class ReservationRequest {
        final DateRange range;
        final RoomType roomType;
        final int guestCount;
        final Set<AddOn> addOns;
        final String specialRequests;

        ReservationRequest(DateRange range, RoomType roomType, int guestCount, Set<AddOn> addOns, String specialRequests) {
            this.range = range;
            this.roomType = roomType;
            this.guestCount = guestCount;
            this.addOns = addOns;
            this.specialRequests = specialRequests;
        }
    }

    /** Fluent, step-by-step construction; validates once at build() instead of at every setter. */
    static final class ReservationRequestBuilder {
        private DateRange range;
        private RoomType roomType;
        private int guestCount = 1;
        private final Set<AddOn> addOns = new LinkedHashSet<>();
        private String specialRequests = "";

        ReservationRequestBuilder withDates(LocalDate checkIn, LocalDate checkOut) {
            this.range = new DateRange(checkIn, checkOut);
            return this;
        }

        ReservationRequestBuilder withRoomType(RoomType type) {
            this.roomType = type;
            return this;
        }

        ReservationRequestBuilder withGuestCount(int count) {
            this.guestCount = count;
            return this;
        }

        ReservationRequestBuilder withAddOn(AddOn addOn) {
            this.addOns.add(addOn);
            return this;
        }

        ReservationRequestBuilder withSpecialRequests(String text) {
            this.specialRequests = text;
            return this;
        }

        ReservationRequest build() {
            if (range == null) throw new IllegalStateException("dates are required");
            if (roomType == null) throw new IllegalStateException("roomType is required");
            return new ReservationRequest(range, roomType, guestCount, addOns, specialRequests);
        }
    }

    // ---------- Pricing strategy ----------

    interface PricingStrategy {
        double calculatePrice(Room room, DateRange range);
    }

    /** Flat per-night rate by room type. */
    static final class StandardPricingStrategy implements PricingStrategy {
        private final Map<RoomType, Double> baseRates = Map.of(
                RoomType.SINGLE, 80.0, RoomType.DOUBLE, 120.0, RoomType.SUITE, 220.0, RoomType.DELUXE, 350.0);

        @Override
        public double calculatePrice(Room room, DateRange range) {
            return baseRates.get(room.roomType) * range.nights();
        }
    }

    /** Adds a weekend surcharge on top of the base rate -- a *new* rule, no change to Reservation/Hotel. */
    static final class WeekendSurchargePricingStrategy implements PricingStrategy {
        private final PricingStrategy delegate;
        private final double weekendMultiplier;

        WeekendSurchargePricingStrategy(PricingStrategy delegate, double weekendMultiplier) {
            this.delegate = delegate;
            this.weekendMultiplier = weekendMultiplier;
        }

        @Override
        public double calculatePrice(Room room, DateRange range) {
            double perNightBase = delegate.calculatePrice(room, new DateRange(range.checkIn, range.checkIn.plusDays(1)));
            double total = 0;
            LocalDate day = range.checkIn;
            while (day.isBefore(range.checkOut)) {
                boolean isWeekend = day.getDayOfWeek() == DayOfWeek.SATURDAY || day.getDayOfWeek() == DayOfWeek.SUNDAY;
                total += isWeekend ? perNightBase * weekendMultiplier : perNightBase;
                day = day.plusDays(1);
            }
            return total;
        }
    }

    // ---------- Reservation ----------

    static final class Reservation {
        final String id;
        final Guest guest;
        final Room room;
        final DateRange range;
        ReservationStatus status;
        final double totalPrice;
        private final BookingNotifier notifier;

        Reservation(String id, Guest guest, Room room, DateRange range, double totalPrice, BookingNotifier notifier) {
            this.id = id;
            this.guest = guest;
            this.room = room;
            this.range = range;
            this.totalPrice = totalPrice;
            this.notifier = notifier;
            this.status = ReservationStatus.CONFIRMED;
        }

        void cancel() {
            if (status == ReservationStatus.CANCELLED) return;
            status = ReservationStatus.CANCELLED;
            room.getInventory().release(range);
            notifier.notifyCancelled(this);
        }
    }

    // ---------- Booking notifier (Observer) ----------

    interface BookingObserver {
        void onConfirmed(Reservation reservation);
        void onCancelled(Reservation reservation);
    }

    static final class BookingNotifier {
        private final List<BookingObserver> observers = new ArrayList<>();

        void subscribe(BookingObserver observer) { observers.add(observer); }

        void notifyConfirmed(Reservation reservation) {
            for (BookingObserver o : observers) o.onConfirmed(reservation);
        }

        void notifyCancelled(Reservation reservation) {
            for (BookingObserver o : observers) o.onCancelled(reservation);
        }
    }

    static final class ConsoleGuestNotifier implements BookingObserver {
        @Override
        public void onConfirmed(Reservation r) {
            System.out.printf("[notify] Reservation %s CONFIRMED for %s, room %s, %.2f total%n",
                    r.id, r.guest.name, r.room.roomNumber, r.totalPrice);
        }

        @Override
        public void onCancelled(Reservation r) {
            System.out.printf("[notify] Reservation %s CANCELLED for %s%n", r.id, r.guest.name);
        }
    }

    // ---------- Hotel (facade) ----------

    static final class Hotel {
        private final List<Room> rooms;
        private final PricingStrategy pricingStrategy;
        private final BookingNotifier notifier;
        private final Map<String, Reservation> reservations = new HashMap<>();
        private int reservationSeq = 0;

        Hotel(List<Room> rooms, PricingStrategy pricingStrategy, BookingNotifier notifier) {
            this.rooms = rooms;
            this.pricingStrategy = pricingStrategy;
            this.notifier = notifier;
        }

        List<Room> searchAvailability(LocalDate checkIn, LocalDate checkOut, RoomType type) {
            DateRange range = new DateRange(checkIn, checkOut);
            List<Room> result = new ArrayList<>();
            for (Room room : rooms) {
                if (type != null && room.roomType != type) continue;
                if (room.getInventory().isAvailable(range)) result.add(room);
            }
            return result;
        }

        Reservation bookRoom(Guest guest, ReservationRequest request) {
            for (Room candidate : rooms) {
                if (candidate.roomType != request.roomType) continue;
                // Atomic check-then-book lives inside RoomInventory.book(); try the next
                // candidate room of the same type if this one lost the race or was already booked.
                if (candidate.getInventory().book(request.range)) {
                    double price = pricingStrategy.calculatePrice(candidate, request.range);
                    String id = "RES-" + (++reservationSeq);
                    Reservation reservation = new Reservation(id, guest, candidate, request.range, price, notifier);
                    reservations.put(id, reservation);
                    notifier.notifyConfirmed(reservation);
                    return reservation;
                }
            }
            throw new IllegalStateException("No available room of type " + request.roomType + " for " + request.range);
        }

        void cancelReservation(String reservationId) {
            Reservation reservation = reservations.get(reservationId);
            if (reservation == null) throw new NoSuchElementException("Unknown reservation " + reservationId);
            reservation.cancel();
        }
    }

    // ---------- Demo ----------

    public static void main(String[] args) throws InterruptedException {
        List<Room> rooms = new ArrayList<>();
        rooms.add(new Room("101", RoomType.DOUBLE, 1));
        rooms.add(new Room("102", RoomType.DOUBLE, 1));
        rooms.add(new Room("201", RoomType.SUITE, 2));

        PricingStrategy pricing = new WeekendSurchargePricingStrategy(new StandardPricingStrategy(), 1.25);
        BookingNotifier notifier = new BookingNotifier();
        notifier.subscribe(new ConsoleGuestNotifier());

        Hotel hotel = new Hotel(rooms, pricing, notifier);

        LocalDate in = LocalDate.of(2026, 10, 2);
        LocalDate out = LocalDate.of(2026, 10, 5);

        System.out.println("Available DOUBLE rooms: " + hotel.searchAvailability(in, out, RoomType.DOUBLE).size());

        ReservationRequest request = new ReservationRequestBuilder()
                .withDates(in, out)
                .withRoomType(RoomType.DOUBLE)
                .withGuestCount(2)
                .withAddOn(AddOn.BREAKFAST)
                .build();

        Guest alice = new Guest("g1", "Alice", "alice@example.com");
        Reservation resA = hotel.bookRoom(alice, request);
        System.out.println("Booked room: " + resA.room.roomNumber + " price=" + resA.totalPrice);

        // Concurrency demo: two guests race for the SAME remaining double room + dates.
        Guest bob = new Guest("g2", "Bob", "bob@example.com");
        ReservationRequest sameRequest = new ReservationRequestBuilder().withDates(in, out).withRoomType(RoomType.DOUBLE).build();
        final Reservation[] results = new Reservation[2];
        Runnable attempt = () -> {
            try {
                results[0] = hotel.bookRoom(bob, sameRequest);
            } catch (Exception ignored) { }
        };
        Thread t1 = new Thread(attempt);
        Thread t2 = new Thread(attempt);
        t1.start(); t2.start();
        t1.join(); t2.join();
        System.out.println("Second concurrent booking landed on room: " + (results[0] != null ? results[0].room.roomNumber : "none"));

        System.out.println("Available DOUBLE rooms after bookings: " + hotel.searchAvailability(in, out, RoomType.DOUBLE).size());

        hotel.cancelReservation(resA.id);
        System.out.println("Available DOUBLE rooms after cancel: " + hotel.searchAvailability(in, out, RoomType.DOUBLE).size());
    }
}