Hotel/Room/RoomType/Reservation classes, efficient date-range availability search via interval representation, and overbooking as a deliberate business rule rather than a bug.
Published September 23, 2026
class Hotel { List<Room> rooms; }
class RoomType { String name; double basePrice; int capacity; } // Standard, Deluxe, Suite
class Room { String number; RoomType type; }
class Reservation { Room room; Guest guest; LocalDate checkIn; LocalDate checkOut; }
The naive approach — for each candidate room, scan every existing reservation checking for date overlap — is O(rooms * reservations per room) per search, which gets slow as reservation history grows. The better representation: for each room, keep reservations in a structure that supports fast interval overlap queries.
class Room {
private final TreeMap<LocalDate, Reservation> reservationsByCheckIn = new TreeMap<>(); // sorted by check-in date
boolean isAvailable(LocalDate checkIn, LocalDate checkOut) {
// Find the reservation with the latest check-in date that's still <= requested checkOut
Map.Entry<LocalDate, Reservation> candidate = reservationsByCheckIn.floorEntry(checkOut.minusDays(1));
if (candidate == null) return true; // no reservation starts before the requested window ends
Reservation r = candidate.getValue();
return !r.getCheckOut().isAfter(checkIn); // the closest-preceding reservation must fully end before our check-in
}
}
A TreeMap sorted by check-in date turns "is there any overlapping reservation" into a single floorEntry() lookup (O(log n)) that finds the most relevant candidate directly, rather than scanning every reservation for this room. This is the same underlying idea as TreeMap & LinkedHashMap's NavigableMap range-query methods, applied to interval scheduling specifically.
Real hotels (and airlines — see Airline Booking / Seat Selection System) deliberately overbook by a small, statistically-modeled margin, betting that historical no-show rates will absorb the overage. This is worth naming explicitly in a design discussion: a strict "never allow more reservations than physical rooms" constraint is the simpler design, but a real hotel booking system needs an explicit overbooking policy as a configurable business parameter (e.g. "allow up to 5% overbooking on RoomType") — and critically, needs a defined contingency process for the rare case where overbooking doesn't get absorbed by no-shows (walking a guest to a partner hotel, compensation). Treating overbooking as an unambiguous bug to eliminate, rather than asking whether it's an intentional business lever, is a common miss in this exact prompt.
Guests book a room type for a date range ("a Deluxe for 12–15 March"). The specific room is usually assigned at check-in. That makes availability a counting problem per type and per night, which is simpler and faster than scanning individual rooms:
class RoomTypeInventory {
private final RoomType type;
private final int totalRooms;
private final Map<LocalDate, Integer> booked = new HashMap<>(); // nights already sold
private final double overbookingFactor; // e.g. 1.05 = sell up to 5% over
RoomTypeInventory(RoomType type, int totalRooms, double overbookingFactor) {
this.type = type; this.totalRooms = totalRooms; this.overbookingFactor = overbookingFactor;
}
private int sellable() { return (int) Math.floor(totalRooms * overbookingFactor); }
/** Nights are [checkIn, checkOut): a guest checking out on the 15th doesn't occupy the night of the 15th. */
synchronized boolean isAvailable(LocalDate checkIn, LocalDate checkOut, int rooms) {
for (LocalDate d = checkIn; d.isBefore(checkOut); d = d.plusDays(1))
if (booked.getOrDefault(d, 0) + rooms > sellable()) return false;
return true;
}
synchronized boolean reserve(LocalDate checkIn, LocalDate checkOut, int rooms) {
if (!isAvailable(checkIn, checkOut, rooms)) return false; // check and write under ONE lock
for (LocalDate d = checkIn; d.isBefore(checkOut); d = d.plusDays(1))
booked.merge(d, rooms, Integer::sum);
return true;
}
synchronized void cancel(LocalDate checkIn, LocalDate checkOut, int rooms) {
for (LocalDate d = checkIn; d.isBefore(checkOut); d = d.plusDays(1))
booked.merge(d, -rooms, Integer::sum);
}
}
Details worth saying out loud:
[checkIn, checkOut): the checkout day is free for the next guest. Off-by-one errors here cause real double bookings.(room_type_id, date, total, booked) rows. A reservation increments booked on each night's row with a conditional update (WHERE booked + :n <= total * :factor), inside one transaction.class RoomAssigner {
Optional<Room> assign(Reservation r, List<Room> roomsOfType, Set<String> occupiedTonight) {
return roomsOfType.stream()
.filter(room -> !occupiedTonight.contains(room.number))
.filter(room -> room.status == RoomStatus.CLEAN)
.min(Comparator.comparing((Room room) -> !r.guest().prefersHighFloor() || room.floor < 5)
.thenComparing(room -> room.number));
}
}
Assignment can consider preferences (floor, accessibility, connecting rooms) and housekeeping status. Separating selling nights from assigning rooms is what lets hotels keep flexibility, for example moving guests between rooms of the same type for maintenance, without touching reservations.
PENDING (held while paying) → CONFIRMED → CHECKED_IN → CHECKED_OUT, with CANCELLED and NO_SHOW as side exits. Each transition does something to the inventory:
The price of a stay is the sum of nightly rates, and each night may differ (weekends, seasons, events, occupancy). Model a RatePlan that returns a price for (room type, date), and compute the stay total by summing over the nights. A flat basePrice × nights breaks as soon as a stay spans a weekend.
Q: How would you extend isAvailable() to search across an entire RoomType rather than one specific room? A: Iterate the RoomType's rooms checking isAvailable() on each, returning the first (or all) available ones — the per-room interval check is the same, this just adds an outer loop; for a hotel with many rooms per type, this could be further optimized by only checking a subset if the caller just needs 'at least one available' rather than a full list.
Q: What's a concrete failure mode of the naive full-scan availability check at scale? A: A large hotel chain with years of reservation history and thousands of rooms would make every search request scan thousands of reservation records per room checked — the TreeMap approach keeps each individual room's lookup to O(log n) regardless of how much reservation history has accumulated.
Q: How would overbooking policy interact with the reservation confirmation flow from Movie Ticket Booking System's temporary-hold pattern? A: The same hold-with-expiry pattern applies directly — a room search result being 'available' doesn't guarantee it stays available through checkout, so a temporary hold during the booking flow prevents two guests from both confirming the same (already-overbooked-to-its-limit) room type simultaneously, exactly the same race the seat-booking hold pattern prevents.
Q: Should room pricing be a field on Room, or computed dynamically? A: Modeling it as dynamically computed (a PricingStrategy, matching the Strategy pattern used throughout this course) is generally the better design — real hotel pricing varies by date, demand, and length of stay, which a static price field on Room can't express, while a pluggable pricing strategy keeps that variability isolated from the room/reservation data model itself.
Q: Why track availability per room type and night rather than per room? A: Guests buy a room type for a set of nights. Counting sold nights against capacity makes availability checks O(nights), and keeps specific room assignment flexible until check-in, when housekeeping status and preferences are known.
Q: How do you prevent two guests from booking the last room at the same moment? A: Do the availability check and the increment atomically: under one lock in memory, or in a single transaction with conditional updates on each night's inventory row in a database. If any night fails the condition, roll back the whole reservation.
Q: Why are hotel stays usually modeled as [checkIn, checkOut)?
A: The guest occupies the nights starting on each date from check-in up to, but not including, the check-out date. Treating the check-out date as occupied would block valid back-to-back bookings, and double-count the night.