Low-Level Design: Hotel Reservation System
A hotel reservation system manages room inventory, bookings, availability checks, pricing, and cancellations. It is a common LLD interview question at companies like Airbnb, Booking.com, and Expedia.
Core Entities
from dataclasses import dataclass, field
from enum import Enum
from datetime import date, timedelta
from typing import Optional
import uuid
class RoomType(Enum):
SINGLE = "single"
DOUBLE = "double"
SUITE = "suite"
class RoomStatus(Enum):
AVAILABLE = "available"
OCCUPIED = "occupied"
MAINTENANCE = "maintenance"
class BookingStatus(Enum):
CONFIRMED = "confirmed"
CANCELLED = "cancelled"
CHECKED_IN = "checked_in"
CHECKED_OUT = "checked_out"
NO_SHOW = "no_show"
BASE_RATES = {
RoomType.SINGLE: 100,
RoomType.DOUBLE: 150,
RoomType.SUITE: 300,
}
@dataclass
class Room:
room_number: str
room_type: RoomType
floor: int
capacity: int
amenities: list[str]
status: RoomStatus = RoomStatus.AVAILABLE
@dataclass
class Guest:
guest_id: str
name: str
email: str
phone: str
@dataclass
class Booking:
booking_id: str
guest_id: str
room_number: str
check_in: date
check_out: date
status: BookingStatus = BookingStatus.CONFIRMED
total_price: float = 0.0
created_at: date = field(default_factory=date.today)
@property
def nights(self) -> int:
return (self.check_out - self.check_in).days
Availability Check and Booking
class HotelService:
def __init__(self):
self._rooms: dict[str, Room] = {}
self._guests: dict[str, Guest] = {}
self._bookings: dict[str, Booking] = {} # booking_id -> Booking
# For availability: room -> list of (check_in, check_out, booking_id)
self._room_calendar: dict[str, list[tuple]] = {}
def add_room(self, room: Room) -> None:
self._rooms[room.room_number] = room
self._room_calendar[room.room_number] = []
def find_available_rooms(self, check_in: date, check_out: date,
room_type: Optional[RoomType] = None) -> 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 self._is_available(room.room_number, check_in, check_out):
available.append(room)
return available
def _is_available(self, room_number: str, check_in: date, check_out: date) -> bool:
for (existing_in, existing_out, _) in self._room_calendar[room_number]:
# Overlap: existing starts before requested ends AND existing ends after requested starts
if existing_in check_in:
return False
return True
def book_room(self, guest_id: str, room_number: str,
check_in: date, check_out: date) -> Booking:
if not self._guests.get(guest_id):
raise ValueError(f"Guest {guest_id} not found")
room = self._rooms.get(room_number)
if not room:
raise ValueError(f"Room {room_number} not found")
if room.status == RoomStatus.MAINTENANCE:
raise ValueError(f"Room {room_number} is under maintenance")
if not self._is_available(room_number, check_in, check_out):
raise ValueError(f"Room {room_number} not available for requested dates")
booking_id = str(uuid.uuid4())
nights = (check_out - check_in).days
total = self._calculate_price(room.room_type, nights, check_in)
booking = Booking(
booking_id=booking_id,
guest_id=guest_id,
room_number=room_number,
check_in=check_in,
check_out=check_out,
total_price=total,
)
self._bookings[booking_id] = booking
self._room_calendar[room_number].append((check_in, check_out, booking_id))
return booking
def _calculate_price(self, room_type: RoomType, nights: int,
check_in: date) -> float:
base = BASE_RATES[room_type] * nights
# Weekend surcharge: +20% if any night is Fri/Sat
weekend_nights = sum(
1 for i in range(nights)
if (check_in + timedelta(days=i)).weekday() in (4, 5) # Fri=4, Sat=5
)
surcharge = weekend_nights * BASE_RATES[room_type] * 0.20
return base + surcharge
Cancellation and Check-in
def cancel_booking(self, booking_id: str) -> float:
booking = self._bookings.get(booking_id)
if not booking or booking.status != BookingStatus.CONFIRMED:
raise ValueError(f"Booking {booking_id} not found or not cancellable")
# Cancellation policy: full refund if > 48h before check-in
hours_until_checkin = (booking.check_in - date.today()).days * 24
refund = booking.total_price if hours_until_checkin >= 48 else 0.0
booking.status = BookingStatus.CANCELLED
# Remove from calendar
calendar = self._room_calendar[booking.room_number]
self._room_calendar[booking.room_number] = [
entry for entry in calendar if entry[2] != booking_id
]
return refund
def check_in(self, booking_id: str) -> None:
booking = self._bookings.get(booking_id)
if not booking or booking.status != BookingStatus.CONFIRMED:
raise ValueError("Invalid booking for check-in")
if date.today() float:
booking = self._bookings.get(booking_id)
if not booking or booking.status != BookingStatus.CHECKED_IN:
raise ValueError("Cannot check out: booking not in CHECKED_IN state")
# Calculate late checkout fee if past noon
# (simplified: charge one extra night if same day after checkout)
booking.status = BookingStatus.CHECKED_OUT
self._rooms[booking.room_number].status = RoomStatus.AVAILABLE
return booking.total_price # in production, add incidentals
Pricing Strategy Extension
from abc import ABC, abstractmethod
class PricingStrategy(ABC):
@abstractmethod
def calculate(self, room_type: RoomType, nights: int, check_in: date) -> float:
pass
class StandardPricing(PricingStrategy):
def calculate(self, room_type: RoomType, nights: int, check_in: date) -> float:
return BASE_RATES[room_type] * nights
class DynamicPricing(PricingStrategy):
"""Surge pricing based on occupancy rate."""
def __init__(self, occupancy_rate: float):
self.multiplier = 1.0 + max(0, (occupancy_rate - 0.7) * 2) # 40% surge at 90% occupancy
def calculate(self, room_type: RoomType, nights: int, check_in: date) -> float:
return BASE_RATES[room_type] * nights * self.multiplier
class SeasonalPricing(PricingStrategy):
PEAK_MONTHS = {6, 7, 8, 12} # Summer and December
def calculate(self, room_type: RoomType, nights: int, check_in: date) -> float:
rate = BASE_RATES[room_type]
if check_in.month in self.PEAK_MONTHS:
rate *= 1.3 # 30% seasonal surcharge
return rate * nights
Concurrency: Preventing Double Booking
In a production system, use optimistic locking: store a version number on each room’s booking list. When booking, read the current version, compute the new state, and update with WHERE version = read_version. If another transaction committed first, the update affects 0 rows — retry or surface conflict to user. Alternatively, use database-level SELECT FOR UPDATE on the room row to serialize concurrent bookings for the same room.
Asked at: Airbnb Interview Guide
Asked at: Stripe Interview Guide
Asked at: DoorDash Interview Guide
Asked at: Lyft Interview Guide