See It Flow
System Design · Trade-offs & Decisions · ~18 min read

Hotel Reservation Trade-offs & Decisions

Hotel booking system design trade-offs: for each decision, the workload constraint, realistic options, selection criteria and when another option wins. Topics include optimistic versus pessimistic locking, holds, search consistency, payment coordination and sharding by hotel_id.

Mangesh MandlikLast updated:
Decision 01

Strict No-Oversell vs. an Explicit Overbooking Policy

The invariant (Complete Design, Chapter 01) says confirmed stays never exceed sellable rooms. Airlines and many hotels deliberately sell past capacity because some guests never arrive, so the first decision is whether the platform enforces the invariant strictly or sells against a modeled margin.

OptionBenefitsLimits
Strict no-oversellOne rule, enforced by CHECK (held + confirmed <= sellable) and every conditional UPDATE; a confirmation is an unconditional promiseUnused capacity from no-shows and late cancellations is not resold
Explicit margin: sellable + overbook_margin per nightRecovers predictable no-show revenue; the margin is a visible, auditable number the same machinery enforcesNeeds per-property no-show history, a walk policy (relocation, compensation) and agreement on who pays when the bet fails
Implicit overbooking through relaxed consistencyNone worth havingOversells at random, on exactly the hottest nights, with no compensation plan: a bug, not a policy
Overbooking options

Selection criteria. Whether no-show rates are predictable per property and night, who carries the liability for a walked guest (the hotel or the platform), and whether the product can promise anything weaker than a guaranteed room.

Choice for this workload. Strict no-oversell. The platform is a reseller of the hotel's inventory and owns no walk process, so a confirmation must be unconditional.

When another option wins. A hotel's own revenue-management team with stable no-show history and a walk agreement. Model it as an overbook_margin column decided by the hotel; the CHECK becomes held + confirmed <= sellable + overbook_margin, and every other mechanism is unchanged.

Remaining risk. The guarantee holds against the platform ledger only. Rooms sold on other channels arrive through PMS sync and can still collide with platform bookings (Decision 12).

Decision 02

Room-Type Allocation vs. Physical-Room Assignment

The data model (Complete Design, Chapter 06) counts rooms per (hotel_id, room_type_id, stay_date). The alternative is to assign room 412 at booking time. The traveler needs a Deluxe room; housekeeping, maintenance and late checkouts are same-day facts.

OptionBenefitsLimits
Room-type counts, room assigned at check-inBooking is one counter check per night; the front desk assigns any clean room of the typeCannot promise a specific room before arrival
Physical room at bookingSells specific rooms (view, accessibility, connecting)Booking must pick a room that is free for every night of the stay; early assignments fragment the calendar and force room moves or blocks
Unique units as their own room typeA villa or named suite becomes a room type with sellable = 1, so the same machinery sells itOnly fits units that are genuinely unique
Allocation granularity

Selection criteria. Whether units of a type are interchangeable to the traveler, and whether the product sells a specific unit.

Choice for this workload. Room-type counts. If every night's count stays within the type's rooms, a check-in-time assignment without moves always exists: a stay is an interval of nights, and assigning rooms in order of arrival never needs more rooms than the busiest night does. Room moves appear only when rooms are fixed early or a room drops out of service.

When another option wins. Products that sell a specific room. Model unique units as their own room type, or keep per-room interval rows (Decision 04) for that segment only.

Remaining risk. A room that goes out of order after the type is sold out leaves sellable below held + confirmed; that is a capacity conflict for the hotel to resolve (Architecture Deep Dive 13).

Decision 03

Relational Database vs. a Specialized Inventory Store

Allocation needs one transaction over several rows (nights, hold, outbox) with durable commits. The load is modest: ~45 booking attempts/s and about 135 write transactions/s at peak, over ~60 M room-night rows (Complete Design, Chapter 05).

OptionBenefitsLimits
Relational (PostgreSQL)Multi-row ACID transactions, CHECK and unique constraints, SKIP LOCKED, mature replication and toolingOne primary per shard; a hot row serializes like anywhere else
Distributed SQLSerializable multi-row transactions across nodes and regions; automatic rebalancingHigher commit latency (consensus per write), heavier operations; a hot row still serializes
In-memory counter storeVery fast atomic decrementsNot the system of record; multi-night atomicity only within one node's script; a crash between counter and database leaves them disagreeing
Store for authoritative inventory

Selection criteria. Transaction scope (several rows atomically), durability, write rate against one primary's capacity, and operational familiarity.

Choice for this workload. A relational database. The write rate is far below one primary's capacity, and every invariant is a constraint or a conditional UPDATE the engine already enforces.

When another option wins. Distributed SQL when bookings must be accepted in several regions at once with no data loss on a regional failure. An in-memory store only as an admission gate in front of the database during extreme spikes, never as the authority.

Remaining risk. Hot-row throughput: 50-200 attempts on one room-night serialize at roughly 200-500 allocations/s whatever the store; short transactions and fail-fast 409s are the only remedy.

Decision 04

Per-Night Rows vs. Stay Intervals vs. a Precomputed Calendar

The invariant is per night, but a search asks about a range of nights. The storage shape decides whether writes lock exactly the nights they touch and how expensive a range read is (Complete Design, Chapter 06).

OptionBenefitsLimits
Per-night counter rowsEach night is its own lock and its own scarce counter; a stay touches check_out - check_in adjacent rowsA range read is N rows; a 14-night stay locks 14 rows
Per-room stay intervals with an exclusion constraintEXCLUDE USING gist (room_id WITH =, stay WITH &&) (with btree_gist) makes double-booking a room impossibleOnly for physical rooms (Decision 02); availability of a type is an overlap count; booking must choose a room
Precomputed calendar as the write modelOne read answers a whole rangeOne calendar row covers many nights, so unrelated stays contend on it; partial updates are complex
Inventory shapes

Selection criteria. Lock granularity on the write path, read cost on the search path, and whether the product allocates types or rooms.

Choice for this workload. Per-night rows for authority, and a per-room-type nightly array as the read-side projection (Architecture Deep Dive 10), so each path gets the shape that suits it.

When another option wins. Per-room intervals when physical rooms are sold. A calendar write model only where writes are rare and one writer owns the calendar.

Remaining risk. Long stays lock many rows; a MAX_STAY_NIGHTS bound caps statements and lock time.

Decision 06

How the Projection Stays Fresh

Having chosen a projection (Decision 05), the design must keep it within ~60 s of the authoritative rows without slowing the booking path (Architecture Deep Dive 10).

OptionBenefitsLimits
Periodic full rebuildSimple; self-correctingStaleness equals the rebuild interval; cost grows with the 400-day horizon, not with change rate
Incremental from the outboxLag of seconds; domain events with absolute per-night values and seq; reused for notifications and refundsRelay, broker and idempotent projector to operate
Incremental from change data captureNo application code; catches writes that bypass the serviceRow-level changes, not domain events; tied to the physical schema
Synchronous update in the booking transactionFreshestCouples booking latency and availability to search; still not atomic across two stores
Update mechanisms

Selection criteria. Freshness target, coupling to the booking path, and whether the same change stream has other consumers.

Choice for this workload. Incremental from the outbox: the event is one extra insert in a transaction that already exists, and the same stream drives notifications and refunds. A rebuild is kept for bootstrap and schema changes.

When another option wins. CDC when many writers bypass the service (legacy tools, bulk loads). Push (WebSocket/SSE) on top of either when live counters are a product feature; it changes delivery, not freshness.

Remaining risk. Relay or projector backlog stretches lag; outbox age and projection lag are alerted.

Decision 07

Pessimistic Locks vs. Optimistic CAS for Inventory Decrement

Stage 2 (Complete Design, Chapter 10) showed the lost update; Stage 3 (Complete Design, Chapter 11) needs a concurrency control that stays correct and fast when 50-200 attempts hit one room-night within seconds.

OptionBenefitsLimits
Conditional UPDATE … WHERE sellable - held - confirmed >= 1Check and write in one statement under one row lock; at READ COMMITTED a waiter re-checks and gets a clean 0-row answerThe decision must fit in a WHERE clause
SELECT … FOR UPDATE, decide, then UPDATEArbitrary application logic between read and writeAn extra round trip while the lock is held; longer queues on hot rows
Optimistic version (WHERE version = :v)No lock between read and write; good when conflicts are rareOn a hot row every loser re-reads and retries; any unrelated change bumps the version
SERIALIZABLE isolationThe database detects every anomalyLosers abort with 40001 and retry the whole transaction
Concurrency control on a room-night

Selection criteria. Expected contention on the hottest row, whether the rule fits in a predicate, and how long locks are held.

Choice for this workload. The conditional UPDATE at READ COMMITTED, in ascending key order (Architecture Deep Dive 03).

When another option wins. FOR UPDATE when the decision needs data a predicate cannot express. Optimistic versions for low-contention edits with user think time, such as the reservation version used by cancellation and modification.

Remaining risk. A conference night queues at roughly 200-500 allocations/s; lock_timeout and fail-fast 409s keep the queue from exhausting connections.

Decision 08

Database Transactions vs. Distributed Locks

Stage 3 (Complete Design, Chapter 11) puts the invariant in a database transaction. A common alternative is a distributed lock (Redis, ZooKeeper) around the check-and-write.

OptionBenefitsLimits
Database transactionThe lock, the check and the durable write are one atomic, crash-safe unitHot rows serialize inside the database
Distributed lock + database writeCan shed a burst before it reaches the databaseA lock holder that pauses past its lease keeps writing; without a fencing token the store cannot reject it, and the lock adds a second system to fail
Lease with fencing token checked by the storeSafe coordination of work outside one transactionEvery write must carry and check the token
Correctness boundary

Selection criteria. Whether the protected state lives in one database, and what happens when a lock holder pauses or crashes.

Choice for this workload. The database transaction, with no external lock. Holds, cursors and attempts already act as fences on their own rows.

When another option wins. Coordinating work that is not one transaction, such as one PMS consumer per hotel, uses a lease plus a fencing token; the per-hotel cursor compare-and-set is that token (Architecture Deep Dive 13).

Remaining risk. None for correctness; throughput limits are those of Decision 07.

Decision 09

Hold-First vs. Immediate Booking

Stage 4 (Complete Design, Chapter 12) faces a payment step that takes seconds to a minute while the last unit on a night can sell to someone else.

OptionBenefitsLimits
Hold first (600 s TTL)The traveler pays for a room that is already theirs; every failure has a clean stateAbandoned holds hide units for up to 10 minutes; needs an expiry worker and fencing
Immediate booking: authorize, then allocate and confirmNo hold, TTL or workerSold-out is discovered after authorization, so the authorization must be voided
Short hold extended while payment runsLess hidden inventory per abandonMore writes on hot rows; more state transitions
Claiming inventory before payment

Selection criteria. Payment duration, scarcity of the night, and the abandonment rate (each abandon hides a unit for the TTL).

Choice for this workload. Hold first, 600 s TTL, one active hold per traveler per stay, explicit release when the traveler leaves, and expiry deferred while a payment is in flight (Architecture Deep Dive 04).

When another option wins. Immediate booking for instant payment methods on ample inventory, and for bookings with no online payment step (such as corporate billing), where allocating straight to confirmed is enough.

Remaining risk. Hold hoarding across accounts on scarce nights; rate limits per principal and per room-night bound it.

Decision 10

Payment Ordering: Capture Before or After Confirmation

Stage 6 (Complete Design, Chapter 14) must order two systems that cannot share a transaction: the hold on the platform and the money at the provider.

OptionBenefitsLimits
Hold → attempt → authorize → capture → confirmA confirmed reservation always has captured money behind itCapture followed by a crash before confirmation needs reconciliation (Stage 7)
Hold → attempt → authorize → confirm → captureSmaller crash window; natural for pay-at-hotel and deferred captureA failed capture means cancelling a confirmed reservation and voiding the authorization
Authorize before the holdRejects bad cards before consuming scarce holdsVoids when the night is gone; authorizations visible on statements
Orderings

Selection criteria. When the rate plan charges the card, which crash window the system prefers to repair, and how visible voids are to travelers.

Choice for this workload. Hold → attempt → authorize → capture → confirm for prepaid rates, and reconciliation is the cost of that choice (Complete Design, Chapter 15).

When another option wins. Authorize → confirm → capture for pay-at-hotel and deferred-capture rates. Authorize-first in markets where invalid payment methods dominate checkout traffic.

Remaining risk. Captured but not confirmed. Reconciliation confirms if the hold fence still matches, otherwise refunds; that state should stay near zero.

Decision 11

Synchronous vs. Asynchronous Booking Confirmation

The booking API (Complete Design, Chapter 07) promises p95 ≤ 800 ms platform-side, but the provider call is bounded separately by auth_timeout = 60 s, and some payment methods complete only by redirect or webhook.

OptionBenefitsLimits
Synchronous 201One request, one answerA client timeout cannot tell "failed" from "still running"; does not fit redirect-based payments
Asynchronous 202, then poll or webhookFits every payment methodEvery client needs polling and pending-state UI
Synchronous with 202 fallbackFast payments get 201; a same-key retry during payment returns 202 PENDING_PAYMENT and later the final resultClients must handle both responses
Confirmation styles

Selection criteria. The slowest supported payment method, client capabilities, and the need for retries to be safe.

Choice for this workload. Synchronous with the 202 fallback, made safe by the idempotency key (Architecture Deep Dive 05). The confirmation notification, driven by the outbox, is the backstop for clients that never come back.

When another option wins. Fully asynchronous confirmation when redirect or bank-transfer methods dominate.

Remaining risk. A traveler who closes the app mid-payment learns the outcome only by notification.

Decision 12

Channel-Manager-Authoritative vs. Platform-Authoritative Inventory

Stage 9 (Complete Design, Chapter 17) makes the hotel's PMS or channel manager a second writer, because the hotel also sells through other channels. When its report contradicts rooms the platform already sold, one side must win.

OptionBenefitsLimits
Platform-authoritative for its own bookingsPaid, confirmed reservations are never silently invalidated; PMS updates are absolute values applied in cursor order and never below held + confirmedConflicts wait for operations; the platform learns of other channels' sales only through sync
Channel-manager-authoritativeFollows the hotel's single operational view across all channelsA stale or buggy feed can shrink inventory below rooms already sold and paid
AllotmentThe hotel contracts a block per night; the platform sells only from it, with no cross-channel raceUnsold rooms return at a release date; blocks sized wrong lose sales on one side
Inventory authority

Selection criteria. The platform's share of the hotel's sales, sync latency and reliability, and contractual liability for an oversell.

Choice for this workload. Platform-authoritative. PMS batches carry absolute per-night values, a per-hotel cursor compare-and-set rejects stale or replayed batches, and a night reported below held + confirmed becomes a recorded conflict instead of an applied change (Architecture Deep Dive 13).

When another option wins. Allotments for hotels without real-time integration. Channel-manager-authoritative only where the platform is a minor channel and the hotel explicitly accepts liability for invalidated bookings.

Remaining risk. A cross-channel oversell window bounded by sync latency, resolved by the hotel (relocation or a PMS correction).

Decision 13

Single Primary vs. Sharding by Hotel

About 135 write transactions/s fit one primary, so Stage 10 (Complete Design, Chapter 18) shards for isolation, not throughput: a conference at one hotel should not slow every other hotel.

OptionBenefitsLimits
Single primary + replicasSimplest operations; cross-hotel queries are localOne hot property or incident affects every hotel
Shard by hotel_id via a directoryEvery booking transaction stays on one shard; hot hotels can move to their own shard; failures are hotel-scopedCross-hotel reads need projections; resharding needs a move procedure
Shard by date or regionEven spread by time or geographyDate shards split one stay across shards and break the single-transaction guarantee; regions are too coarse to isolate one hotel
Partitioning

Selection criteria. Blast radius, hot-property isolation, and keeping every booking transaction on one shard.

Choice for this workload. A single primary until isolation is needed, then hotel_id sharding through a directory (Architecture Deep Dive 14).

When another option wins. Region-first partitioning when data residency demands it, with hotel_id sharding inside each region.

Remaining risk. Sharding does not help one hot room-night; cross-region replication keeps an RPO above zero.

Summary

Decisions at a Glance

DecisionChoiceFlip when
01 OverbookingStrict no-oversellThe hotel owns an explicit margin and a walk policy
02 Allocation unitRoom type; room at check-inSpecific rooms are sold
03 StoreRelationalMulti-region writes are required
04 ShapePer-night rows + projected calendarPhysical rooms → per-room intervals
05 Search sourceOutbox-fed projectionLow-QPS views → primary or replica
06 FreshnessIncremental from outboxWriters bypass the service → CDC
07 ConcurrencyConditional UPDATERule needs app logic → FOR UPDATE
08 LocksDatabase transactionWork outside one transaction → lease + fencing token
09 HoldsHold first, 600 sInstant payment or pay-at-hotel
10 Payment orderCapture, then confirmPay-at-hotel → confirm, then capture
11 ConfirmationSync with 202 fallbackRedirect payments dominate
12 AuthorityPlatform for its bookingsNo real-time integration → allotment
13 PartitioningSingle primary, then hotel_idResidency → region first