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.
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.
| Option | Benefits | Limits |
|---|---|---|
| Strict no-oversell | One rule, enforced by CHECK (held + confirmed <= sellable) and every conditional UPDATE; a confirmation is an unconditional promise | Unused capacity from no-shows and late cancellations is not resold |
Explicit margin: sellable + overbook_margin per night | Recovers predictable no-show revenue; the margin is a visible, auditable number the same machinery enforces | Needs per-property no-show history, a walk policy (relocation, compensation) and agreement on who pays when the bet fails |
| Implicit overbooking through relaxed consistency | None worth having | Oversells at random, on exactly the hottest nights, with no compensation plan: a bug, not a policy |
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).
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.
| Option | Benefits | Limits |
|---|---|---|
| Room-type counts, room assigned at check-in | Booking is one counter check per night; the front desk assigns any clean room of the type | Cannot promise a specific room before arrival |
| Physical room at booking | Sells 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 type | A villa or named suite becomes a room type with sellable = 1, so the same machinery sells it | Only fits units that are genuinely unique |
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).
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).
| Option | Benefits | Limits |
|---|---|---|
| Relational (PostgreSQL) | Multi-row ACID transactions, CHECK and unique constraints, SKIP LOCKED, mature replication and tooling | One primary per shard; a hot row serializes like anywhere else |
| Distributed SQL | Serializable multi-row transactions across nodes and regions; automatic rebalancing | Higher commit latency (consensus per write), heavier operations; a hot row still serializes |
| In-memory counter store | Very fast atomic decrements | Not the system of record; multi-night atomicity only within one node's script; a crash between counter and database leaves them disagreeing |
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.
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).
| Option | Benefits | Limits |
|---|---|---|
| Per-night counter rows | Each night is its own lock and its own scarce counter; a stay touches check_out - check_in adjacent rows | A range read is N rows; a 14-night stay locks 14 rows |
| Per-room stay intervals with an exclusion constraint | EXCLUDE USING gist (room_id WITH =, stay WITH &&) (with btree_gist) makes double-booking a room impossible | Only for physical rooms (Decision 02); availability of a type is an overlap count; booking must choose a room |
| Precomputed calendar as the write model | One read answers a whole range | One calendar row covers many nights, so unrelated stays contend on it; partial updates are complex |
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.
Authoritative Reads vs. a Projection for Search
Stage 1 (Complete Design, Chapter 09) faces ~1,800 search QPS, bursting to ~5,400 availability reads/s, against ~45 booking attempts/s. The question is where those reads go.
| Option | Benefits | Limits |
|---|---|---|
| Authoritative Booking DB | Never stale | Range scans compete with booking transactions for the same rows, connections and CPU, the limit the naive design hit first |
| Read replicas | Usually sub-second staleness; no new pipeline | Search load still follows the booking schema and shards; replica lag is unbounded during incidents |
| Outbox-fed projection (Availability Read Model) | Shaped for range queries, scaled independently, isolated from booking | Lag ≤ ~60 s; a separate store to build, monitor and rebuild |
Selection criteria. Read volume, how much staleness a browsing signal tolerates, and whether search load may touch the booking path.
Choice for this workload. The projection, with the authoritative check made by the conditional UPDATE in the hold transaction. A stale positive costs a failed hold (409); a stale negative costs a missed sale until the lag catches up.
When another option wins. Low-QPS, correctness-sensitive views such as the hotel extranet read the primary or a replica. A small platform can start with replicas.
Remaining risk. Stale negatives on scarce nights lose sales during lag spikes; projection lag is an alerted SLO.
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).
| Option | Benefits | Limits |
|---|---|---|
| Periodic full rebuild | Simple; self-correcting | Staleness equals the rebuild interval; cost grows with the 400-day horizon, not with change rate |
| Incremental from the outbox | Lag of seconds; domain events with absolute per-night values and seq; reused for notifications and refunds | Relay, broker and idempotent projector to operate |
| Incremental from change data capture | No application code; catches writes that bypass the service | Row-level changes, not domain events; tied to the physical schema |
| Synchronous update in the booking transaction | Freshest | Couples booking latency and availability to search; still not atomic across two stores |
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.
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.
| Option | Benefits | Limits |
|---|---|---|
Conditional UPDATE … WHERE sellable - held - confirmed >= 1 | Check and write in one statement under one row lock; at READ COMMITTED a waiter re-checks and gets a clean 0-row answer | The decision must fit in a WHERE clause |
SELECT … FOR UPDATE, decide, then UPDATE | Arbitrary application logic between read and write | An 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 rare | On a hot row every loser re-reads and retries; any unrelated change bumps the version |
| SERIALIZABLE isolation | The database detects every anomaly | Losers abort with 40001 and retry the whole transaction |
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.
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.
| Option | Benefits | Limits |
|---|---|---|
| Database transaction | The lock, the check and the durable write are one atomic, crash-safe unit | Hot rows serialize inside the database |
| Distributed lock + database write | Can shed a burst before it reaches the database | A 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 store | Safe coordination of work outside one transaction | Every write must carry and check the token |
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.
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.
| Option | Benefits | Limits |
|---|---|---|
| Hold first (600 s TTL) | The traveler pays for a room that is already theirs; every failure has a clean state | Abandoned holds hide units for up to 10 minutes; needs an expiry worker and fencing |
| Immediate booking: authorize, then allocate and confirm | No hold, TTL or worker | Sold-out is discovered after authorization, so the authorization must be voided |
| Short hold extended while payment runs | Less hidden inventory per abandon | More writes on hot rows; more state transitions |
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.
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.
| Option | Benefits | Limits |
|---|---|---|
| Hold → attempt → authorize → capture → confirm | A confirmed reservation always has captured money behind it | Capture followed by a crash before confirmation needs reconciliation (Stage 7) |
| Hold → attempt → authorize → confirm → capture | Smaller crash window; natural for pay-at-hotel and deferred capture | A failed capture means cancelling a confirmed reservation and voiding the authorization |
| Authorize before the hold | Rejects bad cards before consuming scarce holds | Voids when the night is gone; authorizations visible on statements |
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.
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.
| Option | Benefits | Limits |
|---|---|---|
Synchronous 201 | One request, one answer | A client timeout cannot tell "failed" from "still running"; does not fit redirect-based payments |
Asynchronous 202, then poll or webhook | Fits every payment method | Every client needs polling and pending-state UI |
Synchronous with 202 fallback | Fast payments get 201; a same-key retry during payment returns 202 PENDING_PAYMENT and later the final result | Clients must handle both responses |
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.
Decisions at a Glance
| Decision | Choice | Flip when |
|---|---|---|
| 01 Overbooking | Strict no-oversell | The hotel owns an explicit margin and a walk policy |
| 02 Allocation unit | Room type; room at check-in | Specific rooms are sold |
| 03 Store | Relational | Multi-region writes are required |
| 04 Shape | Per-night rows + projected calendar | Physical rooms → per-room intervals |
| 05 Search source | Outbox-fed projection | Low-QPS views → primary or replica |
| 06 Freshness | Incremental from outbox | Writers bypass the service → CDC |
| 07 Concurrency | Conditional UPDATE | Rule needs app logic → FOR UPDATE |
| 08 Locks | Database transaction | Work outside one transaction → lease + fencing token |
| 09 Holds | Hold first, 600 s | Instant payment or pay-at-hotel |
| 10 Payment order | Capture, then confirm | Pay-at-hotel → confirm, then capture |
| 11 Confirmation | Sync with 202 fallback | Redirect payments dominate |
| 12 Authority | Platform for its bookings | No real-time integration → allotment |
| 13 Partitioning | Single primary, then hotel_id | Residency → region first |