Skip to content
System Design

Learn

Design a Hotel Booking System

Designing hotel reservations: modelling inventory by date, preventing double booking with pessimistic and optimistic locking, holds and expiry, idempotent payments and search.

4 min readUpdated 2 Sept 2026

#Transactions#Concurrency#Inventory#Search

Booking systems are the counterweight to the social-feed questions: here **consistency matters more than availability**. Selling the same room twice is a real-world failure, not a stale cache. Say that early — it is the framing the whole answer hangs on.

Requirements

Functional

  • Search hotels by location, dates and guests.
  • See available room types and prices.
  • Book, pay, modify, cancel.
  • Manage inventory across many properties.

Non-functional

  • No double booking. Strong consistency on the reservation path.
  • Search must be fast and can be eventually consistent.
  • Availability is read far more than it is written.
100K hotels, ~100 rooms each → 10M rooms
Searches: 10,000/second   Bookings: ~100/second
Read:write ≈ 100:1 — but the writes are the ones that must be exactly right

Modelling inventory

The critical modelling insight: do not book a specific room. Hotels sell room types, and assign a physical room at check-in. Reserving room 412 for a week creates false conflicts and fragments availability for no benefit.

room_inventory
  hotel_id | room_type_id | date       | total | booked
  1        | deluxe       | 2024-07-14 | 20    | 18
  1        | deluxe       | 2024-07-15 | 20    | 20

One row per room type per night. A three-night stay touches three rows, and availability

is "every night in the range has booked < total". This shape makes the concurrency control tractable and partial-availability queries natural.

Preventing double booking

The heart of the question. A naive check-then-write is a race:

User A: SELECT booked (18 of 20) → ok
User B: SELECT booked (18 of 20) → ok
User A: UPDATE booked = 19
User B: UPDATE booked = 19        ← one booking vanished

Pessimistic locking — lock the inventory rows for the duration of the transaction:

sql
BEGIN;
SELECT total, booked FROM room_inventory
 WHERE hotel_id = 1 AND room_type_id = 'deluxe'
   AND date BETWEEN '2024-07-14' AND '2024-07-16'
   FOR UPDATE;                              -- other transactions wait here

UPDATE room_inventory SET booked = booked + 1 WHERE …;
INSERT INTO reservations …;
COMMIT;

Correct and simple. The cost is contention on popular hotels and dates. Always lock rows in a consistent order (by date) or two overlapping multi-night bookings can deadlock.

Optimistic locking — no locks; check on write and retry:

sql
UPDATE room_inventory
   SET booked = booked + 1
 WHERE hotel_id = 1 AND room_type_id = 'deluxe'
   AND date = '2024-07-14'
   AND booked < total;         -- the guard IS the concurrency control
-- 0 rows affected ⇒ someone else took the last room ⇒ fail the booking

Better under low contention, and the booked < total predicate makes overselling impossible regardless of interleaving. A CHECK (booked <= total) constraint is the belt and braces.

Recommend optimistic with a database constraint as the backstop, and mention that pessimistic is the right call for genuinely scarce inventory such as a flash sale.

Holds

Users need time to enter payment details, and holding a database lock during a human's typing is unacceptable.

1. Select a room → create a HOLD with expires_at = now + 10 minutes
   (counts against availability, so nobody else can take it)
2. Payment succeeds → convert the hold into a confirmed reservation
3. Payment fails or the timer expires → release the hold

Expiry needs both a background sweeper and a lazy check at read time — a sweeper alone leaves stale holds visible until it next runs. This two-phase pattern is exactly how concert tickets and airline seats work too.

Search is a different system from booking, and should be read-optimised and eventually consistent.

Elasticsearch: hotel documents with geo coordinates, amenities,
               price ranges, ratings, and denormalised availability summaries
Query: geo filter → facet filters → rank by relevance/price/rating
Then:  verify true availability for the few results shown, against the
       inventory database

The two-step matters: the index can be seconds stale, so a hotel may appear available and not be. Verifying only the displayed page keeps the authoritative check cheap and the search fast.

Payments and idempotency

The booking and the charge must not diverge. Use an idempotency key per booking attempt so a client retry after a timeout cannot charge twice — see payment gateway. Because the payment provider is external, the sequence is typically: authorise, confirm the reservation, then capture, with a saga to compensate (release inventory, void the authorisation) if any step fails.

Deliberate overbooking

Real hotels overbook, because a predictable percentage of guests do not show. Model it as total = physical_rooms × (1 + no_show_rate) per date, with a walk policy when the prediction is wrong. It is worth raising unprompted: it shows you understand that the business rule, not the database, defines correctness here.

Follow-ups to expect

  • Sharding — by hotel_id, so a booking transaction stays on one shard.
  • Dynamic pricing — a separate service; the price is quoted and locked into the hold.
  • Cancellation policy — refund rules, and returning inventory to the pool.
  • Multi-room bookings — all or nothing across room types, in one transaction.
  • Group and corporate rates — inventory allocations reserved per channel.
  • Cross-channel inventory — the same rooms sold on aggregators; a channel manager

syncs availability, and the reconciliation lag is a real source of overselling.

Track this problem on the System Design sheet.

Frequently asked

How do you prevent double booking a hotel room?

Model inventory as one row per room type per night with total and booked counts, then make the decrement conditional: UPDATE … SET booked = booked + 1 WHERE booked < total. If zero rows are affected, someone else took the last room. A CHECK constraint that booked never exceeds total is the backstop. Pessimistic SELECT … FOR UPDATE also works and is preferable under very high contention, but requires consistent lock ordering to avoid deadlocks.

Why book a room type instead of a specific room?

Because hotels sell room types and assign physical rooms at check-in. Reserving room 412 specifically creates false conflicts and fragments availability — a night blocked in the middle of a range can make an otherwise bookable stay look unavailable. Counting availability per type per night avoids all of that.

How do you hold inventory while a user enters payment details?

Create a hold row with an expiry a few minutes out that counts against availability, then convert it to a confirmed reservation when payment succeeds or release it on failure or timeout. Holding a database lock during a human's typing is unacceptable. Expiry needs both a background sweeper and a lazy check at read time, or stale holds stay visible between sweeps.

Related