Low-Level Design: Hotel Reservation System (Availability, Pricing, Concurrency)

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

Scroll to Top