Skip to main content
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

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

Reservation State Machine


📐 Step 3: Class Diagram

Step 4: Implementation

Enums and Constants

Guest Class

Room Classes

Pricing Strategy

Bill and Charges

Reservation Class

Hotel Class

Step 5: Usage Example

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