"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."
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.
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());
}
}
// Problem 16: Hotel Reservation System
//
// Room (static attrs) vs RoomInventory (per-room booked-interval calendar) split.
// ReservationRequestBuilder assembles a booking intent step by step.
// PricingStrategy is injected (Strategy) so pricing rules change independently.
// BookingNotifier is a thin Observer subject for booking lifecycle events.
// Hotel is the facade orchestrating search/book/cancel.
#include <iostream>
#include <vector>
#include <memory>
#include <string>
#include <map>
#include <set>
#include <mutex>
#include <thread>
#include <stdexcept>
#include <sstream>
// ---------- Simple date type (days since epoch-ish counter is enough for this demo) ----------
struct SimpleDate {
int y, m, d;
long toOrdinal() const { return y * 372L + m * 31L + d; } // stable-enough ordering for a demo
int dayOfWeekMod7() const { return static_cast<int>(toOrdinal() % 7); } // pretend weekend detector
bool isBefore(const SimpleDate& o) const { return toOrdinal() < o.toOrdinal(); }
SimpleDate plusDays(int n) const {
// Minimal, non-calendar-accurate increment sufficient for the pricing loop below.
SimpleDate copy = *this;
for (int i = 0; i < n; ++i) {
copy.d++;
if (copy.d > 28) { copy.d = 1; copy.m++; if (copy.m > 12) { copy.m = 1; copy.y++; } }
}
return copy;
}
std::string toString() const {
std::ostringstream os; os << y << "-" << m << "-" << d; return os.str();
}
};
enum class RoomType { SINGLE, DOUBLE, SUITE, DELUXE };
enum class AddOn { BREAKFAST, LATE_CHECKOUT, AIRPORT_PICKUP, EXTRA_BED };
enum class ReservationStatus { CONFIRMED, CANCELLED };
// Half-open [checkIn, checkOut) interval; overlap math lives here, not on Room.
struct DateRange {
SimpleDate checkIn, checkOut;
DateRange(SimpleDate in, SimpleDate out) : checkIn(in), checkOut(out) {
if (!in.isBefore(out)) throw std::invalid_argument("checkIn must be before checkOut");
}
bool overlaps(const DateRange& other) const {
return checkIn.isBefore(other.checkOut) && other.checkIn.isBefore(checkOut);
}
long nights() const { return checkOut.toOrdinal() - checkIn.toOrdinal(); }
};
struct Guest {
std::string id, name, email;
};
// Per-room calendar of booked intervals; the sole overlap-checking authority.
class RoomInventory {
public:
bool isAvailable(const DateRange& range) {
std::lock_guard<std::mutex> lock(mutex_);
return isAvailableInternal(range);
}
// Atomic check-then-book under the same lock, preventing double booking.
bool book(const DateRange& range) {
std::lock_guard<std::mutex> lock(mutex_);
if (!isAvailableInternal(range)) return false;
bookedIntervals_.push_back(range);
return true;
}
void release(const DateRange& range) {
std::lock_guard<std::mutex> lock(mutex_);
for (auto it = bookedIntervals_.begin(); it != bookedIntervals_.end(); ++it) {
if (it->checkIn.toOrdinal() == range.checkIn.toOrdinal() &&
it->checkOut.toOrdinal() == range.checkOut.toOrdinal()) {
bookedIntervals_.erase(it);
return;
}
}
}
private:
bool isAvailableInternal(const DateRange& range) const {
for (const auto& existing : bookedIntervals_) if (existing.overlaps(range)) return false;
return true;
}
std::vector<DateRange> bookedIntervals_;
std::mutex mutex_;
};
class Room {
public:
std::string roomNumber;
RoomType roomType;
int floor;
Room(std::string number, RoomType type, int fl) : roomNumber(std::move(number)), roomType(type), floor(fl) {}
RoomInventory& getInventory() { return inventory_; } // composition: Room owns its inventory 1:1
private:
RoomInventory inventory_;
};
struct ReservationRequest {
DateRange range;
RoomType roomType;
int guestCount;
std::set<AddOn> addOns;
std::string specialRequests;
};
// Fluent, step-by-step construction; validates once at build() instead of at every setter.
class ReservationRequestBuilder {
public:
ReservationRequestBuilder& withDates(SimpleDate in, SimpleDate out) {
range_ = std::make_unique<DateRange>(in, out);
return *this;
}
ReservationRequestBuilder& withRoomType(RoomType type) { roomType_ = type; hasType_ = true; return *this; }
ReservationRequestBuilder& withGuestCount(int count) { guestCount_ = count; return *this; }
ReservationRequestBuilder& withAddOn(AddOn addOn) { addOns_.insert(addOn); return *this; }
ReservationRequestBuilder& withSpecialRequests(std::string text) { specialRequests_ = std::move(text); return *this; }
ReservationRequest build() {
if (!range_) throw std::logic_error("dates are required");
if (!hasType_) throw std::logic_error("roomType is required");
return ReservationRequest{*range_, roomType_, guestCount_, addOns_, specialRequests_};
}
private:
std::unique_ptr<DateRange> range_;
RoomType roomType_ = RoomType::SINGLE;
bool hasType_ = false;
int guestCount_ = 1;
std::set<AddOn> addOns_;
std::string specialRequests_;
};
class PricingStrategy {
public:
virtual ~PricingStrategy() = default;
virtual double calculatePrice(const Room& room, const DateRange& range) = 0;
};
// Flat per-night rate by room type.
class StandardPricingStrategy : public PricingStrategy {
public:
double calculatePrice(const Room& room, const DateRange& range) override {
return baseRate(room.roomType) * static_cast<double>(range.nights());
}
static double baseRate(RoomType type) {
switch (type) {
case RoomType::SINGLE: return 80.0;
case RoomType::DOUBLE: return 120.0;
case RoomType::SUITE: return 220.0;
case RoomType::DELUXE: return 350.0;
}
return 0.0;
}
};
// Adds a weekend surcharge on top of a delegate rate -- a *new* rule, no change to Reservation/Hotel.
class WeekendSurchargePricingStrategy : public PricingStrategy {
public:
WeekendSurchargePricingStrategy(std::shared_ptr<PricingStrategy> delegate, double multiplier)
: delegate_(std::move(delegate)), multiplier_(multiplier) {}
double calculatePrice(const Room& room, const DateRange& range) override {
DateRange oneNight(range.checkIn, range.checkIn.plusDays(1));
double perNight = delegate_->calculatePrice(room, oneNight);
double total = 0;
SimpleDate day = range.checkIn;
while (day.isBefore(range.checkOut)) {
bool isWeekend = day.dayOfWeekMod7() == 5 || day.dayOfWeekMod7() == 6;
total += isWeekend ? perNight * multiplier_ : perNight;
day = day.plusDays(1);
}
return total;
}
private:
std::shared_ptr<PricingStrategy> delegate_;
double multiplier_;
};
class Reservation;
class BookingObserver {
public:
virtual ~BookingObserver() = default;
virtual void onConfirmed(const Reservation& reservation) = 0;
virtual void onCancelled(const Reservation& reservation) = 0;
};
class BookingNotifier {
public:
void subscribe(std::shared_ptr<BookingObserver> observer) { observers_.push_back(std::move(observer)); }
void notifyConfirmed(const Reservation& reservation) { for (auto& o : observers_) o->onConfirmed(reservation); }
void notifyCancelled(const Reservation& reservation) { for (auto& o : observers_) o->onCancelled(reservation); }
private:
std::vector<std::shared_ptr<BookingObserver>> observers_;
};
class Reservation {
public:
std::string id;
Guest guest;
Room* room;
DateRange range;
ReservationStatus status;
double totalPrice;
Reservation(std::string id_, Guest guest_, Room* room_, DateRange range_, double price, BookingNotifier* notifier)
: id(std::move(id_)), guest(std::move(guest_)), room(room_), range(range_),
status(ReservationStatus::CONFIRMED), totalPrice(price), notifier_(notifier) {}
void cancel() {
if (status == ReservationStatus::CANCELLED) return;
status = ReservationStatus::CANCELLED;
room->getInventory().release(range);
notifier_->notifyCancelled(*this);
}
private:
BookingNotifier* notifier_;
};
class ConsoleGuestNotifier : public BookingObserver {
public:
void onConfirmed(const Reservation& r) override {
std::cout << "[notify] Reservation " << r.id << " CONFIRMED for " << r.guest.name
<< ", room " << r.room->roomNumber << ", " << r.totalPrice << " total\n";
}
void onCancelled(const Reservation& r) override {
std::cout << "[notify] Reservation " << r.id << " CANCELLED for " << r.guest.name << "\n";
}
};
// Facade: top-level orchestration.
class Hotel {
public:
Hotel(std::vector<std::unique_ptr<Room>> rooms, std::shared_ptr<PricingStrategy> pricing, std::shared_ptr<BookingNotifier> notifier)
: rooms_(std::move(rooms)), pricingStrategy_(std::move(pricing)), notifier_(std::move(notifier)) {}
std::vector<Room*> searchAvailability(SimpleDate in, SimpleDate out, RoomType type) {
DateRange range(in, out);
std::vector<Room*> result;
for (auto& room : rooms_) {
if (room->roomType != type) continue;
if (room->getInventory().isAvailable(range)) result.push_back(room.get());
}
return result;
}
std::shared_ptr<Reservation> bookRoom(const Guest& guest, const ReservationRequest& request) {
for (auto& 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);
std::string id = "RES-" + std::to_string(++reservationSeq_);
auto reservation = std::make_shared<Reservation>(id, guest, candidate.get(), request.range, price, notifier_.get());
std::lock_guard<std::mutex> lock(reservationsMutex_);
reservations_[id] = reservation;
notifier_->notifyConfirmed(*reservation);
return reservation;
}
}
throw std::runtime_error("No available room of requested type for given dates");
}
void cancelReservation(const std::string& reservationId) {
std::lock_guard<std::mutex> lock(reservationsMutex_);
auto it = reservations_.find(reservationId);
if (it == reservations_.end()) throw std::runtime_error("Unknown reservation " + reservationId);
it->second->cancel();
}
private:
std::vector<std::unique_ptr<Room>> rooms_;
std::shared_ptr<PricingStrategy> pricingStrategy_;
std::shared_ptr<BookingNotifier> notifier_;
std::map<std::string, std::shared_ptr<Reservation>> reservations_;
std::mutex reservationsMutex_;
int reservationSeq_ = 0;
};
int main() {
std::vector<std::unique_ptr<Room>> rooms;
rooms.push_back(std::make_unique<Room>("101", RoomType::DOUBLE, 1));
rooms.push_back(std::make_unique<Room>("102", RoomType::DOUBLE, 1));
rooms.push_back(std::make_unique<Room>("201", RoomType::SUITE, 2));
auto standard = std::make_shared<StandardPricingStrategy>();
auto pricing = std::make_shared<WeekendSurchargePricingStrategy>(standard, 1.25);
auto notifier = std::make_shared<BookingNotifier>();
notifier->subscribe(std::make_shared<ConsoleGuestNotifier>());
Hotel hotel(std::move(rooms), pricing, notifier);
SimpleDate in{2026, 10, 2};
SimpleDate out{2026, 10, 5};
std::cout << "Available DOUBLE rooms: " << hotel.searchAvailability(in, out, RoomType::DOUBLE).size() << "\n";
ReservationRequest request = ReservationRequestBuilder()
.withDates(in, out)
.withRoomType(RoomType::DOUBLE)
.withGuestCount(2)
.withAddOn(AddOn::BREAKFAST)
.build();
Guest alice{"g1", "Alice", "alice@example.com"};
auto resA = hotel.bookRoom(alice, request);
std::cout << "Booked room: " << resA->room->roomNumber << " price=" << resA->totalPrice << "\n";
// Concurrency demo: two guests race for the SAME remaining double room + dates.
Guest bob{"g2", "Bob", "bob@example.com"};
ReservationRequest sameRequest = ReservationRequestBuilder().withDates(in, out).withRoomType(RoomType::DOUBLE).build();
std::shared_ptr<Reservation> winner;
std::mutex winnerMutex;
auto attempt = [&]() {
try {
auto r = hotel.bookRoom(bob, sameRequest);
std::lock_guard<std::mutex> lock(winnerMutex);
winner = r;
} catch (...) { }
};
std::thread t1(attempt), t2(attempt);
t1.join(); t2.join();
std::cout << "Second concurrent booking landed on room: " << (winner ? winner->room->roomNumber : "none") << "\n";
std::cout << "Available DOUBLE rooms after bookings: " << hotel.searchAvailability(in, out, RoomType::DOUBLE).size() << "\n";
hotel.cancelReservation(resA->id);
std::cout << "Available DOUBLE rooms after cancel: " << hotel.searchAvailability(in, out, RoomType::DOUBLE).size() << "\n";
return 0;
}
'use strict';
// Problem 16: Hotel Reservation System
//
// Room (static attrs) vs RoomInventory (per-room booked-interval calendar) split.
// ReservationRequestBuilder assembles a booking intent step by step.
// PricingStrategy is injected (Strategy) so pricing rules change independently.
// BookingNotifier is a thin Observer subject for booking lifecycle events.
// Hotel is the facade orchestrating search/book/cancel.
//
// Node's event loop is single-threaded, so true data races are impossible here;
// the AsyncMutex below still models the same "lock the inventory during the
// check-then-book" shape as the Java/C++ versions for structural fidelity, and
// matters for real correctness once any I/O (e.g. a DB call) is added inside it.
const RoomType = Object.freeze({ SINGLE: 'SINGLE', DOUBLE: 'DOUBLE', SUITE: 'SUITE', DELUXE: 'DELUXE' });
const AddOn = Object.freeze({ BREAKFAST: 'BREAKFAST', LATE_CHECKOUT: 'LATE_CHECKOUT', AIRPORT_PICKUP: 'AIRPORT_PICKUP', EXTRA_BED: 'EXTRA_BED' });
const ReservationStatus = Object.freeze({ CONFIRMED: 'CONFIRMED', CANCELLED: 'CANCELLED' });
/** A minimal FIFO mutual-exclusion primitive: a queue of waiters, one holder at a time. */
class AsyncMutex {
constructor() { this._locked = false; this._waiters = []; }
_acquire() {
return new Promise((resolve) => {
if (!this._locked) { this._locked = true; resolve(); }
else this._waiters.push(resolve);
});
}
_release() {
if (this._waiters.length > 0) this._waiters.shift()();
else this._locked = false;
}
async runExclusive(fn) {
await this._acquire();
try {
return await fn();
} finally {
this._release();
}
}
}
/** Half-open [checkIn, checkOut) interval; overlap math lives here, not on Room. */
class DateRange {
constructor(checkIn, checkOut) {
if (!(checkIn < checkOut)) throw new Error('checkIn must be before checkOut');
this.checkIn = checkIn;
this.checkOut = checkOut;
}
overlaps(other) { return this.checkIn < other.checkOut && other.checkIn < this.checkOut; }
nights() { return Math.round((this.checkOut - this.checkIn) / 86400000); }
}
class Guest {
constructor(id, name, email) { this.id = id; this.name = name; this.email = email; }
}
/** Per-room calendar of booked intervals; the sole overlap-checking authority. */
class RoomInventory {
constructor() {
this._bookedIntervals = [];
this._mutex = new AsyncMutex();
}
_isAvailableInternal(range) {
return this._bookedIntervals.every((existing) => !existing.overlaps(range));
}
async isAvailable(range) {
return this._mutex.runExclusive(() => this._isAvailableInternal(range));
}
// Atomic check-then-book under the same lock, preventing double booking.
async book(range) {
return this._mutex.runExclusive(() => {
if (!this._isAvailableInternal(range)) return false;
this._bookedIntervals.push(range);
return true;
});
}
async release(range) {
return this._mutex.runExclusive(() => {
this._bookedIntervals = this._bookedIntervals.filter(
(r) => !(r.checkIn.getTime() === range.checkIn.getTime() && r.checkOut.getTime() === range.checkOut.getTime())
);
});
}
}
class Room {
constructor(roomNumber, roomType, floor) {
this.roomNumber = roomNumber;
this.roomType = roomType;
this.floor = floor;
this.inventory = new RoomInventory(); // composition: Room owns its inventory 1:1
}
}
class ReservationRequest {
constructor(range, roomType, guestCount, addOns, 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. */
class ReservationRequestBuilder {
constructor() {
this._range = null;
this._roomType = null;
this._guestCount = 1;
this._addOns = new Set();
this._specialRequests = '';
}
withDates(checkIn, checkOut) { this._range = new DateRange(checkIn, checkOut); return this; }
withRoomType(type) { this._roomType = type; return this; }
withGuestCount(count) { this._guestCount = count; return this; }
withAddOn(addOn) { this._addOns.add(addOn); return this; }
withSpecialRequests(text) { this._specialRequests = text; return this; }
build() {
if (!this._range) throw new Error('dates are required');
if (!this._roomType) throw new Error('roomType is required');
return new ReservationRequest(this._range, this._roomType, this._guestCount, this._addOns, this._specialRequests);
}
}
/** @interface PricingStrategy: calculatePrice(room, range) -> number */
/** Flat per-night rate by room type. */
class StandardPricingStrategy {
constructor() {
this._baseRates = { [RoomType.SINGLE]: 80, [RoomType.DOUBLE]: 120, [RoomType.SUITE]: 220, [RoomType.DELUXE]: 350 };
}
calculatePrice(room, range) { return this._baseRates[room.roomType] * range.nights(); }
}
/** Adds a weekend surcharge on top of a delegate rate -- a *new* rule, no change to Reservation/Hotel. */
class WeekendSurchargePricingStrategy {
constructor(delegate, weekendMultiplier) { this._delegate = delegate; this._weekendMultiplier = weekendMultiplier; }
calculatePrice(room, range) {
const oneNight = new DateRange(range.checkIn, new Date(range.checkIn.getTime() + 86400000));
const perNight = this._delegate.calculatePrice(room, oneNight);
let total = 0;
for (let d = new Date(range.checkIn); d < range.checkOut; d = new Date(d.getTime() + 86400000)) {
const isWeekend = d.getDay() === 0 || d.getDay() === 6;
total += isWeekend ? perNight * this._weekendMultiplier : perNight;
}
return total;
}
}
/** @interface BookingObserver: onConfirmed(reservation), onCancelled(reservation) */
class BookingNotifier {
constructor() { this._observers = []; }
subscribe(observer) { this._observers.push(observer); }
notifyConfirmed(reservation) { this._observers.forEach((o) => o.onConfirmed(reservation)); }
notifyCancelled(reservation) { this._observers.forEach((o) => o.onCancelled(reservation)); }
}
class Reservation {
constructor(id, guest, room, range, totalPrice, notifier) {
this.id = id;
this.guest = guest;
this.room = room;
this.range = range;
this.totalPrice = totalPrice;
this.status = ReservationStatus.CONFIRMED;
this._notifier = notifier;
}
async cancel() {
if (this.status === ReservationStatus.CANCELLED) return;
this.status = ReservationStatus.CANCELLED;
await this.room.inventory.release(this.range);
this._notifier.notifyCancelled(this);
}
}
class ConsoleGuestNotifier {
onConfirmed(r) {
console.log(`[notify] Reservation ${r.id} CONFIRMED for ${r.guest.name}, room ${r.room.roomNumber}, ${r.totalPrice.toFixed(2)} total`);
}
onCancelled(r) {
console.log(`[notify] Reservation ${r.id} CANCELLED for ${r.guest.name}`);
}
}
/** Facade: top-level orchestration. */
class Hotel {
constructor(rooms, pricingStrategy, notifier) {
this._rooms = rooms;
this._pricingStrategy = pricingStrategy;
this._notifier = notifier;
this._reservations = new Map();
this._reservationSeq = 0;
}
async searchAvailability(checkIn, checkOut, roomType) {
const range = new DateRange(checkIn, checkOut);
const result = [];
for (const room of this._rooms) {
if (roomType && room.roomType !== roomType) continue;
if (await room.inventory.isAvailable(range)) result.push(room);
}
return result;
}
async bookRoom(guest, request) {
for (const candidate of this._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 (await candidate.inventory.book(request.range)) {
const price = this._pricingStrategy.calculatePrice(candidate, request.range);
const id = `RES-${++this._reservationSeq}`;
const reservation = new Reservation(id, guest, candidate, request.range, price, this._notifier);
this._reservations.set(id, reservation);
this._notifier.notifyConfirmed(reservation);
return reservation;
}
}
throw new Error(`No available room of type ${request.roomType} for given dates`);
}
async cancelReservation(reservationId) {
const reservation = this._reservations.get(reservationId);
if (!reservation) throw new Error(`Unknown reservation ${reservationId}`);
await reservation.cancel();
}
}
async function main() {
const rooms = [
new Room('101', RoomType.DOUBLE, 1),
new Room('102', RoomType.DOUBLE, 1),
new Room('201', RoomType.SUITE, 2),
];
const pricing = new WeekendSurchargePricingStrategy(new StandardPricingStrategy(), 1.25);
const notifier = new BookingNotifier();
notifier.subscribe(new ConsoleGuestNotifier());
const hotel = new Hotel(rooms, pricing, notifier);
const checkIn = new Date(2026, 9, 2);
const checkOut = new Date(2026, 9, 5);
console.log('Available DOUBLE rooms:', (await hotel.searchAvailability(checkIn, checkOut, RoomType.DOUBLE)).length);
const request = new ReservationRequestBuilder()
.withDates(checkIn, checkOut)
.withRoomType(RoomType.DOUBLE)
.withGuestCount(2)
.withAddOn(AddOn.BREAKFAST)
.build();
const alice = new Guest('g1', 'Alice', 'alice@example.com');
const resA = await hotel.bookRoom(alice, request);
console.log(`Booked room: ${resA.room.roomNumber} price=${resA.totalPrice}`);
// "Concurrency" demo: two overlapping async attempts racing on the last DOUBLE room.
const bob = new Guest('g2', 'Bob', 'bob@example.com');
const sameRequest = new ReservationRequestBuilder().withDates(checkIn, checkOut).withRoomType(RoomType.DOUBLE).build();
const [r1, r2] = await Promise.allSettled([hotel.bookRoom(bob, sameRequest), hotel.bookRoom(bob, sameRequest)]);
const winner = r1.status === 'fulfilled' ? r1.value : (r2.status === 'fulfilled' ? r2.value : null);
console.log('Second concurrent booking landed on room:', winner ? winner.room.roomNumber : 'none');
console.log('Available DOUBLE rooms after bookings:', (await hotel.searchAvailability(checkIn, checkOut, RoomType.DOUBLE)).length);
await hotel.cancelReservation(resA.id);
console.log('Available DOUBLE rooms after cancel:', (await hotel.searchAvailability(checkIn, checkOut, RoomType.DOUBLE)).length);
}
if (require.main === module) {
main().catch((e) => { console.error(e); process.exit(1); });
}
module.exports = {
RoomType, AddOn, ReservationStatus, DateRange, Guest, RoomInventory, Room,
ReservationRequest, ReservationRequestBuilder, StandardPricingStrategy,
WeekendSurchargePricingStrategy, BookingNotifier, Reservation, ConsoleGuestNotifier, Hotel,
};
// Problem 16: Hotel Reservation System
//
// Room (static attrs) vs RoomInventory (per-room booked-interval calendar) split.
// ReservationRequestBuilder assembles a booking intent step by step.
// PricingStrategy is injected (Strategy) so pricing rules change independently.
// BookingNotifier is a thin Observer subject for booking lifecycle events.
// Hotel is the facade orchestrating search/book/cancel.
//
// Node's event loop is single-threaded, so true data races are impossible here;
// the AsyncMutex below still models the same "lock the inventory during the
// check-then-book" shape as the Java/C++ versions for structural fidelity, and
// matters for real correctness once any I/O (e.g. a DB call) is added inside it.
enum RoomType { SINGLE = 'SINGLE', DOUBLE = 'DOUBLE', SUITE = 'SUITE', DELUXE = 'DELUXE' }
enum AddOn { BREAKFAST = 'BREAKFAST', LATE_CHECKOUT = 'LATE_CHECKOUT', AIRPORT_PICKUP = 'AIRPORT_PICKUP', EXTRA_BED = 'EXTRA_BED' }
enum ReservationStatus { CONFIRMED = 'CONFIRMED', CANCELLED = 'CANCELLED' }
/** A minimal FIFO mutual-exclusion primitive: a queue of waiters, one holder at a time. */
class AsyncMutex {
private locked = false;
private waiters: Array<() => void> = [];
private acquire(): Promise<void> {
return new Promise<void>((resolve) => {
if (!this.locked) { this.locked = true; resolve(); }
else this.waiters.push(resolve);
});
}
private release(): void {
const next = this.waiters.shift();
if (next) next();
else this.locked = false;
}
async runExclusive<T>(fn: () => T | Promise<T>): Promise<T> {
await this.acquire();
try {
return await fn();
} finally {
this.release();
}
}
}
/** Half-open [checkIn, checkOut) interval; overlap math lives here, not on Room. */
class DateRange {
constructor(public readonly checkIn: Date, public readonly checkOut: Date) {
if (!(checkIn < checkOut)) throw new Error('checkIn must be before checkOut');
}
overlaps(other: DateRange): boolean {
return this.checkIn < other.checkOut && other.checkIn < this.checkOut;
}
nights(): number {
return Math.round((this.checkOut.getTime() - this.checkIn.getTime()) / 86400000);
}
}
class Guest {
constructor(public readonly id: string, public readonly name: string, public readonly email: string) {}
}
/** Per-room calendar of booked intervals; the sole overlap-checking authority. */
class RoomInventory {
private bookedIntervals: DateRange[] = [];
private mutex = new AsyncMutex();
private isAvailableInternal(range: DateRange): boolean {
return this.bookedIntervals.every((existing) => !existing.overlaps(range));
}
async isAvailable(range: DateRange): Promise<boolean> {
return this.mutex.runExclusive(() => this.isAvailableInternal(range));
}
// Atomic check-then-book under the same lock, preventing double booking.
async book(range: DateRange): Promise<boolean> {
return this.mutex.runExclusive(() => {
if (!this.isAvailableInternal(range)) return false;
this.bookedIntervals.push(range);
return true;
});
}
async release(range: DateRange): Promise<void> {
await this.mutex.runExclusive(() => {
this.bookedIntervals = this.bookedIntervals.filter(
(r) => !(r.checkIn.getTime() === range.checkIn.getTime() && r.checkOut.getTime() === range.checkOut.getTime())
);
});
}
}
class Room {
readonly inventory: RoomInventory = new RoomInventory(); // composition: Room owns its inventory 1:1
constructor(public readonly roomNumber: string, public readonly roomType: RoomType, public readonly floor: number) {}
}
class ReservationRequest {
constructor(
public readonly range: DateRange,
public readonly roomType: RoomType,
public readonly guestCount: number,
public readonly addOns: Set<AddOn>,
public readonly specialRequests: string
) {}
}
/** Fluent, step-by-step construction; validates once at build() instead of at every setter. */
class ReservationRequestBuilder {
private range?: DateRange;
private roomType?: RoomType;
private guestCount = 1;
private addOns = new Set<AddOn>();
private specialRequests = '';
withDates(checkIn: Date, checkOut: Date): this { this.range = new DateRange(checkIn, checkOut); return this; }
withRoomType(type: RoomType): this { this.roomType = type; return this; }
withGuestCount(count: number): this { this.guestCount = count; return this; }
withAddOn(addOn: AddOn): this { this.addOns.add(addOn); return this; }
withSpecialRequests(text: string): this { this.specialRequests = text; return this; }
build(): ReservationRequest {
if (!this.range) throw new Error('dates are required');
if (!this.roomType) throw new Error('roomType is required');
return new ReservationRequest(this.range, this.roomType, this.guestCount, this.addOns, this.specialRequests);
}
}
interface PricingStrategy {
calculatePrice(room: Room, range: DateRange): number;
}
/** Flat per-night rate by room type. */
class StandardPricingStrategy implements PricingStrategy {
private baseRates: Record<RoomType, number> = {
[RoomType.SINGLE]: 80, [RoomType.DOUBLE]: 120, [RoomType.SUITE]: 220, [RoomType.DELUXE]: 350,
};
calculatePrice(room: Room, range: DateRange): number {
return this.baseRates[room.roomType] * range.nights();
}
}
/** Adds a weekend surcharge on top of a delegate rate -- a *new* rule, no change to Reservation/Hotel. */
class WeekendSurchargePricingStrategy implements PricingStrategy {
constructor(private delegate: PricingStrategy, private weekendMultiplier: number) {}
calculatePrice(room: Room, range: DateRange): number {
const oneNight = new DateRange(range.checkIn, new Date(range.checkIn.getTime() + 86400000));
const perNight = this.delegate.calculatePrice(room, oneNight);
let total = 0;
for (let d = new Date(range.checkIn); d < range.checkOut; d = new Date(d.getTime() + 86400000)) {
const isWeekend = d.getDay() === 0 || d.getDay() === 6;
total += isWeekend ? perNight * this.weekendMultiplier : perNight;
}
return total;
}
}
interface BookingObserver {
onConfirmed(reservation: Reservation): void;
onCancelled(reservation: Reservation): void;
}
class BookingNotifier {
private observers: BookingObserver[] = [];
subscribe(observer: BookingObserver): void { this.observers.push(observer); }
notifyConfirmed(reservation: Reservation): void { this.observers.forEach((o) => o.onConfirmed(reservation)); }
notifyCancelled(reservation: Reservation): void { this.observers.forEach((o) => o.onCancelled(reservation)); }
}
class Reservation {
status: ReservationStatus = ReservationStatus.CONFIRMED;
constructor(
public readonly id: string,
public readonly guest: Guest,
public readonly room: Room,
public readonly range: DateRange,
public readonly totalPrice: number,
private readonly notifier: BookingNotifier
) {}
async cancel(): Promise<void> {
if (this.status === ReservationStatus.CANCELLED) return;
this.status = ReservationStatus.CANCELLED;
await this.room.inventory.release(this.range);
this.notifier.notifyCancelled(this);
}
}
class ConsoleGuestNotifier implements BookingObserver {
onConfirmed(r: Reservation): void {
console.log(`[notify] Reservation ${r.id} CONFIRMED for ${r.guest.name}, room ${r.room.roomNumber}, ${r.totalPrice.toFixed(2)} total`);
}
onCancelled(r: Reservation): void {
console.log(`[notify] Reservation ${r.id} CANCELLED for ${r.guest.name}`);
}
}
/** Facade: top-level orchestration. */
class Hotel {
private reservations = new Map<string, Reservation>();
private reservationSeq = 0;
constructor(private rooms: Room[], private pricingStrategy: PricingStrategy, private notifier: BookingNotifier) {}
async searchAvailability(checkIn: Date, checkOut: Date, roomType?: RoomType): Promise<Room[]> {
const range = new DateRange(checkIn, checkOut);
const result: Room[] = [];
for (const room of this.rooms) {
if (roomType && room.roomType !== roomType) continue;
if (await room.inventory.isAvailable(range)) result.push(room);
}
return result;
}
async bookRoom(guest: Guest, request: ReservationRequest): Promise<Reservation> {
for (const candidate of this.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 (await candidate.inventory.book(request.range)) {
const price = this.pricingStrategy.calculatePrice(candidate, request.range);
const id = `RES-${++this.reservationSeq}`;
const reservation = new Reservation(id, guest, candidate, request.range, price, this.notifier);
this.reservations.set(id, reservation);
this.notifier.notifyConfirmed(reservation);
return reservation;
}
}
throw new Error(`No available room of type ${request.roomType} for given dates`);
}
async cancelReservation(reservationId: string): Promise<void> {
const reservation = this.reservations.get(reservationId);
if (!reservation) throw new Error(`Unknown reservation ${reservationId}`);
await reservation.cancel();
}
}
async function main(): Promise<void> {
const rooms: Room[] = [
new Room('101', RoomType.DOUBLE, 1),
new Room('102', RoomType.DOUBLE, 1),
new Room('201', RoomType.SUITE, 2),
];
const pricing = new WeekendSurchargePricingStrategy(new StandardPricingStrategy(), 1.25);
const notifier = new BookingNotifier();
notifier.subscribe(new ConsoleGuestNotifier());
const hotel = new Hotel(rooms, pricing, notifier);
const checkIn = new Date(2026, 9, 2);
const checkOut = new Date(2026, 9, 5);
console.log('Available DOUBLE rooms:', (await hotel.searchAvailability(checkIn, checkOut, RoomType.DOUBLE)).length);
const request = new ReservationRequestBuilder()
.withDates(checkIn, checkOut)
.withRoomType(RoomType.DOUBLE)
.withGuestCount(2)
.withAddOn(AddOn.BREAKFAST)
.build();
const alice = new Guest('g1', 'Alice', 'alice@example.com');
const resA = await hotel.bookRoom(alice, request);
console.log(`Booked room: ${resA.room.roomNumber} price=${resA.totalPrice}`);
// "Concurrency" demo: two overlapping async attempts racing on the last DOUBLE room.
const bob = new Guest('g2', 'Bob', 'bob@example.com');
const sameRequest = new ReservationRequestBuilder().withDates(checkIn, checkOut).withRoomType(RoomType.DOUBLE).build();
const [r1, r2] = await Promise.allSettled([hotel.bookRoom(bob, sameRequest), hotel.bookRoom(bob, sameRequest)]);
const winner = r1.status === 'fulfilled' ? r1.value : (r2.status === 'fulfilled' ? r2.value : null);
console.log('Second concurrent booking landed on room:', winner ? winner.room.roomNumber : 'none');
console.log('Available DOUBLE rooms after bookings:', (await hotel.searchAvailability(checkIn, checkOut, RoomType.DOUBLE)).length);
await hotel.cancelReservation(resA.id);
console.log('Available DOUBLE rooms after cancel:', (await hotel.searchAvailability(checkIn, checkOut, RoomType.DOUBLE)).length);
}
main().catch((e) => { console.error(e); process.exit(1); });