Hotel Management
Room inventory with date-range bookings and pluggable pricing. Strategy handles seasonal rates and weekend premiums. Factory creates room types. Observer fires check-in/out events.
Key Abstractions
Facade. Coordinates room inventory, bookings, and guest management.
Physical room with number, type, floor, and amenity list.
Enum: SINGLE, DOUBLE, SUITE. Drives pricing and capacity.
Binds a guest to a room for a check-in/check-out date range.
Contact info and reservation history.
Interface for rate calculation: standard, seasonal, dynamic.
Checks date-range availability and creates reservations with concurrency control.
Class Diagram
Why This Problem Matters
Hotel management looks straightforward until you think about date ranges. A room isn't just "available" or "booked." It's available for specific dates. Room 201 can be booked Jan 5-8 and also Jan 15-20, but not Jan 7-10. This date-range overlap detection is the core algorithmic challenge.
On top of that, pricing varies by room type, by season, and by day of the week. Hardcoding rates means rewriting code every time marketing runs a holiday promotion. Strategy pattern makes this a configuration concern.
Requirements
Functional
- Manage rooms across multiple floors with different types (single, double, suite)
- Search available rooms by type and date range
- Create reservations with date-range conflict detection
- Check-in and check-out with status tracking
- Calculate pricing based on room type, duration, and seasonal rules
Non-Functional
- Thread-safe reservation creation to prevent double-booking under concurrent requests
- Pricing strategy swappable without changing booking logic
- Notification system for housekeeping and billing on check-in/check-out events
Design Decisions
Why date-range availability instead of a boolean flag?
A boolean "isBooked" field on Room works for tonight. It does not work for reservations made three months out. The room has to track all reservations and check for overlaps against the requested date window. Two ranges overlap when start1 < end2 AND start2 < end1. Simple condition, but if you miss it, you get double-bookings.
Why Strategy for pricing?
Hotels change rates constantly. Peak season, off-season, weekends, loyalty discounts, last-minute deals. Each of these is a pricing strategy. StandardPricing uses flat nightly rates. SeasonalPricing iterates day by day, applying peak and weekend multipliers. Composing strategies (seasonal + loyalty discount) is just another implementation of the interface. Room and Reservation know nothing about how their price was computed.
Why Factory for room creation?
Every room type has different default amenities. A SINGLE gets WiFi and TV. A SUITE gets a jacuzzi and balcony. Factory encapsulates this mapping so callers don't duplicate amenity lists every time they create a room. When you add a PENTHOUSE type, you update the factory. Nothing else changes.
Why Observer on check-in/check-out?
Check-out triggers housekeeping. Check-in triggers billing for deposit capture. Future requirements might add loyalty point accrual, minibar restocking notifications, or analytics logging. Each of these subscribes to the same events independently. BookingService fires and forgets.
Interview Follow-ups
- "How would you handle room upgrades?" Add an
upgrade(reservationId, newRoomType)method on BookingService. It cancels the old reservation and creates a new one for the upgraded room, repricing accordingly. - "What about group bookings?" Introduce a
GroupReservationthat wraps multipleReservationobjects, sharing a single group discount via a composite pricing strategy. - "How would you add cancellation policies?" Cancellation policy becomes another strategy: full refund if cancelled 48 hours before, 50% within 24 hours, no refund otherwise. Attach it to the reservation at booking time.
- "How would you scale to a hotel chain?" Each hotel is an independent instance. A
HotelChainregistry aggregates availability across properties for cross-hotel search.
Code Implementation
1 from __future__ import annotations
2 from abc import ABC, abstractmethod
3 from enum import Enum
4 from dataclasses import dataclass, field
5 from datetime import date, timedelta
6 from threading import Lock
7 from typing import Protocol
8 import uuid
9
10
11 class RoomType(Enum):
12 SINGLE = "single"
13 DOUBLE = "double"
14 SUITE = "suite"
15
16 class ReservationStatus(Enum):
17 CONFIRMED = "confirmed"
18 CHECKED_IN = "checked_in"
19 CHECKED_OUT = "checked_out"
20 CANCELLED = "cancelled"
21
22
23 @dataclass
24 class Room:
25 room_number: str
26 room_type: RoomType
27 floor: int
28 amenities: list[str] = field(default_factory=list)
29
30 def __repr__(self) -> str:
31 return f"Room({self.room_number}, {self.room_type.value}, floor={self.floor})"
32
33
34 @dataclass
35 class Guest:
36 id: str
37 name: str
38 email: str
39 phone: str = ""
40
41
42 @dataclass
43 class Reservation:
44 id: str
45 guest: Guest
46 room: Room
47 check_in: date
48 check_out: date
49 status: ReservationStatus = ReservationStatus.CONFIRMED
50 total_cost: float = 0.0
51
52 def overlaps(self, start: date, end: date) -> bool:
53 if self.status == ReservationStatus.CANCELLED:
54 return False
55 return self.check_in < end and start < self.check_out
56
57
58 class PricingStrategy(Protocol):
59 def calculate(self, room_type: RoomType, check_in: date, check_out: date) -> float: ...
60
61 class StandardPricing:
62 BASE_RATES = {
63 RoomType.SINGLE: 100.0,
64 RoomType.DOUBLE: 150.0,
65 RoomType.SUITE: 300.0,
66 }
67
68 def calculate(self, room_type: RoomType, check_in: date, check_out: date) -> float:
69 nights = (check_out - check_in).days
70 return self.BASE_RATES[room_type] * nights
71
72 class SeasonalPricing:
73 BASE_RATES = {
74 RoomType.SINGLE: 100.0,
75 RoomType.DOUBLE: 150.0,
76 RoomType.SUITE: 300.0,
77 }
78 WEEKEND_MULTIPLIER = 1.3
79 PEAK_MONTHS = {6, 7, 8, 12} # summer + holidays
80 PEAK_MULTIPLIER = 1.5
81
82 def calculate(self, room_type: RoomType, check_in: date, check_out: date) -> float:
83 total = 0.0
84 base = self.BASE_RATES[room_type]
85 current = check_in
86 while current < check_out:
87 rate = base
88 if current.month in self.PEAK_MONTHS:
89 rate *= self.PEAK_MULTIPLIER
90 if current.weekday() >= 5: # Saturday=5, Sunday=6
91 rate *= self.WEEKEND_MULTIPLIER
92 total += rate
93 current += timedelta(days=1)
94 return total
95
96
97 class BookingObserver(Protocol):
98 def on_check_in(self, reservation: Reservation) -> None: ...
99 def on_check_out(self, reservation: Reservation) -> None: ...
100
101 class HousekeepingNotifier:
102 def on_check_in(self, reservation: Reservation) -> None:
103 print(f" [Housekeeping] Room {reservation.room.room_number} is now occupied.")
104
105 def on_check_out(self, reservation: Reservation) -> None:
106 print(f" [Housekeeping] Room {reservation.room.room_number} needs cleaning.")
107
108
109 class BookingService:
110 def __init__(self, pricing: PricingStrategy | None = None):
111 self._reservations: list[Reservation] = []
112 self._pricing = pricing or StandardPricing()
113 self._observers: list[BookingObserver] = []
114 self._lock = Lock()
115
116 def add_observer(self, observer: BookingObserver) -> None:
117 self._observers.append(observer)
118
119 def is_available(self, room: Room, check_in: date, check_out: date) -> bool:
120 return not any(
121 r.room.room_number == room.room_number and r.overlaps(check_in, check_out)
122 for r in self._reservations
123 )
124
125 def reserve(self, guest: Guest, room: Room, check_in: date, check_out: date) -> Reservation:
126 with self._lock:
127 if not self.is_available(room, check_in, check_out):
128 raise RuntimeError(
129 f"Room {room.room_number} not available for {check_in} to {check_out}"
130 )
131 cost = self._pricing.calculate(room.room_type, check_in, check_out)
132 reservation = Reservation(
133 id=str(uuid.uuid4())[:8],
134 guest=guest,
135 room=room,
136 check_in=check_in,
137 check_out=check_out,
138 total_cost=cost,
139 )
140 self._reservations.append(reservation)
141 return reservation
142
143 def check_in(self, reservation_id: str) -> None:
144 res = self._find(reservation_id)
145 if res.status != ReservationStatus.CONFIRMED:
146 raise RuntimeError(f"Cannot check in: reservation is {res.status.value}")
147 res.status = ReservationStatus.CHECKED_IN
148 for obs in self._observers:
149 obs.on_check_in(res)
150
151 def check_out(self, reservation_id: str) -> None:
152 res = self._find(reservation_id)
153 if res.status != ReservationStatus.CHECKED_IN:
154 raise RuntimeError(f"Cannot check out: reservation is {res.status.value}")
155 res.status = ReservationStatus.CHECKED_OUT
156 for obs in self._observers:
157 obs.on_check_out(res)
158
159 def cancel(self, reservation_id: str) -> None:
160 res = self._find(reservation_id)
161 if res.status not in (ReservationStatus.CONFIRMED,):
162 raise RuntimeError(f"Cannot cancel: reservation is {res.status.value}")
163 res.status = ReservationStatus.CANCELLED
164
165 def _find(self, reservation_id: str) -> Reservation:
166 for r in self._reservations:
167 if r.id == reservation_id:
168 return r
169 raise ValueError(f"Reservation {reservation_id} not found")
170
171
172 class Hotel:
173 def __init__(self, name: str, rooms: list[Room], pricing: PricingStrategy | None = None):
174 self.name = name
175 self._rooms = rooms
176 self._booking = BookingService(pricing)
177 self._booking.add_observer(HousekeepingNotifier())
178
179 def search_available(self, room_type: RoomType, check_in: date, check_out: date) -> list[Room]:
180 return [
181 r for r in self._rooms
182 if r.room_type == room_type and self._booking.is_available(r, check_in, check_out)
183 ]
184
185 def book(self, guest: Guest, room: Room, check_in: date, check_out: date) -> Reservation:
186 return self._booking.reserve(guest, room, check_in, check_out)
187
188 def check_in(self, reservation_id: str) -> None:
189 self._booking.check_in(reservation_id)
190
191 def check_out(self, reservation_id: str) -> None:
192 self._booking.check_out(reservation_id)
193
194 def cancel(self, reservation_id: str) -> None:
195 self._booking.cancel(reservation_id)
196
197
198 def create_room(room_number: str, room_type: RoomType, floor: int) -> Room:
199 amenity_map = {
200 RoomType.SINGLE: ["WiFi", "TV"],
201 RoomType.DOUBLE: ["WiFi", "TV", "Mini Bar"],
202 RoomType.SUITE: ["WiFi", "TV", "Mini Bar", "Jacuzzi", "Balcony"],
203 }
204 return Room(room_number, room_type, floor, amenity_map[room_type])
205
206
207 if __name__ == "__main__":
208 rooms = [
209 create_room("101", RoomType.SINGLE, 1),
210 create_room("102", RoomType.SINGLE, 1),
211 create_room("201", RoomType.DOUBLE, 2),
212 create_room("202", RoomType.DOUBLE, 2),
213 create_room("301", RoomType.SUITE, 3),
214 ]
215
216 hotel = Hotel("Grand Walnut Hotel", rooms, SeasonalPricing())
217
218 print("=== Hotel Management System ===\n")
219
220 guest = Guest(id="G001", name="Bob", email="bob@example.com", phone="555-1234")
221
222 check_in_date = date(2025, 7, 10) # peak summer
223 check_out_date = date(2025, 7, 13) # 3 nights
224
225 available = hotel.search_available(RoomType.DOUBLE, check_in_date, check_out_date)
226 print(f"Available DOUBLE rooms for Jul 10-13: {available}")
227
228 res = hotel.book(guest, available[0], check_in_date, check_out_date)
229 print(f"Booked: {res.room} for {res.guest.name}, cost=${res.total_cost:.2f}")
230
231 remaining = hotel.search_available(RoomType.DOUBLE, check_in_date, check_out_date)
232 print(f"Available DOUBLE rooms after booking: {remaining}\n")
233
234 print("Checking in:")
235 hotel.check_in(res.id)
236 print(f" Reservation status: {res.status.value}\n")
237
238 print("Checking out:")
239 hotel.check_out(res.id)
240 print(f" Reservation status: {res.status.value}")
241
242 print(f"\nRe-checking availability for same dates:")
243 available_again = hotel.search_available(RoomType.DOUBLE, check_in_date, check_out_date)
244 print(f" Available DOUBLE rooms: {available_again}")Common Mistakes
- ✗Checking availability by a single date instead of a date range. A room booked Jan 5-8 is unavailable on Jan 6, even though no reservation starts on that date.
- ✗Putting pricing logic inside the Room class. Room describes physical attributes. Pricing is a business rule that changes by season, by demand, by customer loyalty tier.
- ✗No overlap detection on reservations. Two guests end up booked into the same room for overlapping dates. Classic concurrency bug.
- ✗Using inheritance for room types instead of composition. SINGLE, DOUBLE, SUITE differ in attributes, not behavior. An enum plus a config record works better.
Key Points
- ✓Date-range availability checking is the core complexity. A room is available only if no existing reservation overlaps the requested date range.
- ✓Strategy pattern for pricing. Standard pricing uses a flat rate per room type. Seasonal pricing applies multipliers for holidays. Weekend pricing bumps Friday/Saturday rates.
- ✓Factory method for room creation. Adding a PENTHOUSE type means one new case in the factory, not scattered construction logic.
- ✓Observer fires events on check-in and check-out. Housekeeping subscribes to check-out. Billing subscribes to check-in for deposit capture.