System Design · step by stepDesign a Hotel Booking System
Step 1 / 9

Design a Hotel Booking System — the walkthrough in full

A written version of the interactive walkthrough above — the same steps, decisions and trade-offs, laid out for reading, reference and search.

The big idea

What is a hotel booking system?

A hundred people are looking at the same last room in the same hotel for the same weekend, at the same moment. Exactly one of them should get it — not zero (a room sits empty because everyone's request errored), and definitely not two (a guest arrives to find their room already occupied). And underneath the excitement of one room, there's a multi-night STAY: three nights need three separate date-rows to all agree to sell.

This is fundamentally an inventory consistency problem wearing a travel-industry costume — the same shape as a flash sale, but keyed by (room type, date) instead of a single SKU, and with the added twist that a booking spans a range of dates that must succeed or fail together. We'll build the naive version, break it, then fix it properly.

How to read this: Each step opens with a real design decision — make the call before I show you what ships. Watch the diagram grow, hover the boxes, and at the end kill inventory and race two bookings for the last room to see what survives. Hit Begin.

Step 1 · The baseline

Read, then decrement

The obvious first implementation: read the current room count for a date, check if it's greater than zero, then write count-1 back. Two guests hit "book" for the last room within milliseconds of each other. What happens?

Design decision: Read-then-decrement, two nearly-simultaneous requests for the last room. What's the failure mode?

The call: Both requests read "1 available," both proceed, and both bookings succeed — overselling the room. — This is the classic check-then-act race: both reads happen before either write, so both see the room as available and both think they succeeded. The hotel now has two confirmed guests for one room — a real, costly failure, not a hypothetical.

Read-then-decrement is a classic check-then-act race: the gap between reading availability and writing the new count is exactly wide enough for two concurrent requests to both see "available" and both succeed, overselling the room. The fix has to make the check and the decrement one atomic operation, not two.

Check-then-act: Any "read a value, decide based on it, then write" sequence that isn't atomic is a race condition waiting for enough concurrency to trigger it. Inventory, seat selection, and flash sales all share this exact bug shape — and share the same fix: a single atomic conditional write.

Step 2 · Atomic, and across every night

A 3-night stay needs all 3 nights to agree

Fix the single-date race with an atomic conditional decrement: UPDATE inventory SET count = count - 1 WHERE date = ? AND count > 0, which only succeeds if a room was actually available. Good — but a real booking is a stay, spanning 3 consecutive nights, each its own date-row. What guarantees you don't end up with a room for night 1, but sold-out on night 2?

Design decision: A 3-night stay touches 3 separate date-rows. What ensures they all succeed together or not at all?

The call: Wrap all N date-rows' conditional decrements in a single atomic transaction: either every night's decrement succeeds, or the whole transaction rolls back and nothing changes. — A multi-night stay is only a valid booking if EVERY night is available. Wrapping all the conditional decrements in one database transaction gives you exactly that all-or-nothing guarantee — if night 2 is sold out, the transaction aborts and night 1's decrement (and every other night's) is rolled back too, leaving inventory untouched and the guest cleanly told "not fully available."

Wrap the whole stay in a single atomic transaction: attempt the conditional decrement for every night of the stay together, and commit only if all of them succeed. If any single night is sold out, the entire transaction rolls back — no partial booking, no inventory left in a half-decremented state.

All-or-nothing across a range: This is the same "atomicity" guarantee databases give you for a single row, deliberately extended across a whole set of rows that must change together or not at all. A multi-night booking, a multi-item order, and a multi-leg flight all need this same "the whole thing succeeds or none of it does" property.

Step 3 · Hold, don't just book

Reserve while the guest finishes checkout

A guest picks a room, but still has to enter payment details — that takes 30-90 seconds, sometimes longer. If you decrement inventory only at the very end (after payment succeeds), someone else could book the same room in the meantime and the guest's card gets charged for a room that's gone. If you decrement immediately and the guest abandons checkout, that room is lost to sale forever.

Design decision: A guest is mid-checkout for 60 seconds. How do you prevent both overselling AND losing inventory to abandoned carts?

The call: Place a short-lived HOLD on the inventory the instant checkout starts, with a TTL — confirm it into a permanent booking if payment succeeds, or auto-release it back to the pool if the TTL expires first. — A hold is the reserve-then-confirm pattern: inventory is atomically decremented immediately (so no one else can take it), but tagged as a temporary hold with an expiry. If payment completes in time, the hold becomes a real booking. If it times out (or the guest abandons), a background process releases the room back to the sellable pool automatically.

Introduce a Hold Manager: the instant checkout starts, atomically decrement inventory and record a hold with a short TTL (e.g. 10-15 minutes). If payment succeeds, the hold converts to a confirmed booking. If it expires, a background sweep releases the inventory back to the sellable pool. This is the reserve-then-confirm pattern — the same shape used by flash-sale checkouts and ticketing systems.

Reserve-then-confirm: Splitting "claim the resource" from "finalize the transaction" into two steps, joined by a TTL, is how you avoid both overselling (the claim is atomic and immediate) and permanent loss to abandonment (the TTL guarantees eventual release). It trades a small window of "held but maybe not sold" inventory for correctness on both failure modes.

Step 4 · Payment confirms the hold

Charge, then finalize

The hold is in place. Now the guest submits payment. What's the correct order of operations between charging the card and marking the booking confirmed?

Charge first, confirm second. Attempt the payment; only on success does the Hold Manager convert the hold into a permanent, confirmed booking. If the charge fails (declined card, timeout), the hold is released immediately rather than waiting for its full TTL to expire, so that inventory is back on sale as fast as possible instead of sitting artificially reserved for a booking that will never complete.

Fail fast, release fast: A hold's TTL is a safety net for silent abandonment (someone just closes the tab), not the primary release mechanism. Whenever you have positive confirmation that a booking has failed — a declined payment — release the hold immediately rather than waiting out the timer; every second of unnecessary hold is inventory a real, paying guest can't book.

Step 5 · Dynamic pricing

The rate changes with demand — but not on every request

Room rates should rise as a date fills up and fall for slow periods far in advance — classic revenue management. Millions of search page-views hit these rates every hour. Should you recompute the "correct" price live, from current occupancy, for every single page view?

Design decision: Rates depend on live occupancy and demand signals. Should every page view trigger a fresh price computation?

The call: Have the Pricing Engine recompute periodically (e.g. every few minutes, or on meaningful inventory change) and cache the result per room-type/date, so most reads are served from cache and only recomputation is expensive. — Prices don't need millisecond freshness — they need to be right within a small, acceptable window. Recomputing periodically (or triggered by significant inventory movement) and caching the result means the vast majority of page views are cheap cache reads, while the rate itself still tracks real demand within minutes.

The Pricing Engine recomputes rates periodically (or when inventory moves meaningfully), keyed by room-type and date, and caches the result with a short TTL. Nearly all read traffic — search results, browsing — is served from that cache; only a small fraction of requests trigger an actual recomputation, keeping the system fast under enormous read volume while rates still track real demand.

Cache the expensive, read-heavy thing: The read:write ratio on hotel prices is enormous — millions of views per booking. Any computation that expensive, with that access pattern, belongs behind a cache with a freshness window that's good enough for the business (a few minutes of price staleness is imperceptible; a live recompute per page view is wasteful.)

Step 6 · Search at scale

Split search from the source of truth

A city search returns thousands of properties across a date range. Should that query run live against the same strongly-consistent inventory table that bookings depend on for correctness?

Design decision: Search across thousands of properties, high volume, low individual stakes. Query live inventory, or something else?

The call: Maintain a separate, denormalized Search Index that's updated asynchronously from inventory — fast and scalable for browsing, allowed to lag by a few seconds, with the FINAL availability check still happening against strongly-consistent inventory at actual booking time. — This is a CQRS-style split: reads-for-browsing go to a fast, eventually-consistent index built for search (filters, sorting, geo, thousands of properties); the strongly-consistent inventory store is only touched at the moment of the atomic booking decrement, where correctness actually matters. A few seconds of index staleness is a fine trade for search performance, because search never itself sells a room.

Maintain a separate Search Index — denormalized, fast to filter and sort, fed asynchronously from inventory changes, and allowed to be a few seconds stale. All the high-volume browsing traffic hits this index; the strongly-consistent inventory store is only touched at the moment of actual booking, where an atomic conditional decrement is the real, final word on availability.

CQRS: separate reads from writes: Command-Query Responsibility Segregation: the path that must be strongly consistent (booking) and the path that must be fast at huge scale (search) are architecturally different problems with different consistency needs. Serving both from one store forces you to compromise one for the other; splitting them lets each be optimized on its own terms.

Step 7 · Cancellations, and choosing to overbook

Give rooms back — or sell more than you have, on purpose

A guest cancels. That room must go back on sale immediately. But some hotels intentionally allow a small amount of overbooking beyond 100% capacity, betting on a statistically predictable no-show/cancellation rate — the same strategy airlines use. Is that a bug, or a deliberate feature?

Cancellation releases the held inventory back to the pool exactly like a hold-timeout does — same mechanism, different trigger. Separately, controlled overbooking is a legitimate business policy layered on top of the technical system: a property manager can set an overbooking margin (e.g. sell 102% of physical rooms for a date with historically high no-show rates), and the inventory system enforces THAT ceiling atomically instead of the physical room count. The technical guarantee (never sell past whatever the configured limit is) stays identical — only the limit itself becomes a business decision, not always equal to "100% of rooms."

The system enforces a policy, not a fact: "Never oversell" and "the sellable count is exactly the physical room count" are two different claims — the booking system's real job is enforcing whatever ceiling the business configures atomically and correctly, whether that ceiling is set conservatively at 100% or deliberately above it to hedge against predictable cancellations.

Step 8 · The sharp edges

The last room, and the bots

Two real-world stress tests: the exact last-room race under genuine concurrent load, and search traffic from scrapers/rate-shopping bots hammering the system far harder than real guests ever would.

The last-room race is resolved by the same primitive from Step 1-2: the atomic conditional decrement means only one of two simultaneous requests can ever actually commit, full stop — no additional locking needed. For bot traffic, put rate limiting in front of the API (per IP/API-key) and lean hard on the Search Index cache — scraper load hits cheap, cached reads, never the strongly-consistent inventory path, so aggressive automated traffic can't degrade real bookings even at high volume.

Design for the unhappy path: Last-room race → atomic conditional write (no partial success possible). Cart abandonment → hold TTL (automatic release). Scraper load → rate limiting + serve from cache, never the write path. A booking system that only works when exactly one polite guest books at a time is a demo; one that survives real concurrency and real abuse is a product.

You did it

You just designed a hotel booking system.

  • R — e
  • A
  • R — e
  • P — a
  • D — y
  • S — e
  • C — a
built so the last room in the city sells exactly once — make the calls, kill inventory, run the gauntlet.
Finished this one? 0 / 58 System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.