# System Design: [Hotel reservation system name]

> Reference design for a hotel group's central reservation system: inventory
> counted per room type and per night, multi-night stays booked atomically,
> rate plans with derived prices and stay restrictions (minimum length of
> stay, closed to arrival, closed to departure), an overbooking policy
> computed from show-up rates and walk costs, group blocks, and distribution
> of availability and rates to channels. It differs from seat booking in that
> nothing is sold by identity: a stay consumes one unit of a room type on each
> night, and the room number is assigned later. It follows the structure of
> the System Design Document template, adds a 45-minute interview path,
> access-pattern and failure-mode tables and a cited "How real systems do it"
> box, and is pre-filled with a worked design to adapt. Replace every
> `[bracketed]` value and every row marked *Example*; values marked *Example*
> are assumptions, not benchmarks.

<!--
Guidance: How to use this template
- Fix the unit of inventory first: room type per property per night. Every
  later decision (keys, locks, cache, overbooking) follows from it.
- Fill the assumptions in 3.1 with your portfolio: properties, rooms, room
  types, rate plans, booking horizon, look-to-book ratio.
- Agree with revenue management who owns overbooking limits and walk policy
  before writing 7.5; the system enforces a limit, it does not choose it.
- Decide which system is the source of truth for each stage of a stay: the
  central system for future reservations, the property management system
  (PMS) for in-house guests.
-->

## Document control

<!-- Guidance: A design is a proposal until it is approved; record who decided and when. -->

| Field | Value |
|---|---|
| Document ID | [SDD-HR-001] |
| System / feature | [Central reservation system] |
| Version | [0.1] |
| Status | [Draft / In review / Approved / Implemented / Superseded] |
| Owner | [@owner] |
| Author(s) | [@author] |
| Reviewers | [@reviewer-1], [@reviewer-2] |
| Last updated | YYYY-MM-DD |
| Classification | [Internal] |
| Difficulty | Intermediate |
| Revenue management owner | [@name] |
| Related | [Payment ledger design], [Channel integration specs], [ADR: overbooking limits] |

### Approvals

| Role | Name | Decision | Date |
|---|---|---|---|
| Tech lead | [@name] | [Approved / Changes requested] | YYYY-MM-DD |
| Staff / principal engineer | [@name] | [Decision] | YYYY-MM-DD |
| Revenue management | [@name] | [Decision] | YYYY-MM-DD |
| Hotel operations | [@name] | [Decision] | YYYY-MM-DD |
| Security and payments | [@name] | [Decision] | YYYY-MM-DD |

## Revision history

<!-- Guidance: Note what changed in the design, not the wording. -->

| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft for design review |
| 0.2 | YYYY-MM-DD | [@name] | *Example:* Overbooking limit capped by walk probability, not only expected profit |
| 0.3 | YYYY-MM-DD | [@name] | *Example:* Group blocks release to general inventory at cutoff |
| 1.0 | YYYY-MM-DD | [@name] | *Example:* Approved |

## Contents

<!-- Guidance: Keep aligned with the headings. -->

1. [Overview](#1-overview)
2. [Requirements](#2-requirements)
3. [Capacity estimation](#3-capacity-estimation)
4. [API design](#4-api-design)
5. [Data model](#5-data-model)
6. [High-level design](#6-high-level-design)
7. [Detailed design](#7-detailed-design)
8. [Scalability and performance](#8-scalability-and-performance)
9. [Reliability, security and operations](#9-reliability-security-and-operations)
10. [Trade-offs and alternatives](#10-trade-offs-and-alternatives)
11. [Delivery plan and risks](#11-delivery-plan-and-risks)
12. [Appendices](#12-appendices)

## 1. Overview

<!-- Guidance: One page that a busy reviewer can read alone: problem, proposed solution in a paragraph, and the decisions that need their attention. -->

### 1.1 Problem statement

[*Example:* Our 1 200 hotels each run their own property management system,
and the website, the call centre and two travel agencies each keep a copy of
availability that is refreshed every 15 minutes. On busy weekends we sell
rooms that no longer exist, front desks walk guests without a policy, and a
three-night booking sometimes holds Friday and Saturday but loses Sunday.
Rate changes take hours to reach all channels, so the same room shows three
prices.]

### 1.2 Proposed solution

[*Example:* A central reservation system owns future inventory as counters
per property, room type and night. A booking reserves one unit on each night
of the stay in a single transaction on the property's shard, or fails as a
whole. Rate plans are defined once (a base rate plus derived plans and
restrictions) and priced per night. Availability and prices for search come
from a precomputed calendar that is updated by events within seconds; the
booking path always re-checks the counters. Overbooking limits per room type
and night come from revenue management and are enforced as part of the
counter check. Channels receive availability, rates and restrictions by push
on every change, and reservations from channels enter through the same
booking path. The PMS receives reservations and sends back in-house changes
and out-of-order rooms.]

### 1.3 Decisions requested from reviewers

| ID | Question | Recommended option | Section |
|---|---|---|---|
| Q-01 | *Example:* Unit of inventory? | Counter per property, room type and night; no room numbers before check-in | 5, 7.1 |
| Q-02 | *Example:* How to book several nights safely? | One transaction with conditional decrements on all nights, rows locked in date order | 7.2 |
| Q-03 | *Example:* Overbooking limit? | Expected-profit optimum, capped so the chance of walking any guest stays at or below 10% | 7.5 |
| Q-04 | *Example:* Channel inventory? | One shared pool with last-room availability, not fixed allotments per channel | 7.6 |
| Q-05 | *Example:* Search freshness? | Precomputed calendar, lag p99 at or below 5 s; booking re-checks | 7.4 |
| [Q-nn] | [Question] | [Option] | [Section] |

### 1.4 45-minute interview path

Use this order when the document serves an interview or a short design
review. The written sections below are the long form of each step.

| Minutes | Cover | Output on the whiteboard |
|---|---|---|
| 0 to 5 | Scope: search, book, modify, cancel; one hotel group; room types, not rooms; below-the-line items (2.3) | The inventory unit "room type x night" |
| 5 to 10 | Estimates: rooms, bookings per day, searches per second, inventory rows (section 3) | Four numbers, "reads dominate by 500 to 1, writes are tiny" consequence |
| 10 to 15 | API: availability search, hold, confirm, modify, cancel with idempotency (section 4) | Five calls |
| 15 to 25 | High-level design: booking service on inventory store, calendar cache for search, channel and PMS sync (section 6) | One diagram with a read path and a write path |
| 25 to 40 | One deep dive: multi-night atomic booking (default, 7.2), or rate plans and restrictions (7.3), or overbooking (7.5) | Options, choice, reason |
| 40 to 45 | Failures (channel sync lag, PMS offline), the 10x row in section 10 | Two failure rows |

Start the default deep dive with the naive answer: "for each night, read
availability, then insert the booking". Show two guests booking the last
Saturday room, one for Friday and Saturday and one for Saturday and Sunday,
both reading 1 available. Then compare a lock per night, a conditional update
per night in one transaction, and a single property lock, and explain why
rows are locked in date order.

## 2. Requirements

<!-- Guidance: Split core requirements from below-the-line items so the design stays focused. Non-functional targets need numbers and a way to measure them. -->

### 2.1 Core functional requirements

| ID | Requirement | Priority |
|---|---|---|
| FR-01 | *Example:* Search availability and prices by property or destination, dates, guests | Must |
| FR-02 | *Example:* Book a stay of 1 to 30 nights in one room type and rate plan, all nights or none | Must |
| FR-03 | *Example:* Rate plans with derived prices, inclusions, cancellation and deposit policies | Must |
| FR-04 | *Example:* Restrictions per night: minimum and maximum length of stay, closed to arrival, closed to departure, stop-sell | Must |
| FR-05 | *Example:* Modify dates or room type; cancel within policy | Must |
| FR-06 | *Example:* Overbooking within limits set per room type and night | Must |
| FR-07 | *Example:* Group blocks with a cutoff date after which unused rooms return to general inventory | Should |
| FR-08 | *Example:* Push availability, rates and restrictions to channels; accept their reservations | Must |
| FR-09 | *Example:* Send reservations to the PMS; receive out-of-order rooms and in-house changes | Must |
| [FR-nn] | [Requirement] | [Must / Should / Could] |

### 2.2 Non-functional requirements and invariants

| ID | Quality | Target | Measured as |
|---|---|---|---|
| NFR-01 | No oversell beyond policy | *Example:* For every property, room type and night: sold plus held never exceeds rooms minus out of order plus the overbooking limit | Nightly audit |
| NFR-02 | Atomic stays | *Example:* No booking with some nights reserved and others not | Audit of booking nights against counters |
| NFR-03 | Search latency | *Example:* p95 at or below 300 ms for a property calendar | Synthetic and real-user monitoring |
| NFR-04 | Booking latency | *Example:* p99 at or below 500 ms for hold and confirm, excluding payment | Service timing |
| NFR-05 | Freshness | *Example:* Calendar and channels updated within 5 s p99 of a change | Event age |
| NFR-06 | Availability (booking path) | *Example:* 99.95% monthly | SLO |
| [NFR-nn] | [Quality] | [Target] | [Measurement] |

### 2.3 Below the line (out of scope)

Name these explicitly so reviewers and interviewers do not pull the design
toward them.

- [*Example:* revenue management forecasting and price optimisation (this system consumes their output)]
- [*Example:* room assignment, housekeeping and check-in (PMS)]
- [*Example:* loyalty points accrual (separate service fed by stay events)]
- [*Example:* seat-based events and onsales (see Ticket and Hotel Booking System Design (`system-design/design-ticket-booking`))]
- [Item deferred, with the release that will take it]

## 3. Capacity estimation

<!-- Guidance: Back-of-the-envelope numbers decide the shape of the design. Show inputs, formulas and results; round generously and say which way you rounded. -->

### 3.1 Assumptions

| Input | Symbol | Value | Source |
|---|---|---|---|
| Properties | $P$ | *Example:* 1 200 | Portfolio |
| Average rooms per property | $R$ | *Example:* 150 | Portfolio |
| Room types per property | $T$ | *Example:* 8 | Product catalogue |
| Rate plans per room type | $Q$ | *Example:* 12 | Revenue management |
| Booking horizon | $H$ | *Example:* 730 nights | Product rule |
| Average occupancy | $o$ | *Example:* 70% | Last 12 months |
| Average length of stay | $\ell$ | *Example:* 2.2 nights | Last 12 months |
| Share of bookings later cancelled | $c$ | *Example:* 25% | Last 12 months |
| Availability queries per gross booking (look-to-book) | $k$ | *Example:* 500 | Channel and web logs |

### 3.2 Bookings and searches

Rooms in the portfolio: $P \times R = 1\,200 \times 150 = 180\,000$.
Occupied room nights per day divided by length of stay gives arrivals, which
equal net bookings in a steady state:

$$
B_{net} = \frac{180\,000 \times 0.70}{2.2} \approx 57\,273 \ \text{bookings/day}, \qquad B_{gross} = \frac{B_{net}}{1 - c} = \frac{57\,273}{0.75} \approx 76\,364 \ \text{bookings/day}
$$

$$
S = B_{gross} \times k \approx 76\,364 \times 500 \approx 3.8 \times 10^{7} \ \text{queries/day} \approx 442 \ \text{queries/s average}, \ 1\,326 \ \text{at a peak factor of 3}
$$

Booking writes are about $76\,364 / 86\,400 \approx 0.9$ per second on average
and about 9 per second during a promotion at 10 times the average. Each
booking touches $\ell$ night rows, about 168 000 night-row updates a day.

### 3.3 Inventory and rate data

| Data | Calculation | Result |
|---|---|---|
| Inventory rows | $1\,200 \times 8 \times 730$ | 7 008 000 rows; at 200 bytes about 1.4 GB |
| Rate rows (price and restrictions per plan and night) | $1\,200 \times 8 \times 12 \times 730$ | 84 096 000 rows; at 100 bytes about 8.4 GB |
| Search calendar | One entry per rate row, already priced | about 8.4 GB, fits in a replicated in-memory cache |

### 3.4 Summary

| Quantity | Estimate | Design consequence |
|---|---|---|
| Searches | about 1 300 queries/s at peak | Separate read path from a precomputed calendar |
| Bookings | under 10/s even in a promotion | One relational cluster sharded by property; contention, not volume, is the issue |
| Inventory | 7 million rows, 1.4 GB | Small; partition by property for single-shard stays |
| Rates | 84 million rows, 8.4 GB | Store base and derivation rules; materialise for search only |

## 4. API design

<!-- Guidance: Define the contract first: resources, methods, request and response shapes, errors, idempotency, pagination, rate limits and versioning. Use RFC 9457 problem details for errors. -->

### 4.1 Endpoints

| Method and path | Purpose | Notes |
|---|---|---|
| `GET /v1/properties/{id}/availability?checkin=&checkout=&adults=` | Room types and rate plans with total price for the stay | From the calendar cache; may be seconds stale |
| `POST /v1/holds` | Reserve inventory for a stay for 15 minutes | Requires `Idempotency-Key`; re-checks counters |
| `POST /v1/reservations` | Confirm a hold with guest and guarantee details | Payment by token only |
| `PATCH /v1/reservations/{id}` | Change dates, room type or guests | New nights held before old nights released, in one transaction |
| `POST /v1/reservations/{id}/cancel` | Cancel; returns the penalty under the rate plan policy | Idempotent |
| `PUT /v1/inventory/{property}/{room_type}/{date}` | Revenue management and operations: overbooking limit, stop-sell, out-of-order count | Audited |

The `Idempotency-Key` header follows the pattern of
[draft-ietf-httpapi-idempotency-key-header-08](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/)
(22 October 2026, an Internet-Draft, not an RFC). Errors use problem details
(RFC 9457).

### 4.2 Request and response examples

Hold request:

```json
{"property_id": "PRP-0417", "room_type": "KING-DLX", "rate_plan": "BAR", "checkin": "2027-03-11", "checkout": "2027-03-14", "adults": 2}
```

Hold response with the per-night breakdown:

```json
{"hold_id": "HLD-88213", "expires_at": "2027-02-02T10:15:00Z", "currency": "USD", "nights": [{"date": "2027-03-11", "price": 180}, {"date": "2027-03-12", "price": 220}, {"date": "2027-03-13", "price": 240}], "total": 640, "cancellation_policy": "Free until 48 hours before arrival, property local time"}
```

A hold that fails on any night returns `409` with the failing night and the
reason (`sold_out`, `closed_to_arrival`, `min_los_not_met`), so the client
can suggest other dates.

## 5. Data model

<!-- Guidance: Choose storage by access pattern, not by habit. Show entities, keys, partitioning and the queries each store must answer. -->

### 5.1 Entities

Inventory and rates are keyed by property-local stay dates (a night is a date,
not a timestamp). A reservation has one row per night it consumes.

```mermaid
erDiagram
  PROPERTY {
    string property_id PK
    string time_zone
    string currency
  }
  ROOM_TYPE {
    string property_id PK
    string room_type PK
    int physical_rooms
  }
  INVENTORY_NIGHT {
    string property_id PK
    string room_type PK
    date stay_date PK
    int physical
    int out_of_order
    int sold
    int held
    int overbook_limit
    bool stop_sell
    int version
  }
  RATE_PLAN {
    string property_id PK
    string rate_plan PK
    string parent_plan
    string derivation
    string cancel_policy
  }
  RATE_NIGHT {
    string property_id PK
    string room_type PK
    string rate_plan PK
    date stay_date PK
    decimal price
    int min_los
    int max_los
    bool closed_to_arrival
    bool closed_to_departure
  }
  RESERVATION {
    string reservation_id PK
    string property_id FK
    string room_type
    string rate_plan
    date checkin
    date checkout
    string status
  }
  RESERVATION_NIGHT {
    string reservation_id PK
    date stay_date PK
    decimal price
  }
  PROPERTY ||--o{ ROOM_TYPE : has
  ROOM_TYPE ||--o{ INVENTORY_NIGHT : "counted per night"
  PROPERTY ||--o{ RATE_PLAN : offers
  RATE_PLAN ||--o{ RATE_NIGHT : "priced per night"
  RESERVATION ||--|{ RESERVATION_NIGHT : consumes
  INVENTORY_NIGHT ||--o{ RESERVATION_NIGHT : "counted in"
```

### 5.2 Storage choices

| Data | Store | Key | Why |
|---|---|---|---|
| Inventory nights, reservations, holds | *Example:* Relational database sharded by `property_id` | (`property_id`, `room_type`, `stay_date`) | Multi-row transactions on one shard |
| Rate plans and rate nights | Same shard as the property | (`property_id`, `room_type`, `rate_plan`, `stay_date`) | Pricing in the booking transaction reads local rows |
| Search calendar | *Example:* Replicated in-memory cache | (`property_id`, `room_type`, `rate_plan`) with a 730-night array | One key read per room type and plan |
| Change events | Partitioned log | `property_id` | Ordered per property for calendar, channels and PMS |
| Payment tokens | Payment provider vault | Token | Card data never stored in this system |

### 5.3 Access patterns

| Access pattern | Frequency | Key or index that serves it | Partition key rationale |
|---|---|---|---|
| Show prices for a stay at one property | about 1 300/s at peak | Calendar cache by (`property_id`, `room_type`, `rate_plan`) | Search never touches the booking database |
| Hold a multi-night stay | under 10/s | Inventory rows (`property_id`, `room_type`, `stay_date` range) | All nights of a stay are on the property's shard, so one local transaction |
| Price the stay inside the hold | Every hold | Rate night rows on the same shard | No cross-shard read in the transaction |
| Modify dates | Rare | Old and new night rows of the same property | Still one shard |
| Find a guest's reservations | Support, guest app | Secondary index on guest ID, or a separate lookup table | Cross-property queries go to a read replica |
| Expire holds | Every minute | Index on `hold.expires_at` per shard | Sweeper per shard |
| Nightly audit | Daily | Full scan per shard of tomorrow's nights | Bounded: 8 room types x 1 night per property |

## 6. High-level design

<!-- Guidance: One diagram that shows every component on the request paths and where state lives. Name components by responsibility. -->

```mermaid
flowchart LR
  guest([Guest web or app]) --> api[Reservation API]
  agent([Call centre]) --> api
  chan([Channels and agencies]) --> cgw[Channel gateway]
  cgw --> api
  api -->|search| cal[(Calendar cache)]
  api -->|hold, book, modify| book[Booking service]
  book --> inv[(Inventory and reservation DB, sharded by property)]
  book --> pay[Payment tokenisation]
  inv --> cdc[[Change events by property]]
  cdc --> calb[Calendar builder]
  calb --> cal
  cdc --> push[Channel push]
  push --> chan
  cdc --> pms[PMS connector]
  pms --> hotel([Property management system])
  hotel -->|out of order, in-house changes| pms
  pms --> book
  rm[Revenue management] -->|rates, restrictions, overbooking limits| book
```

| Component | Responsibility | Scales by |
|---|---|---|
| Reservation API | Authentication, routing of search to cache and writes to booking | Stateless instances |
| Booking service | Holds, confirms, modifications, cancellations; enforces counters and restrictions | Stateless, routes by property shard |
| Inventory and reservation DB | Source of truth for future inventory and reservations | Shards by property |
| Calendar builder | Applies change events to the search calendar | Partitions of the change log |
| Channel push | Sends availability, rate and restriction changes per channel format | Per channel worker pools |
| PMS connector | Two-way sync with each hotel's PMS | Per property queues |

## 7. Detailed design

<!-- Guidance: Go deep on the parts that are new, risky or contested. Show the sequence of calls and the failure behaviour of each step. -->

The deep dives in this section, each with options, choice and reason:

| Deep dive | Options considered | Choice | Reason | Section |
|---|---|---|---|---|
| Multi-night booking | Read then write per night; lock per night; conditional update per night in one transaction | Conditional updates in one transaction, rows in date order | All or nothing, no deadlock, no long locks | 7.2 |
| Pricing | Store every price; derive at read time; derive at write time and materialise | Base rates stored, derived plans materialised into the calendar | One source to change, fast search | 7.3 |
| Overbooking | None; fixed percentage; computed per room type and night | Computed limit, capped by walk risk | Revenue without uncontrolled walks | 7.5 |
| Channel inventory | Fixed allotments; shared pool | Shared pool with push on change | No stranded rooms; one counter | 7.6 |

### 7.1 Inventory model

Sell room types, not rooms. A room number is chosen at or near check-in by the
PMS, which lets the hotel absorb early departures, late check-outs and
maintenance without touching reservations. For each property, room type and
night:

$$
\text{available} = \text{physical} - \text{out\_of\_order} + \text{overbook\_limit} - \text{sold} - \text{held}
$$

A booking is allowed on a night only if `available` is at least 1 and the
night is not stop-sold. NFR-01 is this inequality checked for every row.

### 7.2 Multi-night atomic hold

A stay from 11 to 14 March consumes the nights of 11, 12 and 13 March. The
hold runs one transaction on the property's shard:

```sql
BEGIN;
-- lock the nights in date order first, so overlapping stays queue in the same order
SELECT stay_date FROM inventory_night
 WHERE property_id = 'PRP-0417' AND room_type = 'KING-DLX'
   AND stay_date >= DATE '2027-03-11' AND stay_date < DATE '2027-03-14'
 ORDER BY stay_date
 FOR UPDATE;
UPDATE inventory_night
   SET held = held + 1, version = version + 1
 WHERE property_id = 'PRP-0417' AND room_type = 'KING-DLX'
   AND stay_date >= DATE '2027-03-11' AND stay_date < DATE '2027-03-14'
   AND stop_sell = FALSE
   AND physical - out_of_order + overbook_limit - sold - held >= 1;
-- the service checks that the row count equals the number of nights (3);
-- if not, ROLLBACK and report the first failing night
INSERT INTO hold (hold_id, property_id, room_type, checkin, checkout, expires_at)
VALUES ('HLD-88213', 'PRP-0417', 'KING-DLX', DATE '2027-03-11', DATE '2027-03-14', now() + INTERVAL '15 minutes');
COMMIT;
```

The explicit `SELECT` with `ORDER BY stay_date FOR UPDATE` locks the nights in
date order (an `UPDATE` over a range does not promise any lock order), so two
overlapping stays always lock shared nights in the same order and cannot
deadlock; the locks are held only for the few milliseconds of the
transaction, never across payment. If the updated row count is less than the number of nights, at
least one night is full or closed, and the transaction rolls back.

```mermaid
sequenceDiagram
  participant G as Guest
  participant API as Reservation API
  participant BS as Booking service
  participant DB as Property shard
  participant EV as Change events
  G->>API: POST /v1/holds, 11 to 14 March, KING-DLX, BAR
  API->>BS: hold request with Idempotency-Key
  BS->>DB: check restrictions on rate nights
  BS->>DB: conditional update of 3 inventory nights and insert hold
  alt all 3 nights updated
    DB-->>BS: 3 rows, commit
    BS->>EV: inventory changed for 3 nights
    BS-->>API: hold HLD-88213, total 640
    API-->>G: 201 with expiry in 15 minutes
  else fewer than 3 rows
    DB-->>BS: 2 rows, rollback
    BS-->>API: 409, 2027-03-13 sold out
    API-->>G: suggest other dates
  end
```

Confirming a hold moves the count from `held` to `sold` on each night and
writes reservation nights with their prices. A sweeper releases expired holds
by the reverse update. A modification holds the new nights and releases the
old nights in the same transaction, so a guest who moves a stay never ends up
with neither booking.

### 7.3 Rate plans and restrictions

Revenue management sets a base rate (best available rate, BAR) per room type
and night. Other plans derive from it:

| Plan | Derivation | Policy | Example for Thu 180, Fri 220, Sat 240 |
|---|---|---|---|
| `BAR` | Base | Free cancellation until 48 h before arrival | 640 for 3 nights |
| `NRF` | BAR minus 15% | Non-refundable, charged at booking | 544 |
| `BB2` | BAR plus breakfast 18 per guest per night | As BAR | 640 + 3 x 2 x 18 = 748 |
| `MEMBER` | BAR minus 10%, members only | As BAR | 576 |

The stay price is the sum of the nightly prices, each from that night's rate
row, so a stay over a price change mixes prices correctly. Restrictions are
evaluated against the arrival and departure dates:

| Restriction | Checked on | Rule |
|---|---|---|
| Minimum length of stay (MinLOS) | Arrival night | Stay length at least the value set on the arrival night |
| Maximum length of stay (MaxLOS) | Arrival night | Stay length at most the value |
| Closed to arrival (CTA) | Arrival night | No stays may start on this night |
| Closed to departure (CTD) | Departure date | No stays may end on this date |
| Stop-sell | Every night | No new holds for the plan or room type |

Restrictions make availability depend on the stay, not only on the nights:
a Saturday with MinLOS 2 is available for Friday to Sunday but not for
Saturday alone. The calendar therefore stores restrictions next to prices,
and the search evaluates them per requested stay.

### 7.4 Search calendar

The calendar builder consumes change events per property and keeps, for each
(property, room type, rate plan), an array of 730 entries: available count,
price, MinLOS, MaxLOS, CTA, CTD. A search for one property reads at most
8 x 12 = 96 keys and evaluates the stay in memory. Lag is the event pipeline
lag (NFR-05). The cache can be wrong for a few seconds; the hold in 7.2 is
always the final check, and a failed hold returns the failing night so the
page can refresh.

### 7.5 Overbooking policy

Some guests do not arrive and some cancel too late to resell. Selling a few
rooms above capacity raises expected revenue but risks walking a guest
(relocating them to another hotel at the group's cost). With capacity $C$,
booking limit $B$ and independent show probability $q$, arrivals follow
$X \sim \text{Binomial}(B, q)$. With $c_u$ the margin of a room night lost
when a room stays empty and $c_o$ the cost of walking one guest, the expected
contribution is:

$$
E[\pi(B)] = c_u \, E[\min(X, C)] - c_o \, E[\max(X - C, 0)]
$$

*Example:* $C = 100$, $q = 0.92$, $c_u$ = 150 (room rate 180 minus 30
variable cost), $c_o$ = 450 (relocation, transport, compensation and
goodwill), all in USD:

| Booking limit $B$ | Expected contribution | Expected walks | Probability of at least one walk | Expected empty rooms |
|---|---|---|---|---|
| 100 | 13 800.0 | 0.000 | 0.000 | 8.00 |
| 104 | 14 328.2 | 0.040 | 0.029 | 4.36 |
| 105 | 14 425.2 | 0.108 | 0.071 | 3.51 |
| 106 | 14 482.4 | 0.243 | 0.140 | 2.72 |
| 107 | 14 483.9 | 0.470 | 0.239 | 2.03 |
| 108 | 14 418.0 | 0.810 | 0.359 | 1.45 |

The expected-contribution optimum is $B = 107$ (an overbooking limit of 7),
but it walks at least one guest on about 24% of such nights. With a policy cap
of 10% on the probability of any walk, the limit is $B = 105$ (limit 5), which
gives up 58.7 of expected contribution per night (14 483.9 minus 14 425.2) to
cut the walk probability from 0.239 to 0.071. Revenue management approves the
cap; the system stores `overbook_limit` per room type and night and enforces
it in 7.1. The binomial model assumes guests decide independently; groups
and event nights break that assumption, so set lower limits there.

When arrivals exceed rooms, the walk workflow chooses which reservation to
relocate (never loyalty top tiers or guests already walked on this trip, as
*Example* rules), books a comparable nearby hotel, pays transport and
compensation, and records the walk for the next forecast. Consumer rules on
relocation and disclosure differ by country; check the local rules and a
qualified professional. This is not legal advice.

### 7.6 Channels, group blocks and the PMS

- **Shared pool**: channels sell from the same counters (last-room
  availability), so a room is never stranded in one channel's allotment. The
  channel push sends availability, rates and restrictions on every change,
  batched per property for 1 s. A channel that sells on a stale count sends a
  reservation that fails the hold; the gateway answers with a rejection the
  channel understands, and the lag is tracked per channel (9.3).
- **Group blocks**: a block reserves rooms for a group as its own counter per
  night, taken out of general inventory. Guests book from the block until
  the cutoff date; at cutoff a job returns unused block rooms to general
  inventory in one transaction per property.
- **PMS**: the central system owns future reservations; the PMS owns in-house
  stays and room assignment. Out-of-order rooms from the PMS raise
  `out_of_order` counts and can create a negative availability that triggers
  the walk workflow before arrival day rather than at the desk.

### 7.7 How real systems do it

> **How real systems do it** (paraphrased, not copied). Rothstein's paper
> "Hotel overbooking as a Markovian sequential decision process" (Decision
> Sciences, 1974) describes a general hotel reservation system and models
> booking acceptance as a sequence of decisions; applied to sample data from
> a real hotel, cancellations were frequent enough that the model prescribed
> a substantial amount of overbooking. Talluri and van Ryzin's book "The
> Theory and Practice of Revenue Management" (Springer, 2004) treats
> overbooking and network revenue management, where a product such as a
> multi-night stay uses capacity on several resources at once, which is why
> 7.2 books every night of a stay together. Cross, Higbie and Cross,
> "Revenue Management's Renaissance" (Cornell Hospitality Quarterly, 2009),
> argue that hotel revenue management should move beyond room inventory
> control to total revenue, price elasticity and measures that compare
> performance with competitors. Read the originals in 12.3; this box keeps
> only the general ideas.

## 8. Scalability and performance

<!-- Guidance: Show how the design meets the numbers in section 3 and where it will break first. -->

| Concern | Approach | Limit before redesign |
|---|---|---|
| Search | Calendar cache, replicated per region | *Example:* 20 000 queries/s with current nodes |
| Holds | Sharded by property, short transactions | Contention on one property's popular nights during a promotion |
| Channel push | Batched per property, per-channel workers | Channel rate limits |
| Rate updates | Derived plans recomputed per changed base night only | Bulk yearly loads run off-peak |
| Audit | Per shard, tomorrow's nights only, plus weekly full horizon | [Limit] |

Latency budget for a hold (target p99 500 ms):

| Step | Budget |
|---|---|
| API, authentication, idempotency lookup | 40 ms |
| Restriction check on rate nights | 30 ms |
| Inventory transaction (up to 30 rows) | 80 ms |
| Event publish | 20 ms |
| Headroom | 330 ms |

## 9. Reliability, security and operations

<!-- Guidance: Name the failure modes with their blast radius and recovery time, the threats, and the signals operators watch. Each mitigation should be testable. -->

### 9.1 Failure modes

| Component | Failure | Detection | Blast radius | Mitigation | RTO |
|---|---|---|---|---|---|
| Property shard primary | Failover | Error rate per shard | Properties on that shard cannot book for seconds | Synchronous replica; idempotent retries by clients | *Example:* 30 s |
| Calendar builder | Falls behind | Event age above 5 s | Search shows stale availability; holds still correct | Scale consumers; banner on search if lag above 60 s | *Example:* 5 min |
| Channel push | Channel endpoint down | Push error rate per channel | Channel sells on stale data | Retry with backoff; stop-sell that channel if lag passes 15 min | *Example:* 15 min |
| PMS connector | Hotel PMS offline | Heartbeat per property | One hotel's out-of-order and in-house updates | Queue changes; front desk works from PMS; reconcile on reconnect | *Example:* hours, local |
| Hold sweeper | Stops | Count of expired holds not released | Inventory locked by abandoned holds | Alert; holds also checked for expiry at read time | *Example:* 5 min |
| Rate load | Wrong prices loaded | Price change monitor (large jumps) | Plans derived from the wrong base | Versioned rate loads; rollback to previous version | *Example:* 10 min |
| [Component] | [Failure] | [Signal] | [Scope] | [Mitigation] | [Time] |

### 9.2 Security and abuse

| Threat | Control |
|---|---|
| Card data exposure | Tokenisation at the payment provider; this system stores tokens only, keeping it out of cardholder data scope where possible |
| Inventory hoarding by bots (holds never confirmed) | Holds per session and per IP limited; short TTL; holds by channels count against their quota |
| Rate scraping | Rate limits per client; signed partner API keys |
| Guest data access | Role-based access; reservation lookup needs reservation ID plus surname or authenticated account |
| Tampering with overbooking limits | Changes audited and limited to revenue management roles |

### 9.3 Observability

| Signal | Metric or log | Alert threshold |
|---|---|---|
| Oversell beyond policy | Audit violations (NFR-01) | Any |
| Hold failure reasons | Sold out, restriction, error, by property | Error share above 1% |
| Calendar lag | Event age at cache write | p99 above 5 s |
| Channel lag and rejections | Per channel | Lag above 60 s or rejection rate doubling |
| Walks | Walked guests per property and night | Above forecast from 7.5 |

### 9.4 Cost estimate

| Item | Unit cost | Quantity | Monthly cost |
|---|---|---|---|
| Database shards with replicas | [$/node-month] | *Example:* 8 shards x 3 | [$] |
| Calendar cache nodes | [$/node-month] | *Example:* 6 | [$] |
| Event log and consumers | [$/month] | [Size] | [$] |
| Channel connectivity fees | [$/booking or $/month] | [Volume] | [$] |
| **Total** | | | **[$]** |

## 10. Trade-offs and alternatives

<!-- Guidance: For each major decision list the options, what each costs and why you chose one. Reviewers will propose the alternatives anyway; answer them here. -->

| Decision | Options | Chosen | Why | What we give up |
|---|---|---|---|---|
| Unit of sale | Specific room; room type per night | Room type per night | Flexibility for operations, simple counters | Guests cannot pick a room number at booking |
| Concurrency | Pessimistic locks across payment; conditional updates plus hold TTL | Conditional updates and holds | Short transactions, no lock across network calls | Abandoned holds block inventory for 15 min |
| Partitioning | By date; by property | By property | Stays and modifications stay on one shard | Cross-property guest queries need an index or replica |
| Channel inventory | Allotments; shared pool | Shared pool | No stranded rooms | Stale channels may sell into a full night and get rejected |
| Prices | All materialised; all derived at read; hybrid | Base stored, derived materialised for search | One place to change rates, fast reads | Calendar rebuild after base changes |
| What changes at 10x / 0.1x | 10x (12 000 properties, about 760 000 bookings/day): about 13 000 searches/s at peak, so the calendar cache becomes a regional tier and channels need dedicated push clusters; booking writes (about 90/s in promotions) still fit a sharded cluster. 0.1x (120 properties): one database with a replica and a read-through cache; overbooking limits may be set per property instead of per night | Review at each doubling of properties | Search and channel push grow first | [Note] |
| [Decision] | [Options] | [Chosen] | [Why] | [Cost] |

## 11. Delivery plan and risks

<!-- Guidance: How the design reaches production safely, and what could stop it. -->

### 11.1 Milestones

| Milestone | Scope | Exit criterion | Target date |
|---|---|---|---|
| M1 | Inventory model, holds and bookings for 10 pilot properties, web channel | Nightly audit clean for 30 days | YYYY-MM-DD |
| M2 | Rate plans, restrictions, calendar search | Search p95 at or below 300 ms | YYYY-MM-DD |
| M3 | PMS connector and channel push | Channel lag p99 at or below 60 s | YYYY-MM-DD |
| M4 | Overbooking limits and walk workflow | Walk rate within forecast for one season | YYYY-MM-DD |

### 11.2 Migration and rollback

[*Example:* Migrate property by property. For each property, freeze
availability changes for 10 minutes, load future reservations from the old
system and the PMS, reconcile counts night by night, then switch channels to
the new push. Rollback restores the old system as the source and replays
reservations made in the new one.]

### 11.3 Risks

| ID | Risk | Likelihood | Impact | Mitigation | Owner |
|---|---|---|---|---|---|
| R-01 | *Example:* Migration counts differ from the PMS | High | High | Night-by-night reconciliation before cutover | [@name] |
| R-02 | *Example:* Overbooking model ignores groups and events | Medium | High | Lower limits on flagged nights; review after each season | [@name] |
| R-03 | *Example:* A channel cannot handle push frequency | Medium | Medium | Per-channel batching and rate limits | [@name] |
| [R-nn] | [Risk] | [L / M / H] | [L / M / H] | [Mitigation] | [@name] |

## 12. Appendices

<!-- Guidance: Supporting detail, glossary and open questions. -->

### 12.1 Open questions

| ID | Question | Owner | Due |
|---|---|---|---|
| OQ-01 | *Example:* Do we price by length of stay (different nightly price for 7-night stays)? | [@name] | YYYY-MM-DD |
| OQ-02 | *Example:* Should room upgrades at booking consume the higher room type's inventory? | [@name] | YYYY-MM-DD |
| [OQ-nn] | [Question] | [@name] | YYYY-MM-DD |

### 12.2 Glossary

| Term | Meaning |
|---|---|
| BAR | Best available rate; the base public rate from which others derive |
| ARI | Availability, rates and inventory sent to channels |
| MinLOS, MaxLOS | Minimum and maximum length of stay, set on the arrival night |
| CTA, CTD | Closed to arrival, closed to departure |
| Walk | Relocating a guest with a reservation to another hotel because none is free |
| Out of order | Rooms unavailable for sale because of maintenance |
| Group block | Rooms reserved for a group until a cutoff date |
| Look-to-book | Availability queries per booking |
| [Term] | [Meaning] |

### 12.3 References

| Reference | Use |
|---|---|
| Rothstein, M. "Hotel overbooking as a Markovian sequential decision process", Decision Sciences 5(3), 389 to 404, 1974, [doi:10.1111/j.1540-5915.1974.tb00624.x](https://doi.org/10.1111/j.1540-5915.1974.tb00624.x) | 7.5, 7.7 |
| Talluri, K. T. and van Ryzin, G. J. "The Theory and Practice of Revenue Management", Springer, 2004, [doi:10.1007/b139000](https://doi.org/10.1007/b139000) | 7.5, 7.7 |
| Cross, R. G., Higbie, J. A. and Cross, Z. N. "Revenue Management's Renaissance", Cornell Hospitality Quarterly 50(1), 56 to 81, 2009, [doi:10.1177/1938965508328716](https://doi.org/10.1177/1938965508328716) | 7.7 |
| [draft-ietf-httpapi-idempotency-key-header-08](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/) (Internet-Draft, October 2026) and RFC 9457 Problem Details | 4.1 |
| Ticket and Hotel Booking System Design (`system-design/design-ticket-booking`) | Seat-based inventory, waiting rooms and the simpler allotment model this design extends |
| E-commerce Checkout and Inventory System Design (`system-design/design-ecommerce-checkout-inventory`) | Counted inventory reservations |
| Payment Ledger System Design (`system-design/design-payment-ledger`) | Deposits, penalties and refunds |
| Back-of-Envelope Estimation and Capacity Model (`system-design/back-of-envelope-estimation`) | Method for section 3 |

## Standard and licence note

Based on common industry practice for hotel central reservation systems and
on the revenue management literature cited in 12.3, paraphrased only. No
standard governs this design. The overbooking table is computed from the
stated example inputs with an exact binomial model; all prices, volumes and
limits are illustrative examples, not benchmarks. Notes on relocation rules
are not legal advice; check the current official text and a qualified
professional. Original text written for this collection. Template licence:
MIT.
