Back to Case Studies
system-designintermediate 13 min read

System Design Interview

Design a Hotel Booking System (Booking.com)

Inventory management, double-booking prevention, and search across 1M+ properties

Key outcome: Zero double-bookings at 50K req/sec
System DesignInventoryLockingElasticsearchState MachinePostgreSQL

The Interview Question

"Design a hotel booking system. Users can search for available hotels by location and date, view room availability, and make reservations. The system must prevent double-booking."

This question is fundamentally about inventory consistency: how do you ensure two users cannot book the same room on the same night, even under high concurrency? It also tests search design — availability search across millions of properties with dynamic inventory is not a simple database query.


Step 1: Requirements

Functional

  • Search hotels by location, check-in / check-out dates, number of guests
  • View available room types and prices for a property
  • Reserve a room: hold it for 10 minutes while the user completes payment
  • Confirm booking after successful payment
  • Cancel a booking (full refund if > 24 hours before check-in)
  • View booking history

Non-functional

  • 1 million hotels, 10 million rooms globally
  • 50,000 search requests/second (read-heavy)
  • 5,000 booking attempts/second
  • Zero double-bookings — this is a hard correctness requirement
  • Search results return in under 1 second

Step 2: The Double-Booking Problem

This is the core challenge. Two users search at 09:00:00, both see "1 room available", both click "Book" at 09:00:01. Without concurrency control, both succeed and the same room is sold twice.

Option A: Pessimistic Locking

When User A starts the booking process for Room 101 on Night X, lock that row in the database. User B's request is blocked until A either completes or abandons.

BEGIN TRANSACTION
SELECT * FROM room_availability 
WHERE room_id = 101 AND date = '2026-06-15' 
FOR UPDATE           ← acquires row-level lock

-- User A checks availability, fills payment form (up to 10 minutes)
-- Row is LOCKED for the entire duration

UPDATE room_availability SET status = 'booked' WHERE room_id = 101 AND date = '2026-06-15'
COMMIT

Problem: User A holds the lock while filling in their payment details for 5 minutes. User B waits 5 minutes or gets a timeout error. At 5,000 booking attempts/second this creates a massive queue of blocked transactions.

Option B: Optimistic Locking

Don't lock the row. Instead, when you write, check that nothing has changed since you read.

READ:
  SELECT room_id, status, version FROM room_availability
  WHERE room_id = 101 AND date = '2026-06-15'
  → returns {status: "available", version: 42}

-- User fills in payment form...

WRITE (conditional update):
  UPDATE room_availability
  SET status = 'booked', version = version + 1
  WHERE room_id = 101 AND date = '2026-06-15' AND version = 42
  ← only succeeds if nobody else updated it

  If 0 rows affected: another user got there first → show "no longer available"
  If 1 row affected: success → create booking record

Optimistic locking is correct for hotel booking because conflicts are rare — most booking attempts succeed on the first try. The retry only happens when two users compete for the same room simultaneously.

Option C: Reservation with a Hold

The most production-realistic approach: create a temporary hold before payment.

Step 1: User selects room → system creates a HOLD (10-minute TTL)
  INSERT INTO holds (room_id, date, user_id, expires_at)
  WHERE NOT EXISTS (SELECT 1 FROM holds WHERE room_id = X AND date = Y AND expires_at > NOW())
  AND NOT EXISTS (SELECT 1 FROM bookings WHERE room_id = X AND date = Y)

Step 2: User completes payment (within 10 minutes)
  → Convert hold to confirmed booking
  → Delete hold

Step 3: Hold expires without payment
  → Background job deletes expired holds → room becomes available again

This mirrors what Booking.com and Airbnb actually do: a "soft lock" during checkout that expires automatically.


Step 3: Architecture

┌───────────────────────────────────────────────────────────┐
│                        Clients                             │
└───────────────────────┬───────────────────────────────────┘
                        │
              ┌─────────▼──────────┐
              │    API Gateway      │
              └──┬──────┬──────────┘
                 │      │
    ┌────────────▼──┐  ┌▼───────────────────┐
    │ Search Service│  │  Booking Service    │
    │               │  │  (state machine)    │
    │ Elasticsearch │  │  + Payment Client   │
    │ for property  │  └──────────┬──────────┘
    │ search        │             │
    └───────────────┘    ┌────────▼──────────┐
                         │   PostgreSQL       │
                         │  (inventory,       │
                         │   bookings,        │
                         │   holds)           │
                         └───────────────────┘

Step 4: Search — Why Two Databases

PostgreSQL (or equivalent RDBMS): stores inventory, bookings, holds, and pricing. Strong consistency required. Search by room_id + date range is a simple indexed lookup.

Elasticsearch: stores property descriptions, location, amenities, star rating — the searchable metadata. Supports:

  • Full-text search ("boutique hotel Paris")
  • Geo distance queries ("hotels within 2km of Eiffel Tower")
  • Faceted filtering (star rating, price range, amenities)
  • Returns matching property IDs in milliseconds

The two-step search flow:

1. User searches "Paris hotels, Jun 15-17, 2 guests, max €200/night, ≥4 stars"

2. Elasticsearch query:
   → geo filter: within 20km of Paris centre
   → filter: star_rating >= 4
   → returns: [property_1, property_2, ..., property_200]

3. PostgreSQL availability query (batch):
   SELECT property_id, room_type, price FROM room_availability
   WHERE property_id IN (property_1, ..., property_200)
     AND date BETWEEN '2026-06-15' AND '2026-06-17'
     AND status = 'available'
     AND guests_capacity >= 2
     AND price_per_night <= 200
   → returns: properties that actually have rooms available for those dates

4. Merge Elasticsearch metadata + PostgreSQL availability
   Sort by relevance / price
   Return top 20 results to user

Elasticsearch handles the descriptive search. PostgreSQL enforces availability consistency.


Step 5: Booking State Machine

SEARCHED → HOLD_PLACED (10 min TTL)
                 ├─ PAYMENT_PROCESSING
                 │        ├─ CONFIRMED ──────────────────┐
                 │        └─ PAYMENT_FAILED → (hold expires) │
                 └─ HOLD_EXPIRED (user didn't complete)      │
                                                              │
CONFIRMED ──── CANCELLED (> 24h before check-in, full refund)│
           └── CANCELLED (< 24h before check-in, no refund)  │
                                                              │
COMPLETED ◄───────────────────────────────────────────────────┘
  (check-out date passed, booking archived)

Each transition is an atomic database update. No transition is allowed to skip a state.


Step 6: Pricing Complexity

Hotel pricing changes dynamically based on: dates, occupancy, seasonality, length of stay, promotions, loyalty tier.

For an interview, acknowledge this complexity without designing a full revenue management system:

Price is calculated at search time by a Pricing Service:
  base_price (from hotel's rate table)
  × occupancy_multiplier (% rooms booked for that date)
  × seasonal_multiplier (Christmas, summer)
  × length_of_stay_discount (3+ nights = 5% off)

Price is locked in at the time of hold creation.
If the hold expires, the price may change on re-search.

Step 7: Cancellations and Inventory Return

When a booking is cancelled, the room must become available again. This requires:

1. Update booking status to CANCELLED
2. Update room_availability: SET status = 'available' for those dates
3. Initiate refund via payment provider (if eligible)
4. Update Elasticsearch availability index (async, acceptable slight delay)

All of steps 1-3 happen in ONE database transaction.
If any fails, the whole thing rolls back.
Step 4 is asynchronous — Elasticsearch is eventually consistent for search.

Step 8: Cost Control

| Area | Optimisation | Why | |------|-------------|-----| | Search | Elasticsearch caches popular search queries | Identical searches (Paris, weekend) served from query cache | | Availability | Redis cache for today+7 days availability | Read-heavy near-future dates cached, DB for far future | | Expired holds | Background cleanup job every minute | Prevents ghost inventory blocking real bookings | | Property images | CDN with long TTL | Property photos rarely change; serve from edge |


What the Interviewer Is Actually Testing

  • Do you identify the double-booking race condition immediately?
  • Can you compare pessimistic vs optimistic locking and choose appropriately?
  • Do you propose a hold / reservation TTL pattern?
  • Do you understand why two databases (Elasticsearch + PostgreSQL) are needed for search + availability?
  • Is your booking state machine complete and do transitions prevent invalid states?
  • Do you handle cancellation as an atomic transaction?

Related Case Studies

Go Deeper

Case studies teach the "what". Our courses teach the "how" — the patterns behind these decisions, built up from first principles.

Explore Courses