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.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you check room availability for a date range?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “For each room, maintain a list of (check_in, check_out, booking_id) tuples. Two date ranges overlap if: existing.check_in requested.check_in. If any existing booking satisfies this condition, the room is unavailable. For scale, store bookings in a database indexed on (room_number, check_in, check_out) and use a SQL query: SELECT room_number WHERE check_in :in.”
}
},
{
“@type”: “Question”,
“name”: “How do you implement a cancellation policy in code?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Calculate hours_until_checkin = (booking.check_in – today).total_seconds() / 3600. Apply policy: if hours_until_checkin >= 48, full refund; if 24 <= hours < 48, 50% refund; if < 24, no refund. Update booking.status = CANCELLED and remove the booking from the room's calendar (so the room becomes available again). In production, trigger a payment refund via the payment gateway API as a separate step after status update."
}
},
{
"@type": "Question",
"name": "How do you prevent double booking in a concurrent environment?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Use optimistic locking: add a version column to the room's booking record. Read version V, compute new state, UPDATE WHERE version = V. If 0 rows updated, a concurrent booking won the race u2014 retry or surface conflict. Alternatively, use pessimistic locking: SELECT room FOR UPDATE to serialize concurrent bookings for the same room. In distributed systems, use a distributed lock (Redis SETNX on room_number) with TTL to prevent holding locks across node failures."
}
},
{
"@type": "Question",
"name": "What design patterns does a hotel reservation system use?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Strategy pattern for pricing (standard, dynamic, seasonal) u2014 swap pricing rules without changing HotelService. State machine for booking lifecycle (CONFIRMED u2192 CHECKED_IN u2192 CHECKED_OUT, or CONFIRMED u2192 CANCELLED). Repository pattern for data access (RoomRepository, BookingRepository) separating persistence from domain logic. Observer pattern for notifications u2014 when booking is confirmed or cancelled, notify guest via email/SMS without coupling notification code to reservation logic."
}
},
{
"@type": "Question",
"name": "How would you add support for room upgrades?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Add an upgrade() method: given a confirmed booking, find available rooms of a higher tier for the same dates, apply a price difference calculation (upgrade_price = new_rate * nights – original_price), update booking.room_number to the upgraded room, remove the booking from the old room's calendar and add to the new room's calendar. Handle edge cases: no upgrade available (return False), upgrade at check-in time (same-day upgrade), and whether the guest pays the difference or it is complimentary."
}
}
]
}
Asked at: Airbnb Interview Guide
Asked at: Stripe Interview Guide
Asked at: DoorDash Interview Guide
Asked at: Lyft Interview Guide