Difficulty: 🟡 Intermediate | Time: 45-60 minutes | Patterns: Factory, Strategy, Observer
🎯 Problem Statement
Design a hotel booking system that can:- Manage multiple hotels with different room types
- Handle room reservations and cancellations
- Process check-in and check-out
- Calculate pricing with dynamic rates and discounts
- Handle concurrent booking requests
Why This Problem? Hotel booking tests your ability to handle concurrent access (double booking prevention), complex pricing logic, and state transitions (reservation lifecycle).
📋 Step 1: Clarify Requirements
Interview Tip: Hotel systems can be very complex. Always clarify scope - is this one hotel or a booking platform like Booking.com?
Questions to Ask the Interviewer
| Category | Question | Impact on Design |
|---|---|---|
| Scale | Single hotel or chain/platform? | Multi-tenancy design |
| Rooms | Different room types? Amenities? | Room class hierarchy |
| Pricing | Dynamic pricing? Seasonal rates? | Strategy pattern for pricing |
| Booking | Instant booking or request-based? | Confirmation flow |
| Payment | Full upfront or pay at hotel? | Payment integration |
| Concurrency | Handle simultaneous bookings? | Locking strategy |
Functional Requirements
- Search available rooms by date, location, room type
- Book rooms with guest information
- Cancel/modify reservations
- Check-in and check-out guests
- Calculate total bill including extras
- Send booking confirmations
Non-Functional Requirements
- Handle concurrent bookings (no double-booking!)
- Support multiple payment methods
- Maintain booking history
- Real-time availability updates
🧩 Step 2: Identify Core Objects
Key Insight: The booking flow uses pessimistic locking to prevent double bookings - when a user starts booking, the room is temporarily locked until payment completes or times out.
Hotel Structure
Hotel, Room, RoomType, Amenity
Booking
Reservation, Guest, Bill
Services
BookingService, PaymentService
Entity-Responsibility Mapping
| Entity | Responsibilities | Pattern |
|---|---|---|
Hotel | Manage rooms, handle searches | - |
Room | Track availability, calculate pricing | - |
Reservation | Manage booking lifecycle | State Pattern |
PricingStrategy | Calculate room rates | Strategy Pattern |
NotificationService | Send confirmations | Observer Pattern |
PaymentProcessor | Handle payments | Strategy Pattern |
Reservation State Machine
Copy
┌─────────┐ book() ┌─────────────┐ pay() ┌───────────┐
│ PENDING │──────────►│ CONFIRMED │─────────►│ CHECKED_IN│
└─────────┘ └─────────────┘ └───────────┘
│ │ │
│cancel() cancel() checkout()
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ CANCELLED │ │ CANCELLED │ │ CHECKED_OUT │
└─────────────┘ │ (refund) │ └─────────────┘
└─────────────┘
timeout (no show)
CONFIRMED ─────────────────────► NO_SHOW
📐 Step 3: Class Diagram
Copy
┌─────────────────────────────────────────────────────────────┐
│ Hotel │
├─────────────────────────────────────────────────────────────┤
│ - id: UUID │
│ - name: String │
│ - address: Address │
│ - rooms: List<Room> │
│ - roomTypes: List<RoomType> │
├─────────────────────────────────────────────────────────────┤
│ + searchAvailableRooms(dates, type): List<Room> │
│ + getRoom(roomNumber): Room │
│ + addRoom(room): void │
└─────────────────────────────────────────────────────────────┘
│
│ 1..*
▼
┌─────────────────────────────────────────────────────────────┐
│ Room │
├─────────────────────────────────────────────────────────────┤
│ - id: UUID │
│ - roomNumber: String │
│ - type: RoomType │
│ - status: RoomStatus │
│ - floor: int │
├─────────────────────────────────────────────────────────────┤
│ + isAvailable(checkIn, checkOut): bool │
│ + book(): void │
│ + checkIn(): void │
│ + checkOut(): void │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Reservation │
├─────────────────────────────────────────────────────────────┤
│ - id: UUID │
│ - guest: Guest │
│ - room: Room │
│ - checkInDate: Date │
│ - checkOutDate: Date │
│ - status: ReservationStatus │
│ - totalAmount: Decimal │
├─────────────────────────────────────────────────────────────┤
│ + confirm(): void │
│ + cancel(): void │
│ + modify(newDates): bool │
│ + addCharge(charge): void │
└─────────────────────────────────────────────────────────────┘
Step 4: Implementation
Enums and Constants
Copy
from enum import Enum
from datetime import datetime, date, timedelta
from typing import Optional, List, Dict
from dataclasses import dataclass, field
from decimal import Decimal
from abc import ABC, abstractmethod
import uuid
import threading
class RoomType(Enum):
SINGLE = "single"
DOUBLE = "double"
DELUXE = "deluxe"
SUITE = "suite"
PRESIDENTIAL = "presidential"
class RoomStatus(Enum):
AVAILABLE = "available"
OCCUPIED = "occupied"
MAINTENANCE = "maintenance"
CLEANING = "cleaning"
class ReservationStatus(Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
CHECKED_IN = "checked_in"
CHECKED_OUT = "checked_out"
CANCELLED = "cancelled"
NO_SHOW = "no_show"
class PaymentStatus(Enum):
PENDING = "pending"
COMPLETED = "completed"
FAILED = "failed"
REFUNDED = "refunded"
# Base prices per night
BASE_PRICES = {
RoomType.SINGLE: Decimal("100.00"),
RoomType.DOUBLE: Decimal("150.00"),
RoomType.DELUXE: Decimal("250.00"),
RoomType.SUITE: Decimal("400.00"),
RoomType.PRESIDENTIAL: Decimal("1000.00"),
}
Guest Class
Copy
@dataclass
class Address:
street: str
city: str
state: str
country: str
zip_code: str
@dataclass
class Guest:
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8].upper())
name: str = ""
email: str = ""
phone: str = ""
address: Optional[Address] = None
id_type: str = "" # passport, driver's license, etc.
id_number: str = ""
loyalty_points: int = 0
def add_loyalty_points(self, points: int):
self.loyalty_points += points
def redeem_points(self, points: int) -> bool:
if points > self.loyalty_points:
return False
self.loyalty_points -= points
return True
Room Classes
Copy
@dataclass
class Amenity:
name: str
description: str
charge: Decimal = Decimal("0.00")
@dataclass
class RoomTypeConfig:
type: RoomType
base_price: Decimal
max_occupancy: int
amenities: List[Amenity] = field(default_factory=list)
description: str = ""
class Room:
def __init__(
self,
room_number: str,
room_type: RoomType,
floor: int
):
self.id = str(uuid.uuid4())
self.room_number = room_number
self.room_type = room_type
self.floor = floor
self.status = RoomStatus.AVAILABLE
self._reservations: List['Reservation'] = []
self._lock = threading.Lock()
def is_available(self, check_in: date, check_out: date) -> bool:
"""Check if room is available for given date range"""
with self._lock:
if self.status == RoomStatus.MAINTENANCE:
return False
for reservation in self._reservations:
if reservation.status in [
ReservationStatus.CONFIRMED,
ReservationStatus.CHECKED_IN
]:
# Check for date overlap
if not (check_out <= reservation.check_in_date or
check_in >= reservation.check_out_date):
return False
return True
def add_reservation(self, reservation: 'Reservation') -> bool:
"""Add a reservation if room is available"""
with self._lock:
if self.is_available(reservation.check_in_date, reservation.check_out_date):
self._reservations.append(reservation)
return True
return False
def get_reservations(self, from_date: date = None, to_date: date = None) -> List['Reservation']:
"""Get reservations within date range"""
reservations = []
for res in self._reservations:
if from_date and res.check_out_date < from_date:
continue
if to_date and res.check_in_date > to_date:
continue
reservations.append(res)
return reservations
def __str__(self):
return f"Room {self.room_number} ({self.room_type.value})"
Pricing Strategy
Copy
class PricingStrategy(ABC):
"""Strategy pattern for dynamic pricing"""
@abstractmethod
def calculate_price(
self,
base_price: Decimal,
check_in: date,
check_out: date
) -> Decimal:
pass
class StandardPricing(PricingStrategy):
"""Standard pricing - base price per night"""
def calculate_price(self, base_price: Decimal, check_in: date, check_out: date) -> Decimal:
nights = (check_out - check_in).days
return base_price * nights
class SeasonalPricing(PricingStrategy):
"""Higher prices during peak seasons"""
PEAK_MONTHS = [6, 7, 8, 12] # Summer and December
PEAK_MULTIPLIER = Decimal("1.5")
def calculate_price(self, base_price: Decimal, check_in: date, check_out: date) -> Decimal:
total = Decimal("0.00")
current = check_in
while current < check_out:
if current.month in self.PEAK_MONTHS:
total += base_price * self.PEAK_MULTIPLIER
else:
total += base_price
current += timedelta(days=1)
return total
class WeekendPricing(PricingStrategy):
"""Higher prices on weekends"""
WEEKEND_MULTIPLIER = Decimal("1.25")
def calculate_price(self, base_price: Decimal, check_in: date, check_out: date) -> Decimal:
total = Decimal("0.00")
current = check_in
while current < check_out:
if current.weekday() >= 5: # Saturday = 5, Sunday = 6
total += base_price * self.WEEKEND_MULTIPLIER
else:
total += base_price
current += timedelta(days=1)
return total
class DynamicPricing(PricingStrategy):
"""Combines seasonal and weekend pricing"""
def __init__(self, occupancy_rate: float = 0.5):
self.occupancy_rate = occupancy_rate
def calculate_price(self, base_price: Decimal, check_in: date, check_out: date) -> Decimal:
total = Decimal("0.00")
current = check_in
while current < check_out:
price = base_price
# Peak season multiplier
if current.month in [6, 7, 8, 12]:
price *= Decimal("1.3")
# Weekend multiplier
if current.weekday() >= 5:
price *= Decimal("1.2")
# High occupancy multiplier
if self.occupancy_rate > 0.8:
price *= Decimal("1.15")
total += price
current += timedelta(days=1)
return total.quantize(Decimal("0.01"))
Bill and Charges
Copy
@dataclass
class Charge:
description: str
amount: Decimal
timestamp: datetime = field(default_factory=datetime.now)
category: str = "room" # room, food, service, tax
class Bill:
def __init__(self, reservation: 'Reservation'):
self.id = str(uuid.uuid4())[:8].upper()
self.reservation = reservation
self.charges: List[Charge] = []
self.payments: List[Dict] = []
self.created_at = datetime.now()
def add_charge(self, charge: Charge):
self.charges.append(charge)
def add_room_charge(self, amount: Decimal):
self.add_charge(Charge("Room Charge", amount, category="room"))
def add_service_charge(self, description: str, amount: Decimal):
self.add_charge(Charge(description, amount, category="service"))
def get_subtotal(self) -> Decimal:
return sum(c.amount for c in self.charges)
def get_tax(self, rate: Decimal = Decimal("0.12")) -> Decimal:
return (self.get_subtotal() * rate).quantize(Decimal("0.01"))
def get_total(self) -> Decimal:
return self.get_subtotal() + self.get_tax()
def get_balance_due(self) -> Decimal:
paid = sum(Decimal(str(p["amount"])) for p in self.payments)
return self.get_total() - paid
def add_payment(self, amount: Decimal, method: str):
self.payments.append({
"amount": float(amount),
"method": method,
"timestamp": datetime.now()
})
def print_bill(self):
print(f"\n{'='*50}")
print(f"BILL #{self.id}")
print(f"Reservation: {self.reservation.id}")
print(f"Guest: {self.reservation.guest.name}")
print(f"{'='*50}")
for charge in self.charges:
print(f"{charge.description:30} ${charge.amount:>10.2f}")
print(f"{'-'*50}")
print(f"{'Subtotal':30} ${self.get_subtotal():>10.2f}")
print(f"{'Tax (12%)':30} ${self.get_tax():>10.2f}")
print(f"{'='*50}")
print(f"{'TOTAL':30} ${self.get_total():>10.2f}")
if self.payments:
print(f"\nPayments:")
for p in self.payments:
print(f" {p['method']:25} ${p['amount']:>10.2f}")
print(f"{'Balance Due':30} ${self.get_balance_due():>10.2f}")
print(f"{'='*50}\n")
Reservation Class
Copy
class Reservation:
def __init__(
self,
guest: Guest,
room: Room,
check_in_date: date,
check_out_date: date,
pricing_strategy: PricingStrategy = None
):
self.id = str(uuid.uuid4())[:8].upper()
self.guest = guest
self.room = room
self.check_in_date = check_in_date
self.check_out_date = check_out_date
self.status = ReservationStatus.PENDING
self.created_at = datetime.now()
self.pricing_strategy = pricing_strategy or StandardPricing()
self.bill: Optional[Bill] = None
self.special_requests: List[str] = []
# Calculate initial room charge
self._room_charge = self._calculate_room_charge()
def _calculate_room_charge(self) -> Decimal:
base_price = BASE_PRICES[self.room.room_type]
return self.pricing_strategy.calculate_price(
base_price, self.check_in_date, self.check_out_date
)
@property
def nights(self) -> int:
return (self.check_out_date - self.check_in_date).days
def confirm(self) -> bool:
if self.status != ReservationStatus.PENDING:
return False
if self.room.add_reservation(self):
self.status = ReservationStatus.CONFIRMED
self.bill = Bill(self)
self.bill.add_room_charge(self._room_charge)
print(f"Reservation {self.id} confirmed!")
return True
print("Room not available for selected dates!")
return False
def check_in(self) -> bool:
if self.status != ReservationStatus.CONFIRMED:
print("Reservation must be confirmed before check-in!")
return False
if date.today() < self.check_in_date:
print("Check-in date not reached yet!")
return False
self.status = ReservationStatus.CHECKED_IN
self.room.status = RoomStatus.OCCUPIED
print(f"Guest {self.guest.name} checked into {self.room}")
return True
def check_out(self) -> Optional[Bill]:
if self.status != ReservationStatus.CHECKED_IN:
print("Guest not checked in!")
return None
self.status = ReservationStatus.CHECKED_OUT
self.room.status = RoomStatus.CLEANING
# Add loyalty points
points = int(self._room_charge / 10)
self.guest.add_loyalty_points(points)
print(f"Added {points} loyalty points!")
return self.bill
def cancel(self, refund_policy: str = "flexible") -> Decimal:
"""Cancel reservation and calculate refund"""
if self.status in [ReservationStatus.CHECKED_IN, ReservationStatus.CHECKED_OUT]:
print("Cannot cancel - guest already checked in/out!")
return Decimal("0.00")
self.status = ReservationStatus.CANCELLED
# Calculate refund based on policy
if refund_policy == "flexible":
days_until_checkin = (self.check_in_date - date.today()).days
if days_until_checkin >= 7:
refund = self._room_charge
elif days_until_checkin >= 3:
refund = self._room_charge * Decimal("0.5")
else:
refund = Decimal("0.00")
elif refund_policy == "non-refundable":
refund = Decimal("0.00")
else: # moderate
days_until_checkin = (self.check_in_date - date.today()).days
if days_until_checkin >= 5:
refund = self._room_charge
else:
refund = Decimal("0.00")
print(f"Reservation cancelled. Refund: ${refund}")
return refund
def add_charge(self, description: str, amount: Decimal):
"""Add extra charge (room service, minibar, etc.)"""
if self.bill:
self.bill.add_service_charge(description, amount)
def __str__(self):
return (f"Reservation {self.id}: {self.guest.name} - "
f"{self.room} from {self.check_in_date} to {self.check_out_date}")
Hotel Class
Copy
class Hotel:
def __init__(self, name: str, address: Address):
self.id = str(uuid.uuid4())
self.name = name
self.address = address
self.rooms: List[Room] = []
self.reservations: List[Reservation] = []
self._lock = threading.Lock()
def add_room(self, room: Room):
self.rooms.append(room)
def search_available_rooms(
self,
check_in: date,
check_out: date,
room_type: RoomType = None
) -> List[Room]:
"""Search for available rooms"""
available = []
for room in self.rooms:
if room_type and room.room_type != room_type:
continue
if room.is_available(check_in, check_out):
available.append(room)
return available
def make_reservation(
self,
guest: Guest,
room: Room,
check_in: date,
check_out: date,
pricing_strategy: PricingStrategy = None
) -> Optional[Reservation]:
"""Create a new reservation"""
with self._lock:
if not room.is_available(check_in, check_out):
print(f"Room {room.room_number} not available!")
return None
reservation = Reservation(
guest=guest,
room=room,
check_in_date=check_in,
check_out_date=check_out,
pricing_strategy=pricing_strategy
)
if reservation.confirm():
self.reservations.append(reservation)
return reservation
return None
def find_reservation(self, reservation_id: str) -> Optional[Reservation]:
for res in self.reservations:
if res.id == reservation_id:
return res
return None
def get_occupancy_rate(self, target_date: date = None) -> float:
"""Calculate occupancy rate for a given date"""
target_date = target_date or date.today()
occupied = 0
for room in self.rooms:
if room.status == RoomStatus.MAINTENANCE:
continue
for res in room._reservations:
if (res.check_in_date <= target_date < res.check_out_date and
res.status in [ReservationStatus.CONFIRMED, ReservationStatus.CHECKED_IN]):
occupied += 1
break
available_rooms = len([r for r in self.rooms if r.status != RoomStatus.MAINTENANCE])
return occupied / available_rooms if available_rooms > 0 else 0.0
def get_revenue_report(self, from_date: date, to_date: date) -> Dict:
"""Generate revenue report for date range"""
total_revenue = Decimal("0.00")
room_nights = 0
for res in self.reservations:
if res.status in [ReservationStatus.CHECKED_OUT, ReservationStatus.CHECKED_IN]:
if res.check_in_date >= from_date and res.check_out_date <= to_date:
if res.bill:
total_revenue += res.bill.get_total()
room_nights += res.nights
return {
"from_date": from_date,
"to_date": to_date,
"total_revenue": total_revenue,
"room_nights_sold": room_nights,
"average_daily_rate": total_revenue / room_nights if room_nights > 0 else Decimal("0.00")
}
Step 5: Usage Example
Copy
# Create hotel
address = Address("123 Main St", "New York", "NY", "USA", "10001")
hotel = Hotel("Grand Plaza Hotel", address)
# Add rooms
for floor in range(1, 6):
for room_num in range(1, 11):
room_number = f"{floor}{room_num:02d}"
if room_num <= 4:
room_type = RoomType.SINGLE
elif room_num <= 7:
room_type = RoomType.DOUBLE
elif room_num <= 9:
room_type = RoomType.DELUXE
else:
room_type = RoomType.SUITE
hotel.add_room(Room(room_number, room_type, floor))
print(f"Hotel has {len(hotel.rooms)} rooms")
# Create a guest
guest = Guest(
name="John Smith",
email="[email protected]",
phone="555-1234"
)
# Search for available rooms
check_in = date(2024, 7, 15)
check_out = date(2024, 7, 18)
available = hotel.search_available_rooms(check_in, check_out, RoomType.DELUXE)
print(f"Found {len(available)} available deluxe rooms")
# Make a reservation with seasonal pricing
if available:
room = available[0]
reservation = hotel.make_reservation(
guest=guest,
room=room,
check_in=check_in,
check_out=check_out,
pricing_strategy=SeasonalPricing()
)
if reservation:
print(reservation)
print(f"Total: ${reservation.bill.get_total()}")
# Check in
reservation.check_in()
# Add some charges
reservation.add_charge("Room Service - Dinner", Decimal("45.00"))
reservation.add_charge("Mini Bar", Decimal("22.00"))
# Check out
bill = reservation.check_out()
bill.print_bill()
# Make payment
bill.add_payment(bill.get_total(), "Credit Card")
print(f"Balance Due: ${bill.get_balance_due()}")
Key Design Decisions
Why Strategy Pattern for Pricing?
Why Strategy Pattern for Pricing?
Hotels have complex pricing: seasonal rates, weekend premiums, demand-based pricing, promotions. Strategy pattern allows swapping pricing algorithms without changing reservation logic.
Why Thread Locking on Room Availability?
Why Thread Locking on Room Availability?
Two users might try to book the same room simultaneously. The lock ensures atomic check-and-book operations, preventing double bookings.
Why Separate Bill from Reservation?
Why Separate Bill from Reservation?
Bills can have multiple charges added over time (room service, minibar). Separating them follows SRP and makes billing logic reusable.
Why Reservation Status State Machine?
Why Reservation Status State Machine?
Reservations follow a clear lifecycle: Pending → Confirmed → Checked In → Checked Out. Status prevents invalid transitions and documents the flow.
Extension Points
Interview Extensions - Be ready to discuss:
- Room Assignment: Auto-assign best available room based on preferences
- Overbooking: Industry practice to overbook by 5-10%
- Group Bookings: Multiple rooms, one reservation
- Loyalty Tiers: Different perks for gold/platinum members
- Channel Management: Different rates for different booking sources