System Design Interview: Design a Hotel / Booking Reservation System

What Is a Hotel Reservation System?

A hotel reservation system lets users search for available rooms, make reservations with guaranteed availability, and manage bookings. Core challenges: preventing double-booking when multiple users simultaneously reserve the last room, handling temporal inventory (a room is available or not for specific date ranges), and scaling search across millions of hotels and dates.

  • Snap Interview Guide
  • Lyft Interview Guide
  • Shopify Interview Guide
  • Stripe Interview Guide
  • Uber Interview Guide
  • Airbnb Interview Guide
  • System Requirements

    Functional

    • Search hotels by location, date range, and guest count
    • Check room availability for specific dates
    • Reserve a room (guaranteed, no double-booking)
    • Cancel a reservation
    • Dynamic pricing based on occupancy and demand

    Non-Functional

    • No double-booking: critical correctness requirement
    • Read-heavy: 100 searches per 1 booking
    • 500ms search latency p99

    Data Model

    hotels: id, name, location, amenities, star_rating
    rooms: id, hotel_id, type(single/double/suite), capacity, base_price
    reservations: id, room_id, user_id, check_in, check_out, status, price
    room_inventory: room_id, date, available_count  -- denormalized for fast lookup
    

    Preventing Double Booking

    The critical path: two users simultaneously try to book the last available room for the same dates. Solution using optimistic locking:

    BEGIN TRANSACTION;
    -- Check availability with lock
    SELECT available_count FROM room_inventory
    WHERE room_id = ? AND date BETWEEN ? AND ?
    FOR UPDATE;  -- exclusive row lock
    
    -- If all dates have available_count > 0:
    UPDATE room_inventory
    SET available_count = available_count - 1
    WHERE room_id = ? AND date BETWEEN ? AND ?;
    
    INSERT INTO reservations (room_id, user_id, check_in, check_out, ...) VALUES (...);
    COMMIT;
    

    FOR UPDATE locks the inventory rows for the duration of the transaction. If two users race, one waits for the lock; the second sees available_count=0 and fails. This is pessimistic locking — appropriate here because: (1) booking contention on popular hotels is real, (2) the cost of double-booking is a customer support nightmare.

    Search Architecture

    Full-text and geospatial search for hotels is handled by Elasticsearch (not the transactional DB). Hotels indexed with: location (geo_point), amenities (keyword), star_rating (integer), average_review_score. Search query: geo_distance filter + amenity filters + text search on hotel name/description. Results ranked by relevance + distance + rating. Availability check is separate: after Elasticsearch returns candidate hotels, check real-time availability from the transactional DB for each result (can be parallelized).

    Availability Caching

    Availability queries (room available on date X?) are read-heavy. Cache hotel+date availability in Redis: key = avail:{hotel_id}:{date}, value = {room_type: count}. TTL = 60 seconds (stale availability is acceptable for search display). Cache invalidated on booking or cancellation. Users see cached availability; actual booking uses DB transaction for correctness. Cache stampede prevention: probabilistic early expiration or request coalescing with a short mutex per key.

    Dynamic Pricing

    Price varies by: occupancy rate (higher demand = higher price), days until check-in (last-minute markup), day of week (weekend premium), and events (conferences, concerts near the hotel). Pricing engine runs every 30 minutes as a batch job, updates price_adjustments table. Room display price = base_price * (1 + adjustment_factor). For the reservation itself, the price is locked at booking time — price displayed = price charged.

    Interview Tips

    • Double-booking prevention is the central correctness challenge — FOR UPDATE transaction is the standard answer.
    • Search (Elasticsearch) and booking (RDBMS) are separate — don’t try to do geospatial search in PostgreSQL at scale.
    • room_inventory table (denormalized by date) enables fast date-range availability queries vs. computing from reservations on the fly.
    • Cache invalidation on booking ensures search results don’t show unavailable rooms for long.

    {
    “@context”: “https://schema.org”,
    “@type”: “FAQPage”,
    “mainEntity”: [
    {
    “@type”: “Question”,
    “name”: “How do you prevent double-booking in a hotel reservation system?”,
    “acceptedAnswer”: { “@type”: “Answer”, “text”: “Double-booking occurs when two users simultaneously book the last available room. The critical section is: read availability → check if > 0 → decrement → create reservation. Without synchronization, both users read "available=1", both pass the check, both decrement (result: available=-1), and both get confirmed bookings for the same room. Solution: pessimistic locking with SELECT FOR UPDATE inside a transaction. The FOR UPDATE clause acquires an exclusive row lock on the room_inventory rows for the queried date range. The second transaction attempting to acquire the same lock waits until the first transaction commits or rolls back. After the first transaction commits with available_count decremented, the second reads available_count=0 and fails gracefully. This serializes concurrent booking attempts for the same room+dates. Why pessimistic (not optimistic) locking: room booking has direct, high-value contention (last room during peak season). Optimistic locking (version check) would cause retry loops during high contention, which is poor UX for bookings.” }
    },
    {
    “@type”: “Question”,
    “name”: “How do you design the availability search to handle millions of hotels efficiently?”,
    “acceptedAnswer”: { “@type”: “Answer”, “text”: “Availability search has two phases: (1) Hotel discovery: find hotels matching location, amenities, and star rating. Use Elasticsearch with geo_distance filter and term filters on amenities. Returns top 100 candidate hotel IDs in < 50ms. (2) Availability check: for each candidate hotel, check if any room type has availability for the requested date range. Naive: query the transactional database for each hotel (N sequential queries). Better: batch query all candidate hotel IDs in one query — SELECT hotel_id, room_type, MIN(available_count) as min_avail FROM room_inventory WHERE hotel_id IN (…) AND date BETWEEN ? AND ? GROUP BY hotel_id, room_type HAVING MIN(available_count) > 0. Caching: room_inventory availability is cached per hotel+date in Redis with 60-second TTL. Availability displayed in search results uses cached data; final booking uses the transaction. The 60-second staleness means a user might see "available" in search but fail at booking — handle gracefully with "sorry, just booked" message and re-search.” }
    },
    {
    “@type”: “Question”,
    “name”: “How does dynamic pricing work in a hotel booking system?”,
    “acceptedAnswer”: { “@type”: “Answer”, “text”: “Dynamic pricing adjusts room rates based on real-time supply and demand signals. Key inputs: current occupancy rate (booked rooms / total rooms), days until check-in, day of week, local events (conferences, concerts within 10km), historical booking velocity for similar periods, and competitor pricing (scraped or via OTA APIs). Pricing engine: runs as a scheduled job every 15-30 minutes per hotel. Computes a price_multiplier for each room type and date. Multiplier = f(occupancy_factor, lead_time_factor, event_factor). Store in price_adjustments table: (hotel_id, room_type, date, multiplier). At display time: displayed_price = base_price * multiplier. Price is locked at booking time — INSERT into reservations records the price_at_booking. This prevents a user seeing one price and being charged a different one (price guarantee). Inventory management and pricing are thus decoupled: availability_count (integer) in room_inventory; price (multiplier) in price_adjustments. Both are updated independently.” }
    }
    ]
    }

    Scroll to Top