Home › Blog › Design a Hotel Booking System

Design a Hotel Booking System (Airbnb-Style) Walkthrough

How to answer the hotel reservation or “design Airbnb” question: split search from booking, model inventory per night, stop double bookings, and hand off to payments safely.

A hotel booking system is two systems that share one source of truth. The search path is read-heavy, cache-friendly and can live with availability that is a few seconds stale; the booking path is low-volume but must never sell the same room-night twice. A strong interview answer models inventory per room type (or listing) per night, makes the reserve step atomic in the database, makes every write idempotent, and confirms a booking only after a short-lived hold survives the payment step.

1. Clarify the requirements

Pin down scope first. A reasonable functional list:

Non-functional requirements, in priority order: no double bookings, fast search, high availability for browsing, and a booking flow that may take a couple of seconds. Say explicitly that you favor consistency on the booking path and availability on the search path. Leave out reviews, messaging and recommendations.

Then ask the question that changes the data model: are we selling hotel rooms or unique listings?

Hotel chainAirbnb-style marketplace
What a guest booksA room type (any one of N identical rooms)One specific listing
Inventory per nightA count, such as 40 king roomsFree or taken
OverbookingSometimes allowed by policyNot acceptable
Who edits supplyThe property's own staff and systemsMany independent hosts, at any time
ConfirmationUsually instantInstant, or after host approval

Also ask whether guests pay at booking or at the property, and whether one booking can include several rooms.

2. Back-of-the-envelope numbers

The ByteByteGo hotel reservation walkthrough sizes a chain of 5,000 hotels and 1 million rooms. At 70% occupancy and an average stay of three nights, that is roughly 240,000 reservations a day, or about 3 bookings per second. Its funnel assumes around 300 QPS on hotel and room detail pages, 30 QPS reaching the booking page and 3 QPS actually reserving.

Two conclusions follow:

An Airbnb-style marketplace has more listings, but the shape holds: the hard part is correctness, not throughput.

3. Split the search path from the booking path

Search pathBooking path
TrafficVery high and spikyLow
StoreSearch index (Elasticsearch or OpenSearch) plus cachesRelational database, the source of truth
ConsistencyEventual; seconds of lag is fineStrong, transactional
Worst acceptable failureShows a room that was just takenRejects a booking; never oversells

On the search side, index each property or listing as a document with a geo_point location, capacity, amenities, price and a compact availability summary (open nights, or rooms left per night). A map view becomes a geo_bounding_box filter combined with date, guest and price filters, then ranking. The Google Maps walkthrough compares the geospatial indexes underneath (geohash, S2, H3, quadtrees).

Keep the index fresh by streaming changes out of the booking database (change data capture or a transactional outbox) rather than having the app write to both stores. If search shows a listing booked a moment ago, the booking path catches it.

4. API design

GET  /v1/search?lat=&lng=&check_in=2026-12-18&check_out=2026-12-21&guests=2
GET  /v1/properties/{id}/availability?from=2026-12-01&to=2026-12-31

POST /v1/holds            Idempotency-Key: <uuid>
     { propertyId, roomTypeId, rooms, checkIn, checkOut, guests }
     -> { holdId, expiresAt, quotedPrice }

POST /v1/reservations     Idempotency-Key: <uuid>
     { holdId, paymentMethodToken }
     -> { reservationId, status }

DELETE /v1/reservations/{id}

Two details matter. Dates are nights in a half-open range, [check_in, check_out), so one guest's check-out day can be the next guest's check-in. And the hold returns a price snapshot and an expiry, so a rate change mid-checkout does not surprise the guest.

5. Data model: inventory per night

room_type_inventory
  hotel_id, room_type_id, date     -- composite primary key
  total_inventory                  -- rooms sellable that night
  total_reserved                   -- held + confirmed
  version                          -- for optimistic locking

reservation
  reservation_id                   -- PK; doubles as idempotency key
  guest_id, hotel_id, room_type_id, rooms
  check_in, check_out              -- [check_in, check_out)
  status                           -- HELD | CONFIRMED | CANCELED | EXPIRED
  hold_expires_at, total_price, currency, payment_id

Pre-create inventory rows for a fixed window ahead with a daily job (the ByteByteGo design uses two years). A three-night stay touches three rows. Assign the physical room at check-in, not at booking, so inventory stays a simple count. Nightly prices live in a separate rate table.

For unique listings, keep one row per listing per night, or store each reservation as a date range and let the database reject overlaps (next section). Treat status as a small state machine, HELD → CONFIRMED → CANCELED or HELD → EXPIRED, and make each transition a conditional update such as WHERE status = 'HELD', so a late payment webhook and the expiry job can never both win.

6. Preventing double booking

“Double booking” is really two different bugs:

Idempotency for retries

The client generates a random key (a UUID v4 is fine) per checkout attempt and sends it with every POST. Store it as the reservation's primary key or a unique column, so a duplicate insert hits the constraint and the server returns the original result. Stripe's API (v1) is a good model: it saves the first response for a key, returns it on retries, errors if a retry's parameters differ, and may prune keys once they are at least 24 hours old.

Atomic reserve for races

For hotel room types, one conditional update per stay does the job:

BEGIN;
UPDATE room_type_inventory
   SET total_reserved = total_reserved + :rooms
 WHERE hotel_id = :hotel AND room_type_id = :room_type
   AND date >= :check_in AND date < :check_out
   AND total_reserved + :rooms <= total_inventory;
-- rows updated != number of nights?  ROLLBACK, return 409
INSERT INTO reservation (...) VALUES (..., 'HELD', ...);
COMMIT;

Under the default isolation levels in PostgreSQL and MySQL, the update locks each row it changes, so two requests for the last room serialize: the second waits, re-checks the capacity condition against the committed value and updates fewer rows than there are nights. If overbooking is allowed, the check becomes a policy limit; the ByteByteGo version uses total_reserved + n <= 110% × total_inventory.

For unique listings, PostgreSQL can enforce “no overlapping stays” directly. Its range-types documentation shows the idea with a room-reservation table: install the btree_gist extension and declare EXCLUDE USING GIST (room WITH =, during WITH &&). Adapted to listings, with a WHERE clause (a partial exclusion constraint, allowed by CREATE TABLE) so canceled and expired rows stop blocking dates:

CREATE EXTENSION btree_gist;

CREATE TABLE booking (
  reservation_id uuid      PRIMARY KEY,
  listing_id     bigint    NOT NULL,
  stay           daterange NOT NULL,   -- [check_in, check_out)
  status         text      NOT NULL,
  EXCLUDE USING GIST (listing_id WITH =, stay WITH &&)
    WHERE (status IN ('HELD', 'CONFIRMED'))
);

Two rows conflict only if they share a listing and their ranges overlap. Because daterange is half-open by default, a stay ending on the 21st and one starting on the 21st do not collide. A conflicting insert fails with a constraint error you map to “those dates were just booked.”

ApproachGood forWatch out for
Pessimistic lock (SELECT … FOR UPDATE)Easy to reason aboutHolds locks; lock nights in date order to avoid deadlocks
Optimistic lock (version column, retry on miss)Low contention, no locks heldRetries pile up when many guests want the same nights
Conditional update (capacity check in the WHERE)Simplest; one statement in any SQL databaseWriters to the same hot rows still queue
Exclusion constraintUnique listings and date rangesPostgreSQL-specific; needs btree_gist
Redis lock (SET key NX with expiry)A fast hint in the UINever the source of truth; Redis's own docs steer you to Redlock for locking

Because booking volume is low, the ByteByteGo walkthrough judges optimistic locking and a database constraint both good fits, and advises against pessimistic locking for deadlock and scalability reasons.

Never let a cache approve a booking. If your answer to “what stops two people getting the same room?” involves a cache, expect the interviewer to push until it breaks. The database that owns inventory makes the call.

7. Holds, the payment hand-off and expiry

  1. Hold. Run the atomic reserve and insert the reservation as HELD with an expiry a few minutes out.
  2. Pay. Call the payment provider with an idempotency key derived from the reservation ID, outside the database transaction. Never hold row locks across a network call.
  3. Confirm. On success, flip HELD → CONFIRMED and write a booking.confirmed event to an outbox table in the same transaction. Consumers send the email, notify the host and update the search index.
  4. Release. On payment failure, or when a sweeper job finds an expired hold, flip HELD → EXPIRED and give the nights back in the same transaction.
  5. Reconcile. The nasty edge: payment succeeds after the hold expired. If the nights are still free, re-reserve and confirm; otherwise void or refund automatically. A periodic job compares your records with the provider's.

Mention authorization versus capture. Stripe's documentation uses hotels as its example of authorizing before arrival and capturing at check-out, but an online card authorization is usually valid for only about 7 days (windows vary by card network) unless you use an extended authorization. For a stay booked months ahead, you either charge at booking or save the payment method and charge closer to the date. There is no two-phase commit with an external provider; the flow is a saga with compensating actions (void, refund). The payment system walkthrough covers the ledger and reconciliation side.

8. Caching, consistency and scaling

DataConsistencyWhy it is acceptable
Search resultsEventual (seconds)Booking rechecks
Calendar on a listing pageNear real time, from cacheOnly a hint
Inventory and reservationsStrong (ACID)Correctness
Emails, host alerts, analyticsEventual, via eventsSafe to retry

9. Pacing the round and common follow-ups

Round length varies by company; if you have about 45 minutes, one workable split is 5 minutes on requirements, 5 on numbers and APIs, 10 on the data model and the search/booking split, 15 on double booking and payments, and the rest on scaling. Likely follow-ups:

Rehearse the section 6 deep dive out loud until you can explain the race, the fix and the trade-off in under two minutes; a free AI mock interview in CoPilot Interview can ask system design questions aloud and score your spoken answer.

Rehearse this design before the real round

Paste a job description and your résumé, set the mix to system design, and answer out loud; every answer is scored with what worked and what to improve. The free plan gives you 15 minutes a day with no credit card.

Try a free AI mock interview

FAQ

How do you prevent double booking in a hotel reservation system?

Make the reserve step atomic in the database that owns inventory: a conditional update that succeeds only if every night still has capacity, an optimistic version check, or an exclusion constraint that rejects overlapping date ranges. Add idempotency keys so retries and double clicks create one reservation, not two.

Should room availability be cached?

Yes for search results and listing pages, where a few seconds of staleness is acceptable, but never as the source of truth. The booking path always rechecks and updates inventory in the database inside a transaction.

How is designing Airbnb different from designing a hotel booking system?

A hotel sells interchangeable rooms of a type, so inventory is a count per room type per night. An Airbnb-style listing is unique, so each night is simply free or taken, which suits a date-range constraint. Search, holds and payments work largely the same way.

Should I use pessimistic or optimistic locking for bookings?

Booking writes are low volume, so optimistic locking or a database constraint usually works well without holding locks. Pessimistic locking with SELECT ... FOR UPDATE is safe but needs consistent lock ordering to avoid deadlocks.

How does payment fit into the booking flow?

Place a short-lived hold on the inventory, call the payment provider with an idempotency key, and confirm the reservation only after payment succeeds. If payment fails or the hold expires, release the nights; if payment succeeds after expiry, re-check availability or refund automatically.