Design a Hotel Booking System
A hotel booking platform like Expedia aggregates inventory from thousands of hotels and enables users to search, compare, and book rooms. The key challenge is maintaining consistency with third-party hotel systems while competing with other booking platforms for the same inventory.
This walkthrough follows the Interview Framework and focuses on what you'd actually present in a 45-60 minute interview.
Phase 1: Requirements
Functional Requirements
- Search hotels by location - Users search by geographic coordinates (latitude/longitude) or city name (geocoded), with date range and guest count
- View room availability - Display available rooms with pricing, amenities, and photos
- Book a room - Reserve a room with payment, receive confirmation
- Manage reservations - View, modify, or cancel existing bookings
Keep the scope tight. Explicitly defer features like reviews, loyalty programs, price alerts, and multi-property bookings unless asked. Focus on the core search and booking flow with third-party integration.
Non-Functional Requirements
- Scale: 100K hotels worldwide, 10M rooms total, 1M daily active users, 50K bookings/day
- Availability: 99.9% uptime - users expect booking to always work
- Latency: Search results within 500ms; booking confirmation within 2 seconds (P95) for real-time providers
- Consistency: Strong consistency for bookings - no double-booking allowed
- External Integration: Near real-time inventory sync for search; real-time verification at booking
Key insight for the interviewer: Unlike internal systems, we don't "own" the inventory. Hotels may have their own booking systems, and other OTAs (Online Travel Agencies) like Kayak, Booking.com compete for the same rooms. This fundamentally changes how we handle availability and reservations.
Capacity Estimation
Traffic (Read):
- 1M daily active users, average 3 searches per session
- 3M searches/day = ~35 search requests/second
- Peak (evening hours): 3x average = ~100 searches/second
Bookings (Write):
- 50K bookings/day = ~0.6 bookings/second
- Peak: 5x average = ~3 bookings/second
Storage:
- Hotel metadata: ~50KB per hotel (details, amenities, photos URLs)
- 100K hotels × 50KB = ~5 GB (easily fits in memory for caching)
- Room types: ~5KB each, 10 room types per hotel = ~5 GB
- Reservations: ~2KB each, 50K/day × 365 days = ~36 GB/year
Phase 2: Data Model
Core Entities
Hotel
├── hotel_id (PK)
├── name
├── description
├── latitude
├── longitude
├── city
├── country
├── star_rating
├── amenities[]
├── photos[]
├── external_system_id (provider's hotel ID)
├── external_provider (enum: direct_api, gds, aggregator)
└── last_synced_at
RoomType
├── room_type_id (PK)
├── hotel_id (FK)
├── name (e.g., "Deluxe King", "Standard Double")
├── description
├── max_occupancy
├── amenities[]
├── photos[]
├── base_price_cents
└── external_room_type_id
RoomInventory (daily availability)
├── inventory_id (PK)
├── room_type_id (FK)
├── date
├── total_rooms
├── available_rooms (cached, may be stale)
├── price_cents (dynamic pricing)
└── last_synced_at
Reservation
├── reservation_id (PK)
├── user_id (FK)
├── hotel_id (FK)
├── room_type_id (FK)
├── check_in_date
├── check_out_date
├── num_guests
├── total_price_cents
├── status (enum: pending, confirmed, cancelled, completed)
├── external_confirmation_id (from hotel system)
├── idempotency_key (unique, for retry safety)
├── created_at
└── payment_id
Note: User entity follows standard patterns (user_id, email, name, etc.) and is omitted for brevity.
Database Selection
| Data Type | Storage Solution | Rationale |
|---|---|---|
| Hotel metadata | PostgreSQL + Redis | Relational data, heavily cached for search |
| Room inventory | PostgreSQL + Redis | Transactional updates, cached for reads |
| Reservations | PostgreSQL | Strong consistency, ACID transactions required |
| Geospatial index | PostgreSQL (PostGIS) or Elasticsearch | Efficient lat/long radius queries |
| Search cache | Redis | Fast geographic + date range lookups |
Interview insight: PostgreSQL with PostGIS extension handles geospatial queries efficiently at this scale. For 100K hotels, even a simple bounding box query is fast. You'd consider Elasticsearch only if adding complex filtering (amenities, price ranges, text search) or scaling to millions of properties.
Geospatial Indexing
Use PostGIS for location-based search:
CREATE INDEX idx_hotels_location ON hotels USING GIST (
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography
);
-- Query hotels within 10km radius
SELECT * FROM hotels
WHERE ST_DWithin(
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(:user_long, :user_lat), 4326)::geography,
10000 -- 10km in meters
)
LIMIT 50;
Phase 3: API Design
Search Hotels
GET /api/v1/hotels/search
Query Parameters:
- latitude: 37.7749 (required if city not provided)
- longitude: -122.4194 (required if city not provided)
- city: "San Francisco" (optional, geocoded to lat/long)
- radius_km: 10 (default)
- check_in: 2024-03-15 (required)
- check_out: 2024-03-17 (required)
- guests: 2 (required)
- cursor: (optional) pagination token
- limit: 20 (default)
Response: 200 OK
{
"hotels": [
{
"hotel_id": "htl_123",
"name": "Grand Hotel",
"star_rating": 4,
"distance_km": 1.2,
"thumbnail_url": "https://cdn.example.com/hotels/123/thumb.jpg",
"lowest_price_cents": 15000,
"available_room_types": 3
}
],
"next_cursor": "..."
}
Get Room Availability
GET /api/v1/hotels/{hotel_id}/availability
Query Parameters:
- check_in: 2024-03-15
- check_out: 2024-03-17
- guests: 2
Response: 200 OK
{
"hotel_id": "htl_123",
"check_in": "2024-03-15",
"check_out": "2024-03-17",
"room_types": [
{
"room_type_id": "rt_456",
"name": "Deluxe King",
"description": "Spacious room with city view",
"max_occupancy": 2,
"amenities": ["wifi", "minibar", "ocean_view"],
"available_rooms": 3,
"total_price_cents": 30000,
"price_per_night_cents": 15000,
"cancellation_policy": "Free cancellation until 24h before check-in"
}
],
"availability_expires_at": "2024-03-14T10:15:00Z" // 5-minute validity
}
Key design decision: The availability_expires_at field tells the client this data is only valid for a short window and aligns with the cache TTL. Real-time availability must be re-verified at booking time.
Create Reservation
POST /api/v1/reservations
Headers: Authorization: Bearer <token>
Request Body:
{
"hotel_id": "htl_123",
"room_type_id": "rt_456",
"check_in": "2024-03-15",
"check_out": "2024-03-17",
"guests": 2,
"payment_token": "tok_stripe_123"
}
Response: 201 Created
{
"reservation_id": "res_789",
"status": "confirmed",
"external_confirmation_id": "HOTEL123ABC",
"total_price_cents": 30000,
"check_in": "2024-03-15",
"check_out": "2024-03-17"
}
Response: 409 Conflict
{
"error": "room_unavailable",
"message": "This room is no longer available for the selected dates"
}
Phase 4: High-Level Design
Architecture Diagram
Search Flow
- User enters location (city is geocoded to lat/long), dates, and guest count
- Search Service queries Redis cache for nearby hotels with availability
- If cache miss, query PostgreSQL with PostGIS for hotels in radius
- For each hotel, check cached room inventory for the date range
- Return hotels sorted by relevance (price, distance, rating)
Booking Flow (Critical Path)
Critical insight: We call the external hotel API at multiple steps:
- Check availability after user clicks "Book", before starting checkout
- Hold room (if API supports) before payment
- Confirm reservation after payment succeeds
This ensures we never sell a room that's already booked by another platform.
Phase 5: Deep Dives
Handling Race Conditions with External Systems
This is the core challenge of the interview. Multiple OTAs (Expedia, Kayak, Booking.com) compete for the same hotel room. How do we prevent double-booking?
The Problem:
Timeline:
T0: User A on Expedia sees Room 101 available
T1: User B on Kayak sees Room 101 available
T2: User A clicks "Book Now"
T3: User B clicks "Book Now"
T4: Both systems try to reserve Room 101
T5: Only one should succeed!
Solution: Verification at Key Steps
The hotel's system is the single source of truth. We must verify availability with the external API:
- Before showing availability: Use cached inventory for search and hotel detail pages
- When user clicks "Book": Verify availability in real-time
- After payment: Confirm reservation with hotel's system
- If external confirmation fails: Refund payment immediately
Booking attempt:
1. User submits booking request
2. Call hotel API: GET /availability?room=101&date=2024-03-15
→ If unavailable: Return 409 Conflict immediately
3. Call hotel API: POST /reservations/hold (if supported)
→ Temporarily locks the room for 5-10 minutes
4. Process payment with Stripe
→ If payment fails: Release hold, return error
5. Call hotel API: POST /reservations/confirm (with hold_token)
→ If external confirm fails: Refund payment, return error
6. Save reservation in our database
7. Return success to user
Key insight: Our database is NOT the source of truth for availability. The hotel's system is. We only store confirmed reservations for record-keeping.
Hotel API Integration Patterns
Different hotels have different integration capabilities:
| Hotel Type | Integration Method | Real-time? | Hold Support? |
|---|---|---|---|
| Large chains (Marriott, Hilton) | Direct API | Yes | Yes |
| Mid-size hotels | GDS (Amadeus, Sabre) | Yes | Sometimes |
| Small hotels | Channel Manager (SiteMinder) | Near real-time | Rarely |
| Boutique hotels | Manual/Email | No | No |
For manual/email providers, treat bookings as pending and confirm asynchronously.
Adapter Pattern for Multiple Integrations:
HotelAdapterInterface:
├── checkAvailability(hotelId, dates, roomType) → AvailabilityResponse
├── holdRoom(hotelId, roomType, duration) → HoldToken
├── confirmReservation(hotelId, holdToken, guestDetails) → ConfirmationId
├── cancelReservation(confirmationId) → void
└── syncInventory(hotelId) → InventoryUpdate
Implementations:
├── MarriottAdapter (Direct API)
├── AmadeusGDSAdapter (GDS)
├── SiteMinderAdapter (Channel Manager)
└── ManualAdapter (Email fallback)
Inventory Synchronization Strategy
We can't call external APIs for every search (too slow, rate limits). Use a hybrid approach:
Background Sync (for search):
- Periodically sync availability from hotel systems (every 5-15 minutes)
- Store in Redis for fast search queries
- Mark data with
last_synced_attimestamp
Real-time Verification (for booking):
- Always verify with external API before confirming
- Never trust cached availability for actual bookings
Search flow (uses cache):
User searches → Query Redis cache → Return results (may be slightly stale)
Booking flow (real-time):
User books → Call external API → Verify availability → Process booking
Staleness is acceptable for search - showing a room that's actually booked is annoying but users understand. Staleness is NOT acceptable for booking - we must verify before charging the customer.
Handling Booking Failures
What happens when things go wrong?
Scenario 1: External confirmation fails after payment
1. Payment processed successfully
2. Hotel API returns "room no longer available"
→ Solution: Immediate refund + apologize + offer alternatives
Scenario 2: Hotel API is down
1. User tries to book
2. Hotel API timeout after 5 seconds
→ Solution: Show "booking temporarily unavailable", retry with exponential backoff
→ For critical hotels: Queue the request and process when API recovers
Scenario 3: Network partition during booking
1. We sent confirmation to hotel
2. Network fails before we receive response
3. Did the booking succeed or not?
→ Solution: Implement idempotency keys. Use unique booking reference.
→ On recovery: Query hotel API with reference ID to check status
Preventing Double-Booking on Our Side
Even if the hotel API is the source of truth, we need internal safeguards:
Database-level locking:
BEGIN TRANSACTION;
-- Check if we already have a pending/confirmed reservation
SELECT * FROM reservations
WHERE room_type_id = :room_type_id
AND check_in < :check_out
AND check_out > :check_in
AND status IN ('pending', 'confirmed')
FOR UPDATE; -- Lock these rows
-- If no conflict, insert the new reservation
INSERT INTO reservations (...) VALUES (...);
COMMIT;
Optimistic Concurrency with Version:
1. Read current available_rooms with version number
2. Attempt booking with external API
3. Update available_rooms with "WHERE version = :expected_version"
4. If update fails (version mismatch), re-read and retry
Room Hold Mechanism
For hotels that support it, use temporary holds:
Hold flow:
1. User clicks "Book Now"
2. Call hotel API: POST /rooms/hold
→ Hotel reserves room for 5-10 minutes
→ Returns hold_token
3. Show payment form to user
4. User enters payment details (within hold window)
5. Process payment
6. Call hotel API: POST /reservations/confirm?hold_token=xyz
7. Room is confirmed
If user abandons:
- Hold expires automatically
- Room becomes available again
- No action needed on our side
Not all hotels support holds. For those that don't, we must accept a small risk of "sorry, this room was just booked" after payment. This is why immediate refund capability is essential.
Scaling Search with Caching
Search is read-heavy and latency-sensitive:
Multi-layer caching:
Layer 1: CDN (edge)
- Cache search results by (location_hash, dates, guests)
- TTL: 1 minute
- Reduces load on origin servers
Layer 2: Redis (application)
- Hotel metadata: TTL 1 hour
- Room availability by date: TTL 5 minutes
- Search results: TTL 2 minutes
Layer 3: Local memory
- Static data (amenity lists, city mappings)
- TTL: 10 minutes
Search result composition:
1. Query Redis for hotels in geographic area (pre-computed)
2. For each hotel, get cached availability for date range
3. Filter by guest count and room availability
4. Sort by price/rating/distance
5. Return top 20 results
Common Pitfalls
Trusting cached availability for bookings - Cached data is fine for search results, but you MUST verify with the external hotel API before confirming any reservation.
Not handling external API failures - Hotel APIs go down. Have fallback strategies: retry with backoff, queue for later, or gracefully degrade (show "temporarily unavailable").
Forgetting about payment-booking atomicity - If you charge the customer but fail to confirm with the hotel, you must have automatic refund logic. Never leave a customer charged without a confirmed booking.
Over-caching availability - A 1-hour cache on room availability will lead to frustrated users who see "available" but can't book. Keep availability cache TTL short (5 minutes max).
Ignoring timezone complexity - Check-in/check-out dates are in the hotel's local timezone, not UTC. Store and display correctly.
Not implementing idempotency - Network failures happen. If the user clicks "Book" twice, or if you retry a failed request, you must not create duplicate reservations.
Interview Checklist
Before concluding, verify you've covered:
- Geospatial search with proper indexing (PostGIS or similar)
- External hotel API integration with adapter pattern
- Real-time availability verification before booking
- Race condition handling between competing OTAs
- Room hold mechanism (for hotels that support it)
- Payment-booking atomicity with refund handling
- Inventory sync strategy (background + real-time hybrid)
- Caching layers with appropriate TTLs
- Error handling for external API failures
- Idempotency for booking requests
- Database-level double-booking prevention
Summary
| Aspect | Decision | Rationale |
|---|---|---|
| Source of truth | External hotel system | We don't own inventory; hotels do |
| Search | PostGIS + Redis cache | Fast geo queries, cached for performance |
| Availability | Cache (5 min) + real-time verify | Fast search, accurate booking |
| Booking flow | Verify → Hold → Pay → Confirm | Minimize double-booking risk |
| Integration | Adapter pattern | Abstract multiple hotel APIs |
| Race conditions | Real-time API calls during booking | Hotel system resolves conflicts |
| Failures | Immediate refund + retry logic | Never charge without confirmation |
| Idempotency | Unique booking reference IDs | Handle retries safely |
| Caching | Multi-layer (CDN → Redis → local) | Balance freshness vs. performance |