Design a Hotel / Airbnb Reservation System

Design a hotel or vacation rental reservation system like Booking.com or Airbnb. The core challenge isn’t the search UI — it’s preventing double bookings while handling concurrent requests at scale, and doing it without sacrificing availability or performance.

Requirements Clarification

  • Core operations: Search available listings by location/dates, view listing details, make a booking, cancel a booking, manage listings (hosts).
  • Scale: 100M users, 10M listings (Airbnb has ~7M). 10M searches/day, 1M bookings/day.
  • Consistency: A room that’s booked must immediately be unavailable to other users. No double bookings — ever.
  • Booking flow: Search → Select → Reserve (hold for 10 min) → Pay → Confirm. Payment failure releases the hold.
  • Cancellation policy: Varies per listing. Track refund eligibility based on cancellation window.

Back-of-Envelope

  • 10M searches/day → ~116 searches/sec (spiky: 10× during peak travel season)
  • 1M bookings/day → ~12 bookings/sec
  • 10M listings × avg 365 nights = 3.65B room-night inventory records
  • Read/write ratio: ~100:1 (searches >> bookings)

The Core Problem: Preventing Double Bookings

Two users simultaneously try to book the same room for the same dates. Without proper locking, both see “available,” both pay, and the host has two guests showing up at the same door. This is the central correctness requirement.

Three approaches, in order of sophistication:

1. Pessimistic locking (SELECT FOR UPDATE):

BEGIN;
SELECT * FROM reservations
WHERE listing_id = ? AND dates_overlap(check_in, check_out, ?, ?)
FOR UPDATE;   -- locks these rows for the duration of the transaction

-- If no conflicting rows found, insert the new reservation
INSERT INTO reservations (listing_id, user_id, check_in, check_out, status)
VALUES (?, ?, ?, ?, 'CONFIRMED');
COMMIT;

Pros: guaranteed correctness. Cons: serializes all booking attempts for the same listing, creates contention at high booking rates. Acceptable when booking frequency per listing is low (most listings get booked <10 times/day).

2. Optimistic locking with version numbers: Read the availability, compute a version hash, write with a WHERE version = {seen_version}. If another writer changed the record first, the update affects 0 rows and you retry. Better for low-contention scenarios.

3. Database-level unique constraint: The most elegant solution for continuous date ranges:

-- Reservation table with a constraint enforcing no overlap
CREATE TABLE reservations (
  reservation_id BIGINT PRIMARY KEY,
  listing_id     BIGINT NOT NULL,
  check_in       DATE NOT NULL,
  check_out      DATE NOT NULL,  -- exclusive end date
  status         ENUM('PENDING','CONFIRMED','CANCELLED'),
  CONSTRAINT no_double_booking EXCLUDE USING GIST (
    listing_id WITH =,
    daterange(check_in, check_out) WITH &&
  )
  WHERE (status != 'CANCELLED')
);

PostgreSQL’s exclusion constraint with range types enforces non-overlapping date ranges at the DB level — even with concurrent inserts. The database’s constraint engine handles the race condition. This is the approach Airbnb-scale systems use.

Architecture

Client → API Gateway
           ├─ Search Service    → Elasticsearch (listings index)
           ├─ Listing Service   → MySQL (listing metadata) + S3 (photos)
           ├─ Reservation Service → PostgreSQL (reservations, inventory)
           └─ Payment Service   → (see Design a Payment System)

Reservation Service → [Kafka] → Notification Service (email/push to host + guest)
                   → [Kafka] → Search Index Update (mark dates as unavailable)

Availability Calendar

Searching “available 3-bedroom apartments in Paris, July 4–8” needs to exclude listings that are booked for any of those dates. Two approaches:

Real-time join (simple, doesn’t scale): Query reservations table for conflicts during search. At 116 searches/sec × checking 10K Paris listings = 1.16M reservation lookups/sec. Too slow.

Pre-computed availability calendar (Airbnb’s approach): Maintain a separate availability table per listing per date:

listing_availability:
  listing_id  BIGINT
  date        DATE
  is_available BOOLEAN
  price       DECIMAL(10,2)   -- dynamic pricing can vary by date
  PRIMARY KEY (listing_id, date)

When a reservation is confirmed or cancelled, update the calendar rows for those dates. Searches query the calendar, not the reservations table. A listing is included in results only if ALL requested dates are available.

Index: (is_available, date, listing_id) — allows fast “find listings available on all dates in range.”

Search Service

Geospatial search + availability + filters (price, amenities, rating) requires Elasticsearch or a similar inverted index. The search flow:

  1. Elasticsearch query: geo_distance filter + amenity/price/rating filters → candidate listing IDs
  2. Availability filter: check listing_availability table for those IDs on requested dates (fast PK lookup)
  3. Ranking: sort by relevance score (price, rating, host response rate, distance)
  4. Return paginated results with listing thumbnails

Elasticsearch is updated asynchronously from the listing and reservation services via Kafka. Slight delay (seconds) between a booking and the listing disappearing from search results is acceptable.

Reservation Flow with Hold

1. User selects dates → POST /reservations {listing_id, check_in, check_out}
2. Reservation Service creates reservation with status=PENDING, expires_at=now()+10min
3. Returns reservation_id to client
4. Client calls Payment Service with reservation_id
5. Payment succeeds → Reservation Service updates status=CONFIRMED, clears expires_at
6. Payment fails/timeout → Reservation Service cancels reservation (background job)
7. Notification Service sends confirmation emails/push to both parties

A background job sweeps for PENDING reservations past their expiry and cancels them. This releases the hold and makes dates available again. The sweep runs every 60 seconds.

Pricing: Static vs Dynamic

Base price is set by the host. Dynamic pricing (Airbnb Smart Pricing, similar to Uber surge) adjusts nightly prices based on demand, local events, seasonal patterns, and competitor pricing. Store dynamic prices in listing_availability.price — updated nightly by a batch pricing service. At booking time, lock in the price shown during search (store it in the reservation) to avoid bait-and-switch.

Database Sharding

Shard reservations and listing_availability by listing_id (consistent with the exclusion constraint). All operations for a given listing — availability checks, conflict detection, booking confirmation — land on the same shard and can use local ACID transactions. Cross-shard transactions are avoided entirely by this sharding key choice.

Interview Follow-ups

  • How do you handle a host who blocks dates on multiple platforms simultaneously (Airbnb + VRBO + direct booking)?
  • Design the host payout system — hosts are paid 24 hours after guest check-in.
  • How does your system handle overbooking? (Hotels sometimes deliberately overbook, expecting cancellations.)
  • A user’s payment fails mid-booking during a sold-out weekend in NYC. How do you handle the race condition if another user is waiting?
  • How would you add a “best price guarantee” feature that notifies users if a listing’s price drops after booking?

Related System Design Topics

  • SQL vs NoSQL — why PostgreSQL with exclusion constraints beats NoSQL for inventory locking
  • Caching Strategies — caching search results and listing details; cache invalidation on booking
  • Message Queues — Kafka events from reservation service to notification and search index services
  • Design a Payment System — the payment leg of the booking flow: idempotency, hold-then-charge
  • Database Sharding — sharding reservations and availability by listing_id for local ACID guarantees

Companies That Ask This System Design Question

This problem type commonly appears in interviews at:

See our company interview guides for full interview process, compensation, and preparation tips.

Scroll to Top