Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

System Design

Design a Hotel Booking System

Step 1 / 9

Learn system design by building a hotel/room booking platform like Booking.com or Marriott step by step.

The numbers to beat1 txncovers every night of the stayall-or-nothingno partial bookingsrollbackon any single sold-out night

The whole design, in writing

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

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.

Guestsearching, booking
New in this step: Guest.

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.

What the new pieces do

Guestclient
Someone searching a city and date range, then trying to lock in a specific room before someone else does.

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?

Booking APIsearch + reserveInventoryper room-type × date
New in this step: Booking API, Inventory.

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

  1. A database serializes individual STATEMENTS, but "read count, then later write count-1" is two separate operations with a gap between them — exactly where a second request can read the SAME stale count before the first one's write lands.

  2. 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.

  3. Nothing in a naive read-then-decrement makes the second request wait — that kind of serialization has to be explicitly designed in (a lock, or an atomic conditional write), not assumed for free.

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.

Why this piece earns its place

The conditional decrement fixes the race and quietly introduces a different one: it is not idempotent. The client that sent the booking request does not always find out what happened to it — a gateway timeout, a dropped connection, a phone switching cells mid-request — and the retry that follows looks byte-for-byte like the original. The row gets decremented twice, one guest ends up holding two rooms on a date where the second should have gone back on sale, and nothing anywhere logs an error; inventory just drifts quietly downward. The defense is to give every booking attempt a caller-generated idempotency key and write that key inside the same transaction as the decrement, so a replay collides with a key already recorded and returns the original outcome instead of taking another room. It is the same insight as the atomic write, one level up: the unit that has to happen exactly once is not decrement the counter, it is this specific attempt — and you only get that guarantee if the attempt carries a name the database can recognize the second time it sees it.

What the new pieces do

Booking APIgateway
The front door: routes searches to the index, and routes booking attempts to the strongly-consistent inventory path.
Inventorydata
The source of truth: how many rooms of each type remain for each specific date. Every write here must be atomic and strongly consistent — this is what "sold out" actually means.

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?

GuestBooking APIInventory
The system as it stands at this step. · swipe to pan the diagram

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

  1. A "partial booking" — a room for night 1 and 3 but not night 2 — isn't a valid product outcome for a hotel stay; it's a bug users would experience as your system randomly failing mid-checkout.

  2. Assuming later nights are available just because the first one was ignores that different nights can have completely different demand and remaining inventory — that assumption will be wrong regularly.

  3. 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.

Why this piece earns its place

That transaction changes the concurrency profile in a way worth saying out loud: the transaction now holds locks on several rows at once, for as long as it takes to try all of them. Two overlapping stays — one Friday to Sunday, one Saturday to Monday — can each end up holding a row the other still needs, and the database resolves that standoff by killing one of them. If transactions grab their nights in whatever order they happen to be written, that shows up as bookings failing at random on exactly the busy dates you care about. Acquire the date-rows in a fixed canonical order, ascending by date, and the cycle cannot form: the later stay simply waits behind the earlier one. The related discipline is to keep the transaction to the decrements alone — no payment call, no pricing lookup, no notification inside it — because a long stay already holds more rows for longer than a weekend does, and anything slow inside those boundaries multiplies contention on every date the stay touches.

  • 1 txncovers every night of the stay
  • all-or-nothingno partial bookings
  • rollbackon any single sold-out night

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.

Booking APIInventoryHold Manager
New in this step: Hold Manager. · swipe to pan the diagram

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

  1. A guest who abandons checkout (closes the tab, gets distracted) would permanently remove a sellable room from inventory — over enough abandoned checkouts, you lose real revenue to rooms that sit empty but "sold" in the system.

  2. 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.

  3. Holding a database lock open for the length of a human filling out a payment form (which could be seconds or minutes, or never finish) ties up the row and blocks every other concurrent booking attempt on that same date — a scalability disaster under real load.

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.

Why this piece earns its place

The hold introduces exactly one number you have to choose, and both directions of it cost you something real. The 30-90 seconds of checkout above is the typical case, not the tail: a card that triggers a bank verification step, a guest who mistypes and starts over, a couple talking it through. Set the TTL near typical and those guests lose the room while they are still inside the form, on precisely the scarce dates where it will not be there when they resubmit. Go long instead and every abandoned cart parks a sellable room outside the pool for the whole window; on a nearly-full date, a handful of concurrent abandonments can make a hotel look sold out to every searcher while it is not. So I would set it from the observed distribution of completion times, out in the tail rather than at the median, and treat the held-but-unsold inventory as the price of the pattern. What makes a generous TTL affordable is that it is not the primary release path at all — it only ever covers the guest who walks away saying nothing.

  • 10-15 mintypical hold TTL
  • atomicdecrement happens at hold time
  • auto-releaseon timeout or abandonment

What the new pieces do

Hold Managercontrol
Places a short-lived hold on inventory the instant a guest starts checkout, and automatically releases it back to the pool if payment doesn't complete in time.

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?

Booking APIInventoryHold ManagerPayment
New in this step: Payment. · swipe to pan the diagram

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.

Why this piece earns its place

This ordering splits one outcome across two systems, and the gap between them is where the money sits. If the process dies after the provider has taken the charge but before the hold becomes a booking, the guest has paid and has nothing — and worse, the hold’s clock is still running, so the room returns to the pool and is sold to someone else. The rule that saves you is that an unknown payment outcome is not a failed one. A timeout from the provider must leave the hold in a pending state the sweep refuses to release, resolvable only by asking the provider what actually became of that attempt — which means the charge has to carry the hold’s id so it can be looked up rather than guessed at. Only a definite decline releases early. Get the asymmetry the wrong way round and it is expensive: releasing on ambiguity risks a charged guest with no room, while holding on ambiguity costs a few minutes of one room’s availability.

What the new pieces do

Paymentservice
Charges the guest and, only on success, converts the temporary hold into a confirmed, permanent booking.

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?

Booking APIsearch + reserveInventoryatomic per date-rowPricing Enginedemand-based rate
New in this step: Pricing Engine.

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

  1. "Fully live" pricing on every page view means every search-result impression (which vastly outnumbers actual bookings) triggers a real-time demand computation — that's enormous wasted work for a number that doesn't meaningfully change between one page view and the next one a second later.

  2. Showing an "estimate" that could be meaningfully wrong at the moment of booking is a bad guest experience — the price a guest sees while browsing should be the price they can actually book at, with only rare, small drift.

  3. 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.

Why this piece earns its place

This step quietly assumes the rate a guest saw is the rate they pay, and a periodically recomputed, cached price breaks that assumption in the most visible way there is. Between the search impression and the confirm, a recompute can land — and on a date that is filling, revenue management moves the number up, so someone who has already been through checkout is charged more than the page promised. The fix is to pin the price into the hold at the moment the hold is created and honor it for that hold’s life; the recompute then applies to the next guest, not the one already committed. Notice that the opposite error is the cheap one: a stale-low rate served from cache is bounded by the TTL, only ever undercharges, and can be budgeted for. That asymmetry is the whole argument for caching here. Staleness that costs a little margin is fine; staleness that changes the number after a guest has agreed to it is not a pricing problem, it is a chargeback.

What the new pieces do

Pricing Engineservice
Computes the rate for a room-type/date from occupancy, demand, and lead time — recomputed periodically, not on every single page view.

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?

GuestBooking APIInventorySearch Index
New in this step: Search Index. · swipe to pan the diagram

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

  1. Search volume is orders of magnitude larger than booking volume, and hammering the same strongly-consistent store that guards against overselling with every browse-and-filter search would make that critical path slow and contended for everyone, including people actually trying to book.

  2. 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.

  3. A full city's live inventory, refreshed constantly, is far too large and too volatile to reasonably ship to and keep fresh on every client — this belongs server-side, close to the data.

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.

Why this piece earns its place

Eventually consistent is a promise about how far behind the index runs, and nothing built so far measures it. If the feed from inventory stalls — a stuck consumer, one poisoned message — search does not break. It keeps answering just as fast, out of an index that has quietly frozen, and guests browse an old snapshot with complete confidence. Every rejection then lands at the atomic decrement, which does its job and tells one guest after another that the room they just clicked is gone. So the health signal is not whether the pipeline process is alive; it is the age of the newest change in the index compared with now, because that is the only place the promise is actually stated as a number. Alert on it, and give the system somewhere to go when it is breached: fall back to a live availability check against inventory for the handful of results a guest is about to act on, or label availability as approximate rather than presenting frozen data as fact.

  • thousandsof properties per search
  • secondsof acceptable index staleness
  • 1 source of truthonly inventory decides a real sale

What the new pieces do

Search Indexcache
A denormalized, fast-to-query index across every property in a city — deliberately allowed to lag a few seconds behind real inventory in exchange for handling enormous search volume.

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?

Booking APIInventoryPaymentPricing EngineConfirm/Cancel
New in this step: Confirm/Cancel. · swipe to pan the diagram

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."

Why this piece earns its place

Controlled overbooking rests on an assumption nobody writes down: that no-shows are independent, so a margin derived from a date’s history still holds on the night itself. They are not independent. The things that keep guests away — a cancelled conference, a storm closing the airport — move a whole date at once, and so do the things that make everybody turn up, which is the direction that hurts. The night a margin that averaged out fine all year meets a fully-attended event, the hotel is physically short and a guest with a confirmed booking gets walked. The atomic enforcement the earlier steps bought is not the hard part here. The hard part is that this switch should not be thrown until the operational path behind it exists: oversold dates surfaced to the property days before arrival rather than discovered at the front desk, and a re-accommodation budget that makes the extra sale worth making. Set the margin per property per date from that property’s own history, cap it, and drop it to zero on any date with a known correlated event.

What the new pieces do

Confirm/Cancelevents
Sends booking confirmations and, on cancellation, releases the held inventory back to the pool and triggers any applicable refund.

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.

GuestBooking APIInventoryHold ManagerPaymentPricing EngineSearch IndexConfirm/Cancel
The system as it stands at this step. · swipe to pan the diagram

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.

Why this piece earns its place

Per-IP rate limiting answers the traffic that is merely expensive, and the expensive traffic is not the dangerous kind. Scraped searches land on a cached index and cost very little, while a real rush of guests onto a sale date looks identical to an attack from the limiter’s point of view — so tuning that limit hard mostly turns away customers. The request actually worth protecting is the one that touches inventory, and the most abusable request on this whole page is the hold: free to make, it removes a real room from the sellable pool for the full TTL, and it never has to pay. A script opening holds across every room-type on a competitor’s sold-out weekend can take a hotel off sale without completing a single booking, and each individual request is indistinguishable from a guest starting checkout. So the limit that matters is on holds, not searches — per account, per device, per payment instrument — with identity required before a hold is granted and a tighter expiry for an unauthenticated session than for a signed-in guest with a card on file.

You did it

You just designed a hotel booking system.

GuestBooking APIInventoryHold ManagerPaymentPricing EngineSearch IndexConfirm/Cancel
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • Read-then-decrement is a check-then-act race that oversells the last room — the fix is one atomic conditional write.
  • A multi-night stay needs every date-row's decrement wrapped in a single all-or-nothing transaction.
  • Reserve-then-confirm (a TTL'd hold) prevents both overselling during checkout AND permanent loss to cart abandonment.
  • Payment failure releases the hold immediately rather than waiting out its TTL, so inventory returns to sale as fast as possible.
  • Dynamic pricing is computed periodically and cached — search volume is far too high to recompute live per page view.
  • Search is a separate, eventually-consistent index (CQRS); only the strongly-consistent inventory store gets the final say at actual booking time.
  • Cancellations release inventory the same way a hold timeout does; controlled overbooking is a business policy the same atomic system can enforce above 100% capacity.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Why not just lock the whole date-row for the duration of checkout instead of using a hold with a TTL?

    A database-level lock held open for the length of a human filling out a payment form — which could be 10 seconds or, if they get distracted, never finish at all — blocks every other transaction trying to touch that same row for that entire time. Under real concurrent load (many guests checking out the same popular date simultaneously) that serializes what should be independent operations and tanks throughput. A hold instead does one FAST atomic decrement (claiming the inventory) and then tracks the "is this still valid" question as ordinary application state with a TTL, so the database itself is never blocked waiting on human behavior.

  2. Could you avoid the multi-night atomicity problem by just booking each night as a separate transaction?

    You could, but then a guest could end up with a booking for night 1 and night 3 of their stay but not night 2 — a state that's not a valid product for a hotel stay and that the guest would experience as the system inexplicably failing partway through checkout. Wrapping every night's conditional decrement into ONE transaction means the database itself guarantees you never end up in that half-booked state: either the whole stay succeeds, or none of it does, and the guest gets a clean "not available for these dates" instead of a confusing partial success.

  3. Why is it safe for search to be a few seconds stale, but not safe for the actual booking decrement?

    Because search and booking are answering different questions with different consequences for being wrong. Search answers "roughly what's available, for browsing and comparison" — if it's a few seconds stale and shows a room that just sold out, the WORST outcome is the guest clicks it and is told "sorry, just booked" at the next, authoritative step. Booking answers "reserve this room for me, right now, for real" — if THAT step is stale or non-atomic, the failure mode is two guests both being told they successfully booked the same room, which is a real operational and trust failure, not a minor inconvenience. Staleness is fine exactly where the consequence of staleness is cheap to correct.

  4. How would controlled overbooking actually be implemented without breaking the atomic-decrement guarantee?

    The atomic conditional decrement doesn't care WHAT the ceiling is — it just enforces "don't let the count go below zero relative to whatever total was configured." Controlled overbooking simply sets that configured total above the physical room count (e.g. 102 sellable units for 100 physical rooms on a date with a historically reliable 3%+ no-show rate), and the exact same atomic-decrement mechanism from Steps 1-2 enforces THAT number with identical correctness guarantees. The interesting engineering isn't in the concurrency control at all — it's in a separate, offline process that sets the overbooking margin per property per date based on historical no-show data, which is a forecasting problem, not a systems problem.

  5. What happens if the Hold Manager itself crashes with holds still outstanding?

    This is why holds are TTL-based rather than depending on the Hold Manager process staying alive to release them — the TTL is stored as data (in inventory/the hold record itself), not as an in-memory timer inside the Hold Manager. Any instance of the Hold Manager (or a separate sweep job) can scan for "holds past their TTL with no confirmed booking" and release them, regardless of which specific process created the hold or whether that process is even still running. Decoupling "who creates a hold" from "who is allowed to release an expired one" is what makes the system resilient to any single Hold Manager instance crashing mid-flow.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Read-then-decrement inventory fails under concurrency because…

    This is a check-then-act race — the fix is one atomic conditional write, not two separate steps.

  2. A 3-night stay booking should be…

    Wrapping every night in one transaction guarantees no partial, half-booked stays.

  3. A "hold" with a TTL exists to…

    Reserve-then-confirm prevents both overselling mid-checkout and permanently losing inventory to abandoned carts.

  4. Dynamic pricing is recomputed periodically and cached, rather than live per request, because…

    A short caching window is imperceptible to guests but saves recomputing an expensive value on every one of millions of page views.

  5. Search is served from a separate, eventually-consistent index instead of live inventory because…

    This is a CQRS split — only the final atomic decrement at actual booking time is the authoritative answer.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Search: browse thousands of properties in a city across a date range.
  • Book: claim a specific room for a multi-night stay — sold exactly once.
  • Hold: reserve inventory during checkout, then confirm on payment.
  • Price: show a demand-based rate per room-type and date.
  • Cancel: release a cancelled room back to the pool.

The qualities that shape everything

Each one names the mechanism that buys it.

Sell the last room exactly once
Replace read-then-decrement with a single atomic conditional write (decrement WHERE count > 0), so the check and the decrement can’t race apart.
A stay books all its nights or none
Wrap every night’s conditional decrement in one atomic transaction — if any night is sold out the whole thing rolls back, so there are no partial bookings.
Safe checkout without losing inventory
A TTL’d hold atomically claims the room the instant checkout starts and auto-releases it if payment doesn’t complete — reserve-then-confirm covers both overselling and abandoned carts.
Rates that track demand but survive read volume
The pricing engine recomputes periodically per room-type/date and caches the result with a short TTL, so millions of page views are cheap cache reads.
City-scale search that never slows bookings
A separate, eventually-consistent search index feeds asynchronously from inventory and takes all browse traffic; the strongly-consistent store is touched only at the final atomic decrement.
Enforce whatever ceiling the business sets
Cancellations release inventory like a hold timeout, and controlled overbooking just sets the sellable total above the physical count — the same atomic decrement enforces it exactly.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

One atomic conditional decrement over read-then-decrement

The gap between reading "1 available" and writing count−1 is exactly wide enough for two requests to both see the room free and both succeed — a check-then-act race that oversells. Making check and decrement one operation removes the gap.

One transaction across all nights over a decrement per night, independently

Booking each night separately can leave a guest with night 1 and 3 but not night 2 — not a valid stay. Wrapping every night’s conditional decrement in one transaction gives all-or-nothing: any sold-out night rolls the whole stay back.

A TTL’d hold (reserve-then-confirm) over a row lock for the whole checkout

Holding a database lock for the length of a human filling in a payment form blocks every other booking on that date — a scalability disaster. A hold does one fast atomic decrement and tracks validity as application state with an expiry.

Recompute periodically + cache over a live price on every page view

Search impressions vastly outnumber bookings, so recomputing demand-based pricing per view is enormous wasted work for a number that barely moves second to second. Cache it with a short TTL; the rate still tracks demand within minutes.

A separate search index (CQRS) over searching live inventory directly

Search volume dwarfs booking volume; hammering the strongly-consistent store with every browse would make the critical booking path slow and contended. A fast eventually-consistent index takes browsing, and inventory decides only the real sale.

The answer, out loud

What a strong answer to “Design a Hotel Booking System” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–4 min

    Scope it before drawing anything

    I want to be clear about the shape first. A guest searches a city over a date range, picks a room, holds it while they pay, and can cancel; the rate moves with demand. Almost all of that is ordinary read traffic, and the entire difficulty of the problem sits in one place — the instant a room is actually sold. Everything else can be a little stale, a little late, a little approximate, and nobody is harmed by it. But if two people are both told they booked the same room, someone arrives at a hotel at midnight with nowhere to sleep. So I am going to design the selling path first and hang the rest of the system off it.

  2. 4–9 min

    Sell the last room exactly once

    The naive version is: read the count for a date, check it is above zero, write count minus one. That is two operations with a gap between them, and under real concurrency two requests both read one available, both think they succeeded, and the room sells twice. I do not want to fix that with a lock — I want the check and the decrement to be the same operation. So the write is conditional: decrement where the count is greater than zero, and take whether it changed a row as my answer. Whichever request commits first wins, the second one’s condition is simply false, and that guest is told the room is gone.

    Built in step 1: Read, then decrement
  3. 9–15 min

    A stay is a range, not a room

    But a booking is not a room, it is a stay. Three nights is three separate date rows, and they can have completely different remaining inventory, so I cannot decide the stay from the first night alone. If I decrement them one at a time I can end up owning Friday and Sunday but not Saturday, which is not a product — the guest experiences it as checkout failing halfway through for no reason they can see. So all the nights go into one transaction: every night’s conditional decrement attempted together, committed only if all of them succeed. One sold-out night rolls the whole thing back, inventory is untouched, and the guest gets a clean not available for those dates.

    Built in step 2: A 3-night stay needs all 3 nights to agree
  4. 15–22 min

    Hold the room while they pay

    Now the timing. Payment takes real time — call it thirty to ninety seconds of somebody typing card details. If I decrement only at the end, someone else can take the room while they type and I charge a card for a room that is gone. If I decrement at the start and treat it as sold, an abandoned checkout removes a sellable room permanently. So I reserve then confirm: a hold manager does the atomic decrement the moment checkout opens and records a hold with an expiry, ten to fifteen minutes. Payment turns it into a booking; silence turns it back into inventory. The expiry lives on the record itself, so any sweeper can release it.

    Built in step 3: Reserve while the guest finishes checkout
  5. 22–27 min

    Charge, then finalize

    On the payment itself I would charge first and confirm second. The hold is what makes that order safe — the room is already claimed, so there is no race left to lose while the provider does its work. If the charge is declined I do not wait out the timer; I release the hold immediately, because every minute a doomed hold sits there is a minute a real paying guest cannot book that room. The timer is there for the guest who just closes the tab and tells me nothing at all. Positive news, act on it at once; no news, that is exactly what the expiry is for.

    Built in step 4: Charge, then finalize
  6. 27–33 min

    Keep pricing off the request path

    Pricing I would keep off the request path entirely. Rates should rise as a date fills and fall for slow periods booked far ahead, but search impressions massively outnumber bookings, and a demand computation per page view is enormous work for a number that barely moves between one view and the next. So a pricing engine recomputes per room-type and date periodically, or when inventory moves meaningfully, and caches the result with a short TTL. Browsing reads the cache. The rate still tracks real demand within minutes, which is the freshness the business actually needs from it.

    Built in step 5: The rate changes with demand — but not on every request
  7. 33–40 min

    Split search from the source of truth

    Search is the other half of the system and it wants the opposite properties. A city query spans thousands of properties across a date range with filters and sorting, at a volume far above booking volume, and I do not want any of that near the store that guards against overselling. So I split them: a denormalized search index, fed asynchronously from inventory, takes all the browsing and is allowed to run a few seconds behind. That is an acceptable trade because search never sells anything. The authoritative answer is the atomic decrement at booking time, and that is the only place the answer exists at all.

    Built in step 6: Split search from the source of truth
  8. 40–45 min

    Cancellations, overbooking, and the trade

    Cancellation is the hold release with a different trigger — the room goes straight back on sale. And I would raise overbooking myself, because it is a policy question rather than a bug: a property can choose to sell above its physical count against a historically reliable no-show rate, and the same atomic decrement enforces that ceiling just as faithfully. The system’s job is enforcing whatever limit is configured, not asserting that the limit equals the room count. The trade I have made throughout is strong consistency on the sale and eventual consistency on everything a guest merely looks at. With more time I would work through how holds behave for group and multi-room bookings, and what the property actually does on the night an overbooked date fills up.

    Built in step 7: Give rooms back — or sell more than you have, on purpose

What this teaches

Learn system design by building a hotel/room booking platform like Booking.com or Marriott step by step. An interactive guide covering why read-then-decrement inventory oversells rooms, atomic multi-night holds, reserve-then-confirm with a TTL, dynamic pricing under heavy search traffic, splitting a strongly-consistent booking path from an eventually-consistent search index, and controlled overbooking as a business policy.

Key takeaways

  • Read-then-decrement is a check-then-act race that oversells the last room — the fix is one atomic conditional write.
  • A multi-night stay needs every date-row's decrement wrapped in a single all-or-nothing transaction.
  • Reserve-then-confirm (a TTL'd hold) prevents both overselling during checkout AND permanent loss to cart abandonment.
  • Payment failure releases the hold immediately rather than waiting out its TTL, so inventory returns to sale as fast as possible.
  • Dynamic pricing is computed periodically and cached — search volume is far too high to recompute live per page view.
  • Search is a separate, eventually-consistent index (CQRS); only the strongly-consistent inventory store gets the final say at actual booking time.
  • Cancellations release inventory the same way a hold timeout does; controlled overbooking is a business policy the same atomic system can enforce above 100% capacity.

Concepts covered

  • What is a hotel booking system?
  • Read, then decrement
  • A 3-night stay needs all 3 nights to agree
  • Reserve while the guest finishes checkout
  • Charge, then finalize
  • The rate changes with demand — but not on every request
  • Split search from the source of truth
  • Give rooms back — or sell more than you have, on purpose
  • The last room, and the bots
built so the last room in the city sells exactly once — make the calls, kill inventory, run the gauntlet.
Finished this one? 0 / 65 System Designs done
NextDesign a Rate Limiter Closest system design to this one

Explore the topic

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

More System Designs