Hotel Booking
Design a hotel reservation system like Booking.com or Airbnb: from a simple availability calendar to a system that handles concurrent bookings, double-booking prevention, and room-level inventory management at scale.
TL;DR
- Split the read-heavy search path from the correctness-critical booking path. Search can use an eventually consistent index; inventory writes need an authoritative transactional store.
- Represent availability with one
room_type_inventoryrow per room type and night, using the minimum available count across the requested nights as a fast pre-check. - During booking, lock inventory rows in a consistent order, select one physical room that is free for the entire stay, and create the hold atomically. Idempotency keys make retries safe.
- Keep checkout holds as durable reservation rows with an indexed expiry. Redis may pre-filter known holds, but it must never be the source of truth.
- Publish booking and catalog events to update search asynchronously; stale search results are acceptable only because the booking transaction re-checks authoritative inventory.
Scope and assumptions
This article designs a hotel reservation system for searching by location, dates, and guest count; holding and confirming a specific room; and allowing hotel managers to configure room types, prices, and inventory. The difficult parts are date-range availability, concurrent room assignment, checkout expiry, and the split between an eventually consistent search projection and a strongly consistent booking path.
The interview scenario uses these illustrative assumptions:
- About 50 million daily active users, 500 million stored reservations, 10,000 peak availability searches per second, and 1,000 peak booking attempts per second.
- Check-in is inclusive and check-out is exclusive, so a stay from April 1 through April 3 consumes the nights of April 1 and April 2.
- A reservation assigns one physical room for the complete stay; room changes across nights are out of scope.
- Hotels DB owns catalog metadata. The transactional ownership domain for
room_type_inventory, reservations, and bookings must be the same booking-side store (or one PostgreSQL deployment with transactionally co-located schemas); a cross-database booking transaction is not assumed. - The scale, latency, retention, and availability numbers below are interview requirements for this design, not production measurements or provider guarantees.
Functional Requirements
Core Requirements
- Users can search for available rooms by location, check-in date, check-out date, and guest count.
- Users can reserve a room and complete payment.
- Hotel managers can configure room types, prices, and availability.
- The system prevents double-booking of the same room on the same dates.
Below the Line (out of scope)
- Dynamic pricing and revenue management
- Reviews and ratings
Dynamic pricing requires a pricing engine with write access to RoomType.base_price that responds to real-time demand signals: occupancy rate, events in the area, competitor pricing. A later extension could use a background pricing service that subscribes to booking events and updates prices outside the synchronous booking path. It does not touch the concurrency primitives that make this system interesting, so it stays out of scope.
Reviews and ratings is a read-heavy, eventually consistent feature with no write conflict with the booking path. The integration point is a review_submitted event published after a completed stay. A separate reviews service could store ratings and aggregate scores asynchronously without touching the inventory or reservation models.
The hardest part in scope: Checking room availability for a date range without scanning every stored reservation, and ensuring two users cannot book the same physical room for overlapping dates. These two problems drive nearly every design decision in this article.
Non-Functional Requirements
Core Requirements
- Scale: 50M DAU; ~500M total reservations stored; ~10,000 availability searches per second at peak.
- Booking throughput: ~1,000 bookings per second at peak.
- Latency: Search results under 200ms p99; booking confirmation under 500ms p99.
- Consistency: Strong consistency for room inventory. A room must never be double-booked.
- Database choice: PostgreSQL is the baseline for both the catalog and booking stores. The booking write requires an ACID transaction that locks the booking-side inventory rows, checks availability, assigns a room, and inserts a reservation. Some NoSQL databases provide transactions, but using one here would require a different concurrency design; do not assume a distributed lock service is automatically safer. Elasticsearch handles the search path where horizontal scale and schema flexibility apply.
- Availability: 99.99% uptime for the booking path (roughly 52 minutes downtime per year); search can tolerate brief eventual consistency.
- Durability: Confirmed reservations must never be silently lost across server failures.
Below the Line
- Multi-region active-active replication (a single-region design with read replicas is sufficient)
- Real-time fraud scoring during checkout (a separate async pipeline beside the booking path)
Read/write ratio: In this illustrative scenario, availability searches outnumber bookings by roughly 10:1 at the stated peak rates, while the product-level traffic may still be much more read-heavy over a longer window. The important point is that search and booking have opposite requirements: search needs a read-optimized, eventually consistent index; booking needs an authoritative ACID transaction and row-level locking. They can be separate services even when the exact ratio changes.
30-second answer
Use an eventually consistent search index for location and date filtering, but make the booking-side inventory store authoritative. Maintain one inventory row per room type and night as a fast capacity pre-check, then use a transaction that locks the relevant nights and selects a physical room free for the entire stay. Create a durable, expiring reservation before payment, require an idempotency key, and convert the reservation to a booking with an idempotent payment intent. Publish booking, expiry, and catalog events to refresh search projections. Search may be stale; the final booking decision may not be.
5-minute explanation
The read path starts with a geospatial and date-range query against Elasticsearch (or an equivalent search index). It contains projected hotel metadata and per-night availability hints, so a search request does not scan reservations. Because the projection is asynchronous, it can be stale; the booking path must always re-check the authoritative inventory.
The booking path has a stricter boundary. A Booking Service checks the idempotency key, locks the room_type_inventory rows in ascending date order, verifies the minimum available count, and selects one physical room whose existing reservations and bookings do not overlap the requested interval. It writes the reservation and inventory changes in one transaction. Confirmation validates the hold, uses an idempotent payment operation, and atomically changes the durable reservation into a booking; reconciliation handles a payment success followed by a database failure.
An Expiry Worker releases abandoned holds from the database and publishes an event that refreshes search. Hotel-manager writes update catalog data and inventory through an idempotent workflow. Redis can accelerate idempotency and hold checks, but it is only a pre-filter. The core correctness guarantee comes from the database transaction and its constraints.
The entities and APIs below are the concrete anchors for this explanation; the high-level flows then show how search, booking, manager configuration, and expiry fit together.
Core entities
- Hotel: A property with name, geo-coordinates, address, and star rating. One hotel has many room types.
- RoomType: A category within a hotel (Deluxe Queen, King Suite). Holds base price, max occupancy, and amenity list. One room type has many physical rooms.
- Room: A specific bookable unit linked to a room type. Room 214, Room 412. The unit that appears in a confirmed booking.
- Reservation: A temporary hold created when checkout begins. Expires automatically if payment does not complete within a configurable window (15 minutes by default).
- Booking: A confirmed, paid reservation. The permanent record of a completed stay. Links a specific Room, a User, the stay dates, total price, and a payment reference.
- User: A guest account with contact info, payment methods, and booking history.
Full schema details, including the date-range index strategy and the room_type_inventory table design, come up in the deep dives. These six entities are enough to drive the API and high-level architecture without getting lost in column types.
API design
FR 1: Search for available rooms:
# Returns available hotels matching the search criteria; served from the search index
GET /v1/hotels/search
Query: location, check_in_date, check_out_date, guests, page_cursor?
Response: {
results: [
{ hotel_id, name, rating, address, available_room_types: [...], lowest_price },
...
],
next_cursor: "eyJpZCI6Mj..."
}
GET over POST because this is a read with filter parameters. Cursor-based pagination handles open-ended result sets without OFFSET performance degradation at depth. The available_room_types field is pre-computed from the availability index, so this endpoint never scans the reservations table on the hot path.
FR 1b: View room type availability detail:
# Detailed availability for a specific room type and date range
GET /v1/hotels/{hotel_id}/room-types/{room_type_id}/availability
Query: check_in_date, check_out_date
Response: { room_type_id, available_count, price_per_night, total_price }
This endpoint drives the "how many rooms of this type are still open for my dates?" panel on the hotel detail page. The available_count is computed from a dedicated inventory table (see Deep Dive 1) rather than by joining against the full reservations history.
FR 2: Create a reservation (start checkout):
# Atomically holds one room of the requested type; returns reservation with expiry
POST /v1/reservations
Body: { room_type_id, check_in_date, check_out_date, user_id, idempotency_key }
Response: {
reservation_id: "res_abc123",
room_id: "room_214",
expires_at: "2026-03-29T12:15:00Z",
total_price: 450
}
idempotency_key is client-generated (a UUID v4) and required on every call. If a network timeout causes the client to retry, the server returns the original reservation instead of creating a duplicate. The server assigns a specific room_id from the available pool of the requested room_type_id. Two concurrent requests for the same room_type_id on the same dates must each receive a different room_id or one must receive 409 Conflict.
FR 2b: Confirm booking (complete checkout):
# Processes payment and converts the reservation to a confirmed booking
POST /v1/reservations/{reservation_id}/confirm
Body: { payment_method_id: "pm_abc" }
Response: { booking_id: "bk_def", room_id, check_in_date, check_out_date, total_amount }
The server re-validates that the reservation has not expired before charging. If the reservation is expired, the endpoint returns 410 Gone and the client must restart the checkout flow from the search step.
FR 2c: Cancel a reservation (explicit checkout abandon):
# Explicit release; expired reservations are also released by the background Expiry Worker
DELETE /v1/reservations/{reservation_id}
Response: 204 No Content
The client sends this on explicit cancel. The Expiry Worker handles silently abandoned reservations automatically. Both paths converge on the same state change: room status returns to available.
FR 3: Hotel manager: configure room types and inventory:
# Create a new room type; request triggers creation of N individual Room rows
POST /v1/hotels/{hotel_id}/room-types
Body: { name, max_occupancy, base_price, amenities, room_count }
Response: { room_type_id, rooms_created: 12 }
# Update pricing or mark a room type as inactive for maintenance
PATCH /v1/hotels/{hotel_id}/room-types/{room_type_id}
Body: { base_price?, is_active? }
Response: { room_type_id, updated_fields: [...] }
PATCH over PUT because managers rarely update all fields at once. room_count in the POST body causes the server to generate that many Room rows in the same transaction, keeping the room type and its physical rooms atomically consistent.
45-minute interview approach
Use this section only as the pacing plan for a hotel-booking design prompt; keep the concurrency and storage detail in the architecture and deep dives.
- 0-5 minutes β clarify the product: Confirm search filters, date semantics, whether one physical room must cover the whole stay, hold duration, cancellation, payment timing, and manager operations.
- 5-10 minutes β requirements and estimates: State the illustrative peak search and booking rates, latency targets, durability, and the rule that authoritative inventory cannot double-book.
- 10-15 minutes β entities and APIs: Identify Hotel, RoomType, Room, Reservation, Booking, and User. Sketch search, availability detail, reservation, confirm, cancel, and manager endpoints with idempotency where money or inventory is involved.
- 15-25 minutes β baseline architecture and flows: Draw the search projection, Booking Service, booking-side inventory store, payment boundary, expiry worker, and catalog event path. Walk through search and then a reservation/confirmation request.
- 25-35 minutes β choose deep dives: Let the interviewer select date-range availability, concurrent booking, or checkout holds. Compare the naive option with the selected design and state which store is authoritative.
- 35-41 minutes β reliability, security, and operations: Cover payment/database partial failure, expiry-worker idempotency, search-index lag, role-based manager access, payment-data boundaries, and reconciliation metrics.
- 41-45 minutes β trade-offs and close: Explain eventual consistency in search, the cost of row locks and inventory projections, the multi-region boundary, and the changes needed for fraud, pricing, or active-active booking.
High-level architecture and critical flows
There are four critical flows: search reads an asynchronous projection; reserve and confirm make authoritative inventory and payment state changes; manager configuration updates catalog and inventory projections; and expiry releases abandoned holds. Search can be stale, but the reservation transaction must re-check the booking-side source of truth.
1. Hotel search
The search path: a user submits location and date filters; the system returns a paginated list of hotels with available room types and lowest price.
The naive approach is a SQL query against the reservations table: find all hotels near the location, for each hotel find which rooms are booked in the date range, subtract from total, return what remains. At the illustrative 50M DAU and 10,000-searches-per-second target, this becomes a full table scan on a 500M-row table for every request. It does not meet the target without a search projection; precompute the common availability view and keep the transactional database for authoritative checks.
Start with the naive SQL approach long enough to show its failure mode, then introduce the search projection. That makes the reason for Elasticsearch (or an equivalent index) explicit rather than treating it as a memorized component.
Split search from booking before adding capacity details. Search is read-heavy, geospatial, and tolerant of a few seconds of staleness; booking is write-bound, transactional, and must make an immediate inventory decision.
Components:
- Client: Web or mobile app sending search queries via the API Gateway.
- API Gateway: Routes
/v1/hotels/searchtraffic to the Search Service; handles auth token validation and rate limiting. - Search Service: Stateless service that applies geo and availability filters against Elasticsearch and returns paginated results.
- Elasticsearch (Search Index): Hotel documents with geo-coordinates indexed for bounding-box queries and projected availability counts per date range. Updated asynchronously when catalog or booking events are processed.
- Hotels DB (PostgreSQL): Source of truth for hotel metadata, room types, and room definitions. The booking-side store owns the transactional inventory counters; Elasticsearch is populated through a change-event pipeline.
Request walkthrough:
- Client sends
GET /v1/hotels/search?location=NYC&check_in_date=2026-04-01&check_out_date=2026-04-03&guests=2. - API Gateway authenticates the request and routes to the Search Service.
- Search Service queries Elasticsearch: geo-filter by bounding box around the requested location, filter
available_count[2026-04-01] > 0ANDavailable_count[2026-04-02] > 0for all nights in the range, sort by rating or price. - Elasticsearch returns matching hotel documents with pre-aggregated availability per room type.
- Search Service returns paginated results with a cursor for the next page. No PostgreSQL reads on this path.
The search path never touches the reservations table directly. Availability counts in Elasticsearch are maintained by a background pipeline that processes booking events. The booking path comes next.
2. Room reservation and payment
The booking path: a user selects a room type; the system assigns a specific room, creates a reservation with an expiry, takes payment, and confirms the booking.
Adding a dedicated Booking Service keeps booking logic isolated from search. Both services scale independently: search is read-heavy while booking is write-bound with ACID requirements. Merging them means a search surge can compete with booking writes for connections and CPU.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.