Low-Level Design: Hotel Booking System (OOP)
The Hotel Booking System is a rich LLD problem that combines real-world business rules with OOP design: room types, availability calendars, reservations, cancellations, and pricing strategies. It tests your ability to model time-bounded resources with complex state transitions.
Requirements
- Multiple room types: Standard, Deluxe, Suite, with different capacities and prices
- Book a room for a date range; check availability before booking
- Cancel a reservation (with optional cancellation policy)
- Handle concurrent booking requests for the same room
- Support amenities (WiFi, breakfast, parking) as add-ons
- Generate invoices with itemized charges
Core Classes
Room Hierarchy and Pricing
from enum import Enum
from datetime import date, timedelta
from typing import Optional
import threading
class RoomType(Enum):
STANDARD = "standard"
DELUXE = "deluxe"
SUITE = "suite"
class RoomStatus(Enum):
AVAILABLE = "available"
OCCUPIED = "occupied"
MAINTENANCE = "maintenance"
class Amenity(Enum):
WIFI = "wifi"
BREAKFAST = "breakfast"
PARKING = "parking"
SPA = "spa"
AMENITY_PRICES = {
Amenity.WIFI: 0.0, # Free
Amenity.BREAKFAST: 25.0, # Per night
Amenity.PARKING: 20.0, # Per night
Amenity.SPA: 50.0, # Per night
}
class Room:
BASE_RATES = {
RoomType.STANDARD: 100.0,
RoomType.DELUXE: 180.0,
RoomType.SUITE: 350.0,
}
def __init__(self, room_number: str, room_type: RoomType, capacity: int):
self.room_number = room_number
self.room_type = room_type
self.capacity = capacity
self.status = RoomStatus.AVAILABLE
self.amenities: set[Amenity] = set()
self._bookings: list['Reservation'] = [] # Non-overlapping date ranges
self._lock = threading.Lock()
def get_nightly_rate(self, pricing_strategy: 'PricingStrategy' = None) -> float:
base = self.BASE_RATES[self.room_type]
if pricing_strategy:
return pricing_strategy.calculate_rate(base)
return base
def is_available(self, check_in: date, check_out: date) -> bool:
with self._lock:
for res in self._bookings:
if res.status != ReservationStatus.CANCELLED:
# Overlap if not (checkout = res.check_out)
if not (check_out = res.check_out):
return False
return True
def add_reservation(self, reservation: 'Reservation'):
with self._lock:
self._bookings.append(reservation)
def cancel_reservation(self, reservation_id: str):
with self._lock:
for res in self._bookings:
if res.reservation_id == reservation_id:
res.status = ReservationStatus.CANCELLED
return True
return False
Reservation and Status Machine
import uuid
from dataclasses import dataclass, field
from datetime import datetime
class ReservationStatus(Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
CHECKED_IN = "checked_in"
CHECKED_OUT = "checked_out"
CANCELLED = "cancelled"
@dataclass
class Guest:
guest_id: str
name: str
email: str
phone: str
@dataclass
class Reservation:
reservation_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8].upper())
guest: Guest = None
room: Room = None
check_in: date = None
check_out: date = None
amenities: set[Amenity] = field(default_factory=set)
status: ReservationStatus = ReservationStatus.PENDING
created_at: datetime = field(default_factory=datetime.now)
@property
def nights(self) -> int:
return (self.check_out - self.check_in).days
def calculate_total(self, pricing_strategy=None) -> float:
nightly = self.room.get_nightly_rate(pricing_strategy)
amenity_cost = sum(AMENITY_PRICES[a] for a in self.amenities)
return (nightly + amenity_cost) * self.nights
def generate_invoice(self, pricing_strategy=None) -> dict:
nightly = self.room.get_nightly_rate(pricing_strategy)
amenity_breakdown = {a.value: AMENITY_PRICES[a] * self.nights
for a in self.amenities}
return {
'reservation_id': self.reservation_id,
'guest': self.guest.name,
'room': self.room.room_number,
'check_in': str(self.check_in),
'check_out': str(self.check_out),
'nights': self.nights,
'room_charges': nightly * self.nights,
'amenity_charges': amenity_breakdown,
'total': self.calculate_total(pricing_strategy),
}
def can_cancel(self) -> bool:
return self.status in (ReservationStatus.PENDING, ReservationStatus.CONFIRMED)
Pricing Strategies (Strategy Pattern)
from abc import ABC, abstractmethod
class PricingStrategy(ABC):
@abstractmethod
def calculate_rate(self, base_rate: float) -> float:
pass
class StandardPricing(PricingStrategy):
def calculate_rate(self, base_rate: float) -> float:
return base_rate
class WeekendPricing(PricingStrategy):
WEEKEND_MULTIPLIER = 1.3
def calculate_rate(self, base_rate: float) -> float:
return base_rate * self.WEEKEND_MULTIPLIER
class SeasonalPricing(PricingStrategy):
def __init__(self, season_multiplier: float):
self.multiplier = season_multiplier
def calculate_rate(self, base_rate: float) -> float:
return base_rate * self.multiplier
class DiscountPricing(PricingStrategy):
def __init__(self, discount_percent: float):
self.discount = discount_percent / 100.0
def calculate_rate(self, base_rate: float) -> float:
return base_rate * (1 - self.discount)
Hotel Controller and Search
class CancellationPolicy:
def __init__(self, free_cancel_days: int = 2, penalty_percent: float = 50.0):
self.free_cancel_days = free_cancel_days
self.penalty_percent = penalty_percent
def calculate_refund(self, reservation: Reservation) -> float:
days_until_checkin = (reservation.check_in - date.today()).days
total = reservation.calculate_total()
if days_until_checkin >= self.free_cancel_days:
return total # Full refund
return total * (1 - self.penalty_percent / 100.0)
class Hotel:
def __init__(self, name: str, cancellation_policy: CancellationPolicy = None):
self.name = name
self.rooms: dict[str, Room] = {}
self.reservations: dict[str, Reservation] = {}
self.cancellation_policy = cancellation_policy or CancellationPolicy()
self._lock = threading.Lock()
def add_room(self, room: Room):
self.rooms[room.room_number] = room
def search_available_rooms(self, check_in: date, check_out: date,
room_type: RoomType = None,
guests: int = 1) -> list[Room]:
available = []
for room in self.rooms.values():
if room.status == RoomStatus.MAINTENANCE:
continue
if room_type and room.room_type != room_type:
continue
if room.capacity Reservation:
with self._lock:
if room_number not in self.rooms:
raise ValueError(f"Room {room_number} not found")
room = self.rooms[room_number]
# Re-check availability under lock (prevent TOCTOU race)
if not room.is_available(check_in, check_out):
raise ValueError(f"Room {room_number} not available for selected dates")
reservation = Reservation(
guest=guest,
room=room,
check_in=check_in,
check_out=check_out,
amenities=amenities or set(),
status=ReservationStatus.CONFIRMED,
)
room.add_reservation(reservation)
self.reservations[reservation.reservation_id] = reservation
return reservation
def cancel_reservation(self, reservation_id: str) -> float:
"""Returns refund amount."""
if reservation_id not in self.reservations:
raise ValueError("Reservation not found")
reservation = self.reservations[reservation_id]
if not reservation.can_cancel():
raise ValueError(f"Cannot cancel reservation in status {reservation.status}")
refund = self.cancellation_policy.calculate_refund(reservation)
reservation.room.cancel_reservation(reservation_id)
reservation.status = ReservationStatus.CANCELLED
return refund
def check_in(self, reservation_id: str):
res = self.reservations.get(reservation_id)
if not res or res.status != ReservationStatus.CONFIRMED:
raise ValueError("Invalid reservation for check-in")
res.status = ReservationStatus.CHECKED_IN
res.room.status = RoomStatus.OCCUPIED
def check_out(self, reservation_id: str) -> dict:
res = self.reservations.get(reservation_id)
if not res or res.status != ReservationStatus.CHECKED_IN:
raise ValueError("Guest is not checked in")
res.status = ReservationStatus.CHECKED_OUT
res.room.status = RoomStatus.AVAILABLE
return res.generate_invoice()
Key Design Decisions
- TOCTOU prevention: Check availability and create reservation under the same lock to prevent two guests booking the same room simultaneously
- Strategy pattern for pricing: Inject pricing strategy at query time — easy to add seasonal, loyalty, or corporate pricing without modifying Room
- Reservation status machine: PENDING → CONFIRMED → CHECKED_IN → CHECKED_OUT; CANCELLED is terminal from PENDING or CONFIRMED
- Per-room locks: Fine-grained locking per room allows concurrent bookings of different rooms
- Cancellation policy as dependency: Inject policy into Hotel — different hotels can have different policies (non-refundable rates vs flexible)
Interview Extensions
- Overbooking: Hotels intentionally overbook by 5-10% accounting for no-shows; track overbooking buffer per room type
- Housekeeping scheduling: After checkout, assign room to housekeeping queue; room becomes available after cleaning
- Loyalty program: Track guest points; redeem for discounts; feed into DiscountPricing strategy
- Multi-property: HotelChain aggregate manages multiple Hotel instances; search across properties
🏢 Asked at: Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering
🏢 Asked at: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering
🏢 Asked at: Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering
🏢 Asked at: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems
🏢 Asked at: Atlassian Interview Guide
🏢 Asked at: Shopify Interview Guide
Asked at: Lyft Interview Guide