Chapter 7: Design a Hotel Reservation System

volume2 hotel-reservation booking transactions concurrency

Status: 🟩 Interview ready
Difficulty: Hard
Time to complete: 50 min read + practice


Overview

A hotel reservation system lets users search for available hotels by location and date, reserve rooms, and manage bookings. Booking.com, Expedia, and Airbnb are the canonical examples.

Why this matters:

  • Classic concurrency and double-booking problem (appears at Booking.com, Airbnb, Expedia interviews)
  • Deep dive into database transactions, locking strategies, and idempotency
  • Teaches how to handle inventory at scale (same patterns apply to ticketing, airline seats)

Problem Statement

Design a hotel reservation system that:

  • Allows users to search available hotels by location and date range
  • Reserves rooms without double-booking
  • Supports cancellation
  • Handles concurrent booking attempts (race conditions)
  • Provides exactly-once reservation semantics (idempotency)

Step 1: Requirements & Scope (5 min)

Functional Requirements

Clarifying questions:

  • How many hotels and rooms? β†’ 5,000 hotels, 1 million rooms
  • Search by? β†’ Location, check-in/check-out dates, room type
  • Double-booking prevention required? β†’ Yes, absolutely no double-booking
  • Cancellations allowed? β†’ Yes
  • Payment in scope? β†’ Yes, pre-authorization flow
  • Any overbooking allowed (like airlines)? β†’ Support configurable overbooking

Scope:

  • Search hotels by location + date
  • Reserve a room (prevent double booking)
  • Cancel reservations
  • View reservation history
  • Payment integration (pre-auth β†’ capture)

Non-Functional Requirements

  • No double-booking: Highest priority correctness requirement
  • Read heavy: 10:1 read/write ratio
  • Scale: 5,000 hotels, 1M rooms
  • 70% occupancy: ~700K rooms occupied on average
  • Peak booking: 10% of bookings occur within 3 days of check-in
  • Idempotency: Exactly-once reservation semantics
  • Low latency: Search < 100ms, booking < 500ms
  • High availability: 99.99% uptime

Scale Estimation

Hotels:          5,000
Rooms:           1,000,000 (avg 200 rooms/hotel)
Occupancy:       70% β†’ 700,000 active reservations at any time

Daily bookings:  1,000,000 Γ— 70% / 365 β‰ˆ 1,918 bookings/day
Peak bookings:   10% in last 3 days = ~64 bookings/hour in peak
Reads/writes:    10:1 ratio β†’ reads dominate β†’ cache aggressively

Room availability queries (search):
- 5,000 hotels Γ— 365 days Γ— ~20 room types = 36.5M records in inventory
- Very cacheable (availability changes infrequently)

Step 2: High-Level Design (10 min)

Services

                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚           API Gateway             β”‚
                        β”‚   (Auth, Rate Limiting, Routing)  β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                      β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚                           β”‚                           β”‚
          β–Ό                           β–Ό                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Hotel Service   β”‚       β”‚  Rate Service    β”‚       β”‚Reservation Serviceβ”‚
β”‚                  β”‚       β”‚                  β”‚       β”‚                  β”‚
β”‚ - CRUD hotels    β”‚       β”‚ - Dynamic pricingβ”‚       β”‚ - Create booking  β”‚
β”‚ - CRUD rooms     β”‚       β”‚ - Seasonal rates β”‚       β”‚ - Cancel booking  β”‚
β”‚ - Amenities      β”‚       β”‚ - Room type ratesβ”‚       β”‚ - View bookings   β”‚
β”‚ - Photos         β”‚       β”‚ - Discounts      β”‚       β”‚ - Check inventory β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                               β”‚
                                                               β–Ό
                                                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                    β”‚  Payment Service  β”‚
                                                    β”‚                  β”‚
                                                    β”‚ - Pre-auth        β”‚
                                                    β”‚ - Capture         β”‚
                                                    β”‚ - Refund          β”‚
                                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Data Models

Hotel data:

CREATE TABLE hotel (
    hotel_id    BIGINT PRIMARY KEY,
    name        VARCHAR(255) NOT NULL,
    address     VARCHAR(500),
    city        VARCHAR(100),
    country     VARCHAR(100),
    star_rating TINYINT,          -- 1-5
    description TEXT,
    created_at  TIMESTAMP
);
 
CREATE TABLE room_type (
    room_type_id  BIGINT PRIMARY KEY,
    hotel_id      BIGINT REFERENCES hotel(hotel_id),
    name          VARCHAR(100),   -- "King Suite", "Standard Double"
    description   TEXT,
    max_occupancy INT,
    price_per_night DECIMAL(10,2)
);

Availability/Inventory data:

-- Core inventory table: one row per hotel + room type + date
CREATE TABLE room_type_inventory (
    id               BIGINT PRIMARY KEY,
    hotel_id         BIGINT NOT NULL,
    room_type_id     BIGINT NOT NULL,
    date             DATE NOT NULL,
    total_inventory  INT NOT NULL,  -- total rooms of this type
    total_reserved   INT NOT NULL DEFAULT 0,
    UNIQUE KEY uk_hotel_roomtype_date (hotel_id, room_type_id, date)
);

Reservation data:

CREATE TABLE reservation (
    id              BIGINT PRIMARY KEY,
    hotel_id        BIGINT NOT NULL,
    room_type_id    BIGINT NOT NULL,
    user_id         BIGINT NOT NULL,
    status          ENUM('pending', 'confirmed', 'cancelled', 'refunded'),
    start_date      DATE NOT NULL,
    end_date        DATE NOT NULL,
    idempotency_key VARCHAR(64) UNIQUE NOT NULL,  -- client-generated UUID
    created_at      TIMESTAMP,
    updated_at      TIMESTAMP
);

API Design

Search hotels:

GET /v1/hotels/availability?city=NYC&check_in=2026-06-01&check_out=2026-06-05&guests=2

Response:
{
  "hotels": [
    {
      "hotel_id": 101,
      "name": "Grand Hotel NYC",
      "star_rating": 4,
      "available_room_types": [
        {
          "room_type_id": 201,
          "name": "King Suite",
          "available_rooms": 5,
          "price_per_night": 299.00
        }
      ]
    }
  ]
}

Create reservation:

POST /v1/reservations
{
  "hotel_id": 101,
  "room_type_id": 201,
  "user_id": 9999,
  "start_date": "2026-06-01",
  "end_date": "2026-06-05",
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000"  // UUID from client
}

Response 201 Created:
{
  "reservation_id": 7654321,
  "status": "confirmed",
  "total_price": 1196.00
}

Response 409 Conflict (double-booking detected):
{
  "error": "no_availability",
  "message": "Room type no longer available for selected dates"
}

Cancel reservation:

DELETE /v1/reservations/{reservation_id}

Response 200:
{
  "reservation_id": 7654321,
  "status": "cancelled",
  "refund_amount": 1196.00
}

Step 3: Deep Dive (25 min)

The Core Problem: Preventing Double Booking

This is the crux of the interview. Two users try to book the last available room at the same time.

Time: T0 - 1 room of type "King Suite" available on 2026-06-01
      (total_inventory=3, total_reserved=2 β†’ 1 available)

User A: Check availability β†’ sees 1 room available
User B: Check availability β†’ sees 1 room available

User A: Books β†’ total_reserved = 3  βœ…
User B: Books β†’ total_reserved = 4  ❌ DOUBLE BOOKED!

Three solutions to prevent double booking:


Option 1: Pessimistic Locking (SELECT FOR UPDATE)

How it works: Lock the row when reading, preventing others from reading or writing until transaction completes.

BEGIN TRANSACTION;
 
-- Lock the inventory rows for the date range
SELECT * FROM room_type_inventory
WHERE hotel_id = 101
  AND room_type_id = 201
  AND date BETWEEN '2026-06-01' AND '2026-06-04'
FOR UPDATE;  -- <-- Lock acquired, no one else can read/modify
 
-- Check availability
-- If available_rooms > 0 for all dates, proceed
 
UPDATE room_type_inventory
SET total_reserved = total_reserved + 1
WHERE hotel_id = 101
  AND room_type_id = 201
  AND date BETWEEN '2026-06-01' AND '2026-06-04';
 
INSERT INTO reservation (...) VALUES (...);
 
COMMIT;

Pros:

  • Correct: Guarantees no double-booking
  • Simple to reason about

Cons:

  • Deadlock risk: Two transactions lock rows in different order
  • Low throughput: Popular rooms on popular dates become bottleneck
  • Lock contention: Other requests queue up waiting for lock

When to use: Low concurrency scenarios, safety-critical operations


Option 2: Optimistic Locking (Version Number / CAS)

How it works: No lock on read. Add a version column. On update, check that version hasn’t changed since you read it. If version changed, someone else updated first β€” retry.

-- Schema change: add version column
ALTER TABLE room_type_inventory ADD COLUMN version INT DEFAULT 0;
 
-- Step 1: Read (no lock)
SELECT id, total_inventory, total_reserved, version
FROM room_type_inventory
WHERE hotel_id = 101
  AND room_type_id = 201
  AND date = '2026-06-01';
-- Returns: id=5001, total_inventory=3, total_reserved=2, version=7
 
-- Step 2: In application code, verify availability
available = total_inventory - total_reserved  -- = 1 (OK!)
 
-- Step 3: Try to update with CAS (Compare-And-Swap)
UPDATE room_type_inventory
SET total_reserved = total_reserved + 1,
    version = version + 1
WHERE id = 5001
  AND version = 7;  -- <-- Only succeeds if version is still 7!
 
-- Check rows affected:
-- If rows_affected = 1: Success! We won the race.
-- If rows_affected = 0: Someone else updated first (version changed) β†’ RETRY

Pros:

  • No deadlocks (no locks held)
  • High read throughput
  • Works well when conflicts are rare

Cons:

  • Retry logic adds complexity
  • High conflict rate under heavy load β†’ many retries β†’ poor performance
  • Not suitable for very hot inventory (e.g., last few rooms of a popular hotel)

When to use: Low-to-moderate concurrency, reads >> writes, conflicts are rare


Option 3: Database Constraint (UNIQUE + Conditional Update)

How it works: Use a SQL constraint to enforce the invariant directly at the DB level.

-- Constraint: reserved cannot exceed total
-- Use a conditional update and check it atomically
 
UPDATE room_type_inventory
SET total_reserved = total_reserved + 1
WHERE hotel_id = 101
  AND room_type_id = 201
  AND date BETWEEN '2026-06-01' AND '2026-06-04'
  AND total_reserved < total_inventory;  -- <-- DB enforces this atomically
 
-- If rows_affected == expected_days: Success
-- If rows_affected < expected_days: At least one date is fully booked β†’ Rollback

Pros:

  • Atomic at DB level
  • Simple: no version column, no application-level retry loop
  • No deadlock risk

Cons:

  • Less explicit than version-based locking
  • Needs careful transaction handling to rollback partial updates

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Booking Flow                             β”‚
β”‚                                                             β”‚
β”‚  1. Client sends booking request with idempotency_key       β”‚
β”‚  2. Check idempotency_key β†’ if exists, return cached result β”‚
β”‚  3. Read inventory (no lock) for all nights                 β”‚
β”‚  4. If any night has 0 availability β†’ return 409 fast       β”‚
β”‚  5. BEGIN TRANSACTION                                       β”‚
β”‚     a. UPDATE inventory WHERE total_reserved < total_inventoryβ”‚
β”‚     b. INSERT INTO reservation                             β”‚
β”‚     c. If any UPDATE affected 0 rows β†’ ROLLBACK, return 409 β”‚
β”‚     d. COMMIT                                              β”‚
β”‚  6. Trigger payment pre-authorization                       β”‚
β”‚  7. Cache result against idempotency_key                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Idempotency: Preventing Duplicate Bookings

Problem: Network timeout after DB commit β€” client retries, creating a second booking for same reservation.

Client β†’ POST /reservations β†’ [network timeout]
Client doesn't know if it succeeded
Client β†’ POST /reservations (retry) β†’ Creates duplicate booking!

Solution: Idempotency Key

  1. Client generates a UUID before sending the request: idempotency_key = UUID()
  2. Server stores idempotency_key with UNIQUE constraint in reservation table
  3. On retry, server sees key already exists β†’ returns original response (no duplicate)
Request 1:  POST /reservations  {idempotency_key: "abc-123", ...}
            β†’ Creates reservation #7654321
            β†’ Stores idempotency_key in DB

Network timeout β†’ client doesn't see response

Request 2:  POST /reservations  {idempotency_key: "abc-123", ...}  (retry)
            β†’ Server: SELECT * FROM reservation WHERE idempotency_key = "abc-123"
            β†’ Found! Return same response as Request 1
            β†’ NO new reservation created

Implementation:

-- UNIQUE constraint prevents duplicate inserts at DB level
CREATE TABLE reservation (
    ...
    idempotency_key VARCHAR(64) UNIQUE NOT NULL
);
 
-- If INSERT fails with unique constraint violation:
-- β†’ Fetch and return existing reservation

Database Schema (Complete)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              hotel                           β”‚
β”‚  hotel_id (PK) | name | city | star_rating   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   β”‚ 1:N
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              room_type                       β”‚
β”‚  room_type_id (PK) | hotel_id (FK) | name   β”‚
β”‚  max_occupancy | price_per_night            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚ 1:N
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         room_type_inventory                  β”‚
β”‚  id (PK) | hotel_id | room_type_id | date   β”‚
β”‚  total_inventory | total_reserved | version  β”‚
β”‚  UNIQUE(hotel_id, room_type_id, date)        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              reservation                     β”‚
β”‚  id (PK) | hotel_id | room_type_id | user_id β”‚
β”‚  status | start_date | end_date              β”‚
β”‚  idempotency_key (UNIQUE) | created_at       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Overbooking Strategy

Hotels (and airlines) intentionally overbook to account for expected cancellations.

total_inventory = 100 (physical rooms)
overbooking_buffer = 10% β†’ allow up to 110 reservations

Booking allowed when:
  total_reserved < total_inventory * (1 + overbooking_ratio)

In DB:
  UPDATE room_type_inventory
  SET total_reserved = total_reserved + 1
  WHERE total_reserved < (total_inventory * 1.10);

Why it works: Historical data shows ~10% cancellation rate. Overbooking by 10% fills rooms that would otherwise sit empty. If everyone shows up, hotel compensates guests with upgrades or alternative accommodation.


Caching Strategy

Problem: Availability queries are read-heavy (10:1 ratio). Querying DB for every search is wasteful.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Cache Architecture                   β”‚
β”‚                                                         β”‚
β”‚  Search Query β†’ Redis Cache β†’ Cache Hit β†’ Return data   β”‚
β”‚                     ↓                                   β”‚
β”‚               Cache Miss                                β”‚
β”‚                     ↓                                   β”‚
β”‚               Read from DB β†’ Populate cache             β”‚
β”‚                                                         β”‚
β”‚  Cache Key:   "availability:{hotel_id}:{room_type_id}   β”‚
β”‚               :{date}"                                  β”‚
β”‚  Cache TTL:   30 seconds (availability is time-sensitive)β”‚
β”‚                                                         β”‚
β”‚  Write path:  DB write β†’ Invalidate Redis cache         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

What to cache:

  • Room availability counts (read frequently, changes on booking)
  • Hotel metadata (name, description, photos β€” changes rarely, long TTL)

What NOT to cache:

  • Reservation details (user PII, security concern)
  • Final availability check before booking (must go to DB for correctness)

Search Optimization with Elasticsearch

Problem: SQL LIKE queries for hotel search by name/amenity/description are slow at scale.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Two-Phase Search                          β”‚
β”‚                                                              β”‚
β”‚  Phase 1: Elasticsearch (text search + geo search)          β”‚
β”‚   Query: city=NYC, amenities=pool, rating>=4                 β”‚
β”‚   Returns: List of matching hotel_ids                        β”‚
β”‚                                                              β”‚
β”‚  Phase 2: Availability DB / Redis                           β”‚
β”‚   Query: Check room_type_inventory for returned hotel_ids   β”‚
β”‚   Returns: Hotels with available rooms for requested dates   β”‚
β”‚                                                              β”‚
β”‚  Search DB β‰  Booking DB (separate concerns, separate scale) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Elasticsearch index:

{
  "hotel_id": 101,
  "name": "Grand Hotel NYC",
  "city": "New York",
  "location": { "lat": 40.7128, "lon": -74.0060 },
  "star_rating": 4,
  "amenities": ["pool", "gym", "spa", "parking"],
  "description": "Luxury hotel in midtown Manhattan"
}

Payment Flow

Step 1: Pre-Authorization (at time of booking)
  Client β†’ Payment Service β†’ Card Network
  "Reserve $1196.00 on card"
  Card network holds funds but does not charge yet
  Returns: pre_auth_id

Step 2: Reservation Confirmation
  Reservation created in DB with status = "confirmed"
  pre_auth_id stored with reservation

Step 3: Capture (at check-in or D-1 day)
  Payment Service β†’ Card Network
  "Capture $1196.00" (using pre_auth_id)
  Card is charged

Step 4: On Cancellation
  Payment Service β†’ Card Network
  "Void pre-auth" (if before capture) or "Refund" (if after)

Why pre-auth: Protects hotel from non-payment. Protects guest from being charged for cancelled bookings. Standard in travel industry.


Microservices Architecture (Final)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Clients                                  β”‚
β”‚              (Web, iOS, Android, Partner APIs)                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      API Gateway                                 β”‚
β”‚          Auth (JWT) | Rate Limiting | SSL Termination            β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                   β”‚                      β”‚
       β–Ό                   β–Ό                      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Hotel Service β”‚  β”‚Reservation Serviceβ”‚  β”‚   Rate Service    β”‚
β”‚              β”‚  β”‚                  β”‚  β”‚                   β”‚
β”‚ - Hotel CRUD β”‚  β”‚ - Book room      β”‚  β”‚ - Dynamic pricing β”‚
β”‚ - Room CRUD  β”‚  β”‚ - Cancel booking β”‚  β”‚ - Seasonal rates  β”‚
β”‚ - Photos     β”‚  β”‚ - View bookings  β”‚  β”‚ - Discount rules  β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                   β”‚                       β”‚
       β–Ό                   β–Ό                       β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚ Hotel DB     β”‚  β”‚  Reservation DB  β”‚             β”‚
β”‚ (MySQL RDS)  β”‚  β”‚  (MySQL RDS)     β”‚             β”‚
β”‚              β”‚  β”‚                  β”‚             β”‚
β”‚ hotel        β”‚  β”‚ reservation      β”‚             β”‚
β”‚ room_type    β”‚  β”‚ room_type_       β”‚             β”‚
β”‚              β”‚  β”‚ inventory        β”‚             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
                           β”‚                       β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”                β”‚
                    β”‚    Redis    β”‚β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚  (Cache)    β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚  Elasticsearch      β”‚
                  β”‚  (Hotel search,     β”‚
                  β”‚   geo search,       β”‚
                  β”‚   text search)      β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚  Payment Service    β”‚
                  β”‚  (Stripe/Braintree  β”‚
                  β”‚   integration)      β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Design Summary β€” Full Booking Flow

1. User searches: GET /hotels/availability?city=NYC&check_in=...
   └─ API Gateway β†’ Hotel Service
   └─ Elasticsearch: find hotels matching city/amenities
   └─ Redis: check availability counts for matching hotels
   └─ Return: list of hotels with available room types

2. User selects room and clicks "Book"
   └─ Client generates idempotency_key = UUID()
   └─ POST /reservations {hotel_id, room_type_id, dates, idempotency_key}

3. Reservation Service receives request
   └─ Check idempotency_key in reservation table
      └─ If exists β†’ return existing result (deduplicate)
      └─ If new β†’ continue

4. Pre-authorization
   └─ Reservation Service β†’ Payment Service
   └─ Payment Service β†’ Card Network: pre-auth $X
   └─ Returns: pre_auth_id

5. Atomic DB transaction
   └─ BEGIN TRANSACTION
   └─ UPDATE room_type_inventory
         SET total_reserved = total_reserved + 1
         WHERE hotel_id=X AND room_type_id=Y AND date IN (...)
           AND total_reserved < total_inventory
   └─ If affected_rows != expected_nights β†’ ROLLBACK β†’ 409 Conflict
   └─ INSERT INTO reservation (status='confirmed', pre_auth_id)
   └─ COMMIT

6. Invalidate Redis cache for affected inventory

7. Return 201 Created with reservation_id

8. At check-in (D-day): Payment Service captures pre-auth

9. On cancellation:
   └─ UPDATE reservation SET status='cancelled'
   └─ UPDATE room_type_inventory SET total_reserved = total_reserved - 1
   └─ Payment Service: void/refund pre-auth

Interview Questions & Answers

Q: How do you prevent double-booking?
A: Use a database transaction with a conditional UPDATE: UPDATE room_type_inventory SET total_reserved = total_reserved + 1 WHERE total_reserved < total_inventory. The DB evaluates this atomically. If rows_affected == 0, no room was available and we rollback. Optionally add an optimistic lock version column for higher-read scenarios. Never rely on application-level read-then-write β€” always enforce the constraint in the DB.

Q: What is idempotency and why do you need it for reservations?
A: Idempotency means the same request produces the same result when repeated. For reservations, if a network timeout causes the client to retry, we could create duplicate bookings. The solution is an idempotency key (client-generated UUID) stored with UNIQUE constraint. On retry, the server detects the key already exists and returns the original response without creating a new reservation.

Q: Pessimistic vs Optimistic locking β€” which would you choose?
A: For hotel reservations, optimistic locking (or a DB constraint) is generally better because: (1) read-heavy workload means most operations don’t conflict, (2) pessimistic locking creates contention on popular rooms/dates, (3) deadlock risk with pessimistic locking across multiple date rows. Use pessimistic locking only if the system requires strict serialization and conflict rates are very high.

Q: How would you handle the search at scale (5,000 hotels)?
A: Two-phase approach: Phase 1 uses Elasticsearch for text/geo search, returning matching hotel IDs quickly. Phase 2 checks Redis for real-time availability counts for those hotel IDs. This separates the search concern (text/geo) from the availability concern (inventory counts). Redis is particularly effective here since availability for a given hotel+room+date is a small integer that fits in a few bytes and can be cached with a short TTL (30 seconds is acceptable).

Q: What happens if the payment service fails during booking?
A: We need a saga or two-phase approach. The pre-auth happens before the DB reservation insert. If pre-auth fails β†’ return error, no DB write. If DB write fails after pre-auth β†’ void the pre-auth. If the void also fails β†’ mark reservation as pending_payment_void and a background job retries the void asynchronously. Use a dead letter queue for permanent payment failures so they can be manually reviewed.


Key Takeaways

  1. Double-booking prevention: Conditional UPDATE in a DB transaction (WHERE total_reserved < total_inventory) is the cleanest solution β€” atomic at the DB level, no deadlocks
  2. Idempotency key: Client-generated UUID with UNIQUE constraint prevents duplicate reservations from retries
  3. Optimistic locking (version column + CAS) outperforms pessimistic locking for read-heavy booking workloads
  4. Separate search from booking: Elasticsearch for hotel discovery, relational DB + Redis for inventory management
  5. Cache availability aggressively: 10:1 read/write ratio means Redis can absorb most availability checks; always verify in DB before final commit
  6. Payment pre-authorization: Reserve funds before confirming; avoid charging before the booking is confirmed in DB
  7. Overbooking is intentional: Hotels accept it as a business model; encode total_inventory * overbooking_ratio as the cap, not raw total_inventory


Last Updated: 2026-04-13
Status: Core concurrency + transaction design β€” must know for booking/ticketing system interviews!