Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

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). This is one of the most interview-realistic problems because it mirrors actual business systems: the booking flow has natural states (Pending, Confirmed, Checked In, Checked Out), pricing varies by season and day of week (Strategy pattern), and the critical concurrency challenge of preventing double-bookings appears in nearly every resource-allocation system. Companies like Airbnb and Booking.com deal with these exact design challenges at scale, making this a common interview topic for companies in the travel and hospitality space.

📋 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

CategoryQuestionImpact on Design
ScaleSingle hotel or chain/platform?Multi-tenancy design
RoomsDifferent room types? Amenities?Room class hierarchy
PricingDynamic pricing? Seasonal rates?Strategy pattern for pricing
BookingInstant booking or request-based?Confirmation flow
PaymentFull upfront or pay at hotel?Payment integration
ConcurrencyHandle 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

EntityResponsibilitiesPattern
HotelManage rooms, handle searches-
RoomTrack availability, calculate pricing-
ReservationManage booking lifecycleState Pattern
PricingStrategyCalculate room ratesStrategy Pattern
NotificationServiceSend confirmationsObserver Pattern
PaymentProcessorHandle paymentsStrategy Pattern

Reservation State Machine

┌─────────┐  book()   ┌─────────────┐  pay()   ┌───────────┐
│ PENDING │──────────►│  CONFIRMED  │─────────►│ CHECKED_IN│
└─────────┘           └─────────────┘          └───────────┘
     │                       │                       │
     │cancel()         cancel()               checkout()
     │                       │                       │
     ▼                       ▼                       ▼
┌─────────────┐       ┌─────────────┐        ┌─────────────┐
│  CANCELLED  │       │  CANCELLED  │        │ CHECKED_OUT │
└─────────────┘       │ (refund)    │        └─────────────┘
                      └─────────────┘
                      
             timeout (no show)
  CONFIRMED ─────────────────────► NO_SHOW

📐 Step 3: Class Diagram

┌─────────────────────────────────────────────────────────────┐
│                          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

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

@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

@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

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

@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

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

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

# 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="john@example.com",
    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

Hotels have complex pricing: seasonal rates, weekend premiums, demand-based pricing, promotions, loyalty discounts, and corporate agreements. Without Strategy, you would have one massive calculate_price() method with dozens of conditional branches that grow every time a new pricing rule is added. With Strategy, each pricing model is encapsulated in its own class, and the Reservation receives the appropriate strategy at creation time. This means the revenue team can add a “last-minute deal” pricing model without touching any existing pricing code — a textbook example of the Open/Closed Principle. In an interview, pointing out that the pricing strategy can be composed (e.g., DynamicPricing wrapping SeasonalPricing wrapping StandardPricing) shows you understand the Decorator pattern as well.
Two users might try to book the same room simultaneously. Without locking, the classic race condition occurs: User A checks availability (available), User B checks availability (available), User A books the room, User B also books the room — double booking. The lock ensures the check-and-book operation is atomic. In a production system, you would use database-level pessimistic locking (SELECT FOR UPDATE) or optimistic locking (version numbers) rather than in-memory thread locks, because the application might run on multiple servers. Mentioning this distinction in an interview shows you understand the gap between a single-process prototype and a distributed production system.
Bills can have multiple charges added over time (room service, minibar). Separating them follows SRP and makes billing logic reusable.
Reservations follow a clear lifecycle: Pending → Confirmed → Checked In → Checked Out. Status prevents invalid transitions and documents the flow.

Interview Deep-Dive Questions

Strong answer:
  • Room.is_available(check_in, check_out) acquires the room’s lock, then iterates every reservation on that room. For each reservation with status CONFIRMED or CHECKED_IN, it checks for date overlap using the condition: not (check_out <= res.check_in_date or check_in >= res.check_out_date). If any overlap exists, the room is unavailable. This is O(R) per room where R is the number of reservations for that room.
  • Hotel.search_available_rooms() calls is_available() for each room, making the total cost O(N * R) where N is rooms and R is average reservations per room. With 500 rooms and 20 reservations each, that is 10,000 comparisons — fine. But _reservations is a flat list that never gets cleaned up. After a year of operation with 10,000 historical reservations per room (checked-out, cancelled, no-shows all still in the list), you are scanning 5 million entries per search. The fix is straightforward: filter only CONFIRMED/CHECKED_IN reservations before the overlap check, or maintain a separate “active reservations” list that gets cleaned up on checkout.
  • A production system would not iterate at all — it would use an interval tree or a database query with indexed date ranges. The date-overlap check translates directly to SQL: WHERE room_id = ? AND status IN ('confirmed','checked_in') AND check_in < ? AND check_out > ? with a composite index on (room_id, status, check_in, check_out).
  • Example: Booking.com reportedly handles 1.5 million room-nights per day. Their availability engine uses pre-computed availability bitmaps — one bit per room per day. Checking availability for a 3-night stay is three bitwise AND operations, not a linear scan of reservations.
Red flag answer: “It just loops through rooms and checks dates” — a correct but shallow restatement. Missing the scaling concern (unbounded reservation list growth), the lock contention issue (every availability check acquires a per-room lock), and the production-reality gap (no real system does linear scans at scale).Follow-ups:
  1. The per-room threading.Lock means two users searching for available rooms will contend on the same lock if they are checking the same room. Would you use a read-write lock (multiple readers, single writer) instead? What are the tradeoffs?
  2. If the hotel wants to show real-time availability on a website where 1,000 users are searching simultaneously, how would you architect this differently — caching, eventual consistency, or something else?
Strong answer:
  • The Hotel.make_reservation() method wraps the entire flow in with self._lock — a single mutex for the entire hotel. Thread A enters, acquires the lock, calls room.is_available() which returns True, creates a Reservation, and calls reservation.confirm() which calls room.add_reservation(). The reservation is appended to the room’s list and status becomes CONFIRMED. Thread A releases the lock. Thread B enters, acquires the lock, calls room.is_available() — this time it finds Thread A’s confirmed reservation overlapping the dates and returns False. make_reservation returns None. Double-booking prevented.
  • The vulnerability is the double-lock acquisition. Hotel.make_reservation holds self._lock (hotel-level), then calls room.is_available() which acquires room._lock (room-level). Then reservation.confirm() calls room.add_reservation() which also acquires room._lock. In Python’s threading.Lock, this is a non-reentrant lock by default — the second acquisition will deadlock. The code has a deadlock bug. The room.is_available() call inside hotel.make_reservation() will acquire the room lock, but then room.add_reservation() tries to acquire it again from the same thread, causing a hang.
  • The fix is either: (a) use threading.RLock (reentrant lock) on the room, (b) remove the room-level lock entirely since the hotel lock already serializes access, or (c) restructure so is_available and add_reservation share a single lock-acquisition scope.
  • Example: This is a real-world class of bugs. I have seen a hotel booking system deadlock in production because the availability check and the booking confirmation each acquired the same database row lock. The system appeared to “hang” — no error, no timeout, just stuck threads. It took 3 hours to diagnose because the symptoms looked like a slow database.
Red flag answer: “The lock prevents double booking, it is safe” — this misses the nested lock acquisition that causes deadlock. An interviewer asking about concurrency is specifically testing whether you can trace lock acquisition order and identify deadlock potential.Follow-ups:
  1. If this were a distributed system with multiple server instances behind a load balancer, the in-process threading.Lock is useless. How would you implement distributed locking — Redis SETNX, database SELECT FOR UPDATE, or an optimistic concurrency approach with version numbers?
  2. Some hotel booking systems use a “temporary hold” pattern — the room is locked for 10 minutes while the user enters payment details. How would you implement a time-limited lock that auto-releases if the user abandons checkout?
Strong answer:
  • The current implementation encodes all three policies inside Reservation.cancel() as if/elif branches with hardcoded day thresholds (7 days = full refund, 3 days = 50%, else = nothing for flexible). This has several problems: it violates OCP (adding a fourth policy means editing the cancel method), it puts business logic in the domain entity (the Reservation should not know about refund percentages — those are hotel policy, not reservation state), and the thresholds are hardcoded (no way for different room types or seasons to have different cancellation windows).
  • A cleaner design uses the Strategy pattern — the same one already used for pricing. Define a CancellationPolicy abstract class with calculate_refund(reservation, cancellation_date) -> Decimal. Implement FlexibleCancellationPolicy, ModerateCancellationPolicy, NonRefundableCancellationPolicy, and CustomCancellationPolicy. The policy is assigned at booking time and stored on the Reservation. cancel() simply delegates to self.cancellation_policy.calculate_refund(self, date.today()).
  • Real hotel systems make this even more nuanced: the refund might be prorated (cancel 5 days before = charge for 1 night, refund the rest), the cancellation fee might differ by booking channel (direct booking = lenient, third-party = strict), and some policies have a “free cancellation window” (cancel within 24 hours of booking for full refund, regardless of check-in date).
  • Example: Airbnb has 5 cancellation tiers (Flexible, Moderate, Strict, Super Strict 30, Super Strict 60) each with different day thresholds, refund percentages, and service fee rules. Encoding all of those as if/elif branches in a single method would be unmaintainable. Strategy objects make each policy self-contained and independently testable.
Red flag answer: “The three if-branches are fine, you just add more branches for new policies” — this is a maintenance anti-pattern. After 8 policies with different day thresholds, percentages, and special cases, the method becomes a 100-line mess that no one dares refactor.Follow-ups:
  1. A guest books a non-refundable room but then the hotel overbooks and cannot honor the reservation. Who gets the refund, and how does the system handle forced cancellations that override the policy? Where does this exception logic live?
  2. The refund_policy is passed as a string parameter to cancel(). What happens if the caller passes “flexibl” (a typo)? How would you make the policy selection type-safe and impossible to misconfigure?
Strong answer:
  • The current design lets you pick one strategy per reservation, but real pricing is layered: the base rate is modified by season, then by day-of-week, then by occupancy, then by loyalty tier, then by booking channel. You need a Decorator pattern or a chain of pricing modifiers. Each modifier wraps the previous one and applies its adjustment.
  • Concretely: PricingStrategy stays as the base interface. CompositePricingStrategy takes a base strategy and a list of PriceModifier objects. Each modifier has adjust_price(base_price, date) -> Decimal. The composite iterates the modifiers in order: price = base then for each modifier price = modifier.adjust(price, date). Seasonal modifier multiplies by 1.3 in summer. Weekend modifier multiplies by 1.2 on Saturdays. Occupancy modifier multiplies by 1.15 when occupancy is above 80%.
  • The order of application matters and can produce different results. If base is 100,seasonal1.3xthenweekend1.2x=100, seasonal 1.3x then weekend 1.2x = 156. Weekend 1.2x then seasonal 1.3x = 156(samebecausemultiplicationcommutes).Butifonemodifierisadditive(156 (same because multiplication commutes). But if one modifier is additive (20 surcharge) and another is multiplicative (1.3x), order matters: 100+100 + 20 = 1201.3=120 * 1.3 = 156 vs 1001.3=100 * 1.3 = 130 + 20=20 = 150. You need clear documentation of modifier ordering.
  • Example: The DynamicPricing class in the code actually already tries to combine seasonal + weekend + occupancy in a single class. But it is not composable — you cannot reuse the seasonal logic independently. Splitting it into three separate modifiers that compose via the Decorator pattern gives you the same result with better reuse.
Red flag answer: “Just use DynamicPricing since it combines everything” — this misses the design point entirely. DynamicPricing hardcodes the combination. What if a corporate rate should ignore weekend surcharges but keep seasonal adjustments? You cannot configure DynamicPricing for that without editing the class.Follow-ups:
  1. Hotels often run promotions: “20% off for bookings made 30 days in advance” or “third night free.” These are not per-night multipliers — they modify the total. How would you extend the modifier chain to support both per-night and per-stay adjustments?
  2. If two conflicting promotions apply (loyalty discount 15% + early booking discount 20%), should they stack (35% off), compound (1 - 0.85 * 0.80 = 32% off), or should the system pick the better one? How would you make this configurable per hotel?
Strong answer:
  • Valid transitions defined in the diagram: Pending to Confirmed (via confirm()), Confirmed to Checked-In (via check_in()), Checked-In to Checked-Out (via check_out()), Pending/Confirmed to Cancelled (via cancel()), Confirmed to No-Show (via timeout). Each method checks the current status before proceeding.
  • Transitions the code prevents: check_in() checks status != CONFIRMED and rejects if not. check_out() checks status != CHECKED_IN. cancel() checks for CHECKED_IN or CHECKED_OUT and rejects those. These guards prevent the most obvious invalid transitions.
  • Transitions the code does NOT prevent: There is no guard against calling confirm() on an already-confirmed reservation — it only checks status != PENDING, so calling confirm() on a CANCELLED reservation would pass if the status were somehow set back to PENDING. More critically, there is no protection against cancel() on a PENDING reservation that was already cancelled — calling cancel() twice would recalculate the refund both times. The No-Show transition has no implementation at all — nothing in the code sets this status.
  • A robust state machine implementation would use an explicit transition table: a dictionary mapping (current_state, event) to (next_state, action). Any transition not in the table raises InvalidTransitionError. This makes illegal transitions impossible by construction rather than relying on each method having the right guard.
  • Example: In a real hotel PMS (Property Management System), the state machine includes even more states: Waitlisted, Guaranteed, Non-Guaranteed, Partially-Checked-In (group booking where some guests arrive), and Due-Out. Managing this with per-method if-checks becomes impossible — an explicit state machine library (like transitions in Python) is essential.
Red flag answer: “Each method checks the status so invalid transitions cannot happen” — this assumes every method has correct guards, which is not true in the current code. It also does not scale: adding a new state means auditing every existing method. A transition table is the systematic solution.Follow-ups:
  1. The No-Show status has no implementation. How would you detect no-shows (guest did not check in by a cutoff time), and should this be a background cron job, an event-driven trigger, or manual staff action?
  2. If a guest calls to modify their reservation dates (not cancel), is modify() a separate state transition or does it stay in the same state? What happens if the new dates are unavailable — does the original reservation remain intact?
Strong answer:
  • Issue 1 — Tax calculation is wrong for real-world use. The code applies a flat 12% tax on the subtotal. In reality, room tax, service tax, and food tax are often different rates. Room charges might have a 12% hotel occupancy tax + 2% city tax + 1% tourism levy. Room service food might have 8% sales tax. Minibar alcohol might have 15% excise tax. A flat rate across all charges is financially incorrect.
  • Issue 2 — Payments store amounts as floats. The add_payment method wraps amount in float(amount), converting a precise Decimal to a lossy float. After enough transactions, get_balance_due() converts back to Decimal(str(p["amount"])) which introduces floating-point representation errors. A bill with charges of 100.10,100.10, 200.20, 300.30mightshowabalanceof300.30 might show a balance of 0.0000000001 instead of $0.00 due to float roundtrip.
  • Issue 3 — No audit trail. Charges can be added but never removed or reversed. If a minibar charge is disputed, there is no remove_charge() or void_charge() method. In accounting, you never delete a charge — you add a negative “adjustment” entry. The Bill class has no concept of credit notes or voids.
  • Issue 4 — No payment status tracking. A credit card payment might be authorized but not yet captured. The code treats add_payment as final. Real hotel PMS systems have authorization holds, partial captures, and refund transactions.
  • Example: Marriott’s billing system generates an itemized folio with per-line tax breakdowns, differentiating occupancy tax, sales tax, and resort fees. Their system also tracks pre-authorization holds separately from settled payments. The current Bill class could not produce a legally compliant hotel invoice in most jurisdictions.
Red flag answer: “The Bill class looks fine, it adds charges and calculates total” — this is the perspective of someone who has never dealt with financial systems. An experienced engineer knows that money is one of the hardest domains to model correctly, and the red flags in this code (float conversion, flat tax, no audit trail) would cause real financial discrepancies in production.Follow-ups:
  1. The hotel operates in multiple countries with different tax regimes. How would you design a TaxCalculator that applies the correct tax rules by jurisdiction without hardcoding rates in the Bill class?
  2. A guest disputes a charge after checkout. The payment has already been processed. How do you model a partial refund in the billing system while maintaining a clean audit trail?
Strong answer:
  • Overbooking means Room.is_available() and Hotel.make_reservation() need to consider expected availability, not just current bookings. You would add an overbooking_ratio config (e.g., 1.05 for 5% overbooking). search_available_rooms would return rooms even when all physical rooms are booked, as long as total confirmed reservations do not exceed capacity * overbooking_ratio. For 100 rooms, you allow up to 105 confirmed reservations.
  • The implementation requires separating physical room assignment from reservation confirmation. Currently, a reservation is tied to a specific Room object at booking time. With overbooking, reservations should reference a room type, not a specific room. The specific room is assigned at check-in time, not at booking time. This is a fundamental architectural shift — the Reservation class needs a room_type field instead of (or in addition to) a room field, and a new RoomAssignment step at check-in.
  • When the gamble fails (all guests actually show up), the hotel must “walk” a guest — relocate them to a comparable or upgraded room at a partner hotel, at the original hotel’s expense. This requires a WalkingPolicy that determines: which guest gets walked (usually the one with the lowest rate or no loyalty status), what compensation they receive (free night, upgrade, cash), and integration with partner hotel APIs.
  • Example: Airlines pioneered this model — every major airline overbooks by 10-15%, and “involuntary denied boarding” has detailed DOT regulations. Hotels are less regulated but follow similar practices. Hilton’s revenue management system dynamically adjusts the overbooking ratio based on historical no-show rates per day-of-week and season.
Red flag answer: “Just book more rooms than you have and hope it works out” — this shows no awareness of the downstream consequences (walking guests, compensation costs, reputation damage) or the architectural changes needed (room-type booking vs. specific-room booking).Follow-ups:
  1. How would you calculate the optimal overbooking ratio? What data inputs would you need (historical no-show rate, cancellation rate, walk cost vs. empty-room cost), and is this a design problem or a data science problem?
  2. If a walked guest has a confirmed reservation and the hotel cannot provide the room, is this a breach of contract? How should the system differentiate between a “guaranteed” reservation (credit card on file, must honor) and a “non-guaranteed” one (no penalty for no-show, first to be walked)?
Strong answer:
  • The current DynamicPricing accepts occupancy_rate as a constructor argument, making it a snapshot frozen at initialization time. A real dynamic pricing system needs to query current occupancy at price-calculation time. The simplest fix: instead of passing a float, pass a callable occupancy_provider: Callable[[], float] that queries the Hotel.get_occupancy_rate() method on each price calculation. This way the pricing strategy always uses fresh data.
  • But true dynamic pricing goes far beyond occupancy. Revenue management systems use: competitor pricing (scrape Booking.com and Expedia for comparable hotels), demand signals (local events — a Taylor Swift concert means 3x rates), booking velocity (if rooms are selling faster than expected for a date, raise prices), lead time (bookings 6 months out are discounted, bookings for tonight are premium), and price elasticity (business travelers are price-insensitive Mon-Thu, leisure travelers are elastic Fri-Sun).
  • Architecturally, this means the PricingStrategy interface needs richer context than just base_price, check_in, check_out. You would pass a PricingContext object containing occupancy rate, day of week, lead time (days until check-in), competitor rates, and event calendar. Each pricing modifier examines the context fields it cares about.
  • Example: IDeaS (a major hotel revenue management vendor) recalculates room rates every few minutes based on demand signals. Their system uses machine learning models trained on years of historical booking data. The architecture separates the pricing model (ML) from the pricing engine (applies the model output to specific reservations) — the same Strategy pattern, but the strategy’s internals are a neural network, not a multiplier table.
Red flag answer: “Just update the occupancy_rate field periodically” — this requires someone to remember to update it, introduces stale data between updates, and does not address the architectural problem (the strategy should pull fresh data, not have stale data pushed to it).Follow-ups:
  1. If the pricing system recalculates rates every 5 minutes, a guest might see 200onthesearchpagebutbythetimetheyreachcheckout6minuteslater,thepriceis200 on the search page but by the time they reach checkout 6 minutes later, the price is 220. How do you handle this — honor the displayed price, show an error, or something else?
  2. Dynamic pricing creates a perverse incentive: a guest who checks availability repeatedly might see prices rise because their searches increase the “demand signal.” How would you prevent your own search traffic from inflating prices?

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