Study Notes
Note

Design a Hotel Reservation App

Search, hold, and pay for a room from a phone — where the interesting problems are a countdown clock you can't trust and a payment you can't fumble.

13 min read
Mobile System Design · Chapter 07

Search, hold, and pay for a room from a phone — where the interesting problems are a countdown clock you can't trust and a payment you can't afford to fumble.

🧭
How to read this

Scope and API (§1–2) frame the booking flow; the architecture (§3) is the skeleton. The three deep dives are where the real difficulty lives: holding a room against a clock the device can lie about (§4), autocomplete that survives a dead connection (§5), and payments that keep card data and liability off your servers (§6). Anything marked [+] is context added beyond the source chapter; the rest is a rewrite. Diagrams are original.

01

Scope an aggregator, not one chain#

The first scoping question decides the whole shape: this is an aggregator like Booking.com — browsing and booking across many providers — not a single hotel chain. The patterns here carry over to flights, rentals, and event ticketing. Two requirements from the interview do most of the design work: bookings must survive offline, and a room must be held on a timer once payment starts.

Functional requirements#

  • Search hotels by location or destination, with autocomplete suggestions while typing.
  • View hotel details and available room types; complete a reservation in-app.
  • Pay via multiple providers (credit card, PayPal, and similar).
  • Access confirmed reservations offline.
  • Enforce a time limit to finish a reservation once it's started.

Non-functional requirements#

  • High availability — reliable even at holiday-peak traffic.
  • Low latency — booking confirmations in a few seconds.
  • Data consistency — never let two users book the same room for overlapping dates.
🚫
Deliberately excluded

Real-time availability updates, authentication (assume logged in), cancellations, check-in, and push notifications. Scale target: ~5 million monthly active users worldwide, designed to grow beyond that.

📱
Three screens

Hotel List (search + featured/results), Hotel Details (rooms, amenities, a "Book" button), and the reservation/payment flow that follows. The user path is linear: search → pick → view → book.

02

A REST API built around the booking flow#

No real-time updates or push are needed, so interactions are client-initiated: HTTP + REST over JSON. The endpoints below cover the two flows that matter most — finding a hotel, and booking one. In an interview, define these precisely and gesture at the rest.

Hotel endpoints#

  • GET/v1/hotels/search?page&limit&latitude&longitude&check_in_date&check_out_date&guests → HotelSearchResponse (a paginated list of HotelPreview).
  • GET/v1/hotels/{id} → HotelDetailsResponse — images, amenities, curated rooms, price.
🔌
JSON in, platform models out

Responses are shown as Kotlin/Swift models, but on the wire they're JSON, deserialized in the network layer. Android uses kotlinx.serialization or Moshi; iOS uses the built-in Codable protocol. [+] Preview vs. Details are split so the list stays lightweight (a lowResImageUrl, a price) and the detail call carries the heavy payload only when a hotel is opened.

Reservation endpoints#

  • POST/v1/reservations → 201 with ReservationResponse. Secures a room; body is a ReservationRequest. Errors: HotelNotAvailable, RoomNotAvailable, TooManyGuests, InvalidDates.
  • GET/v1/reservations/{id} — status and details.
  • PUT/v1/reservations/{id} — attach personal and payment details. Errors: PaymentError, PriceMismatch, and similar.
🔑
The idempotency key that prevents double-booking

ReservationRequest carries a client-generated requestId — an idempotency key. If a flaky network makes the client retry the same POST, the backend recognizes the repeated requestId and returns the original reservation instead of booking a second room. [+] This is the client-side half of the "data consistency" requirement; the server enforces the rest by locking inventory. The response adds a backend-generated reservationId and a status (pending / failed / succeeded).

03

The layered client#

Same layered approach as the other case studies: a UI layer of screens with state holders, over a data layer of repositories that each own a data type and talk to local and remote sources. All network traffic funnels through one Network Dispatcher.

CLIENT UI LAYER Hotel List Hotel Detail Reservation MakePayment State holders Navigator DATA LAYER Hotels repository Reservations repository Local · Remote sources Local · Remote sources Network Dispatcher SERVER-SIDE (external) API Gateway auth · access control Backend CDN PaymentProviders HTTPS
Figure 1 — Screens and state holders over repositories, one dispatcher out to the gateway, and the external services behind it.

External pieces: the Backend (HTTP), a CDN for static images, the API Gateway (auth and access control), and external payment providers. Two deep-dive features add components later — a Reservation Timing Controller (data layer) and Reservation Timer (UI), and payment state holders wired to a PSP SDK.

04

The reservation hold and its untrustworthy clock#

When a user starts booking, the backend holds the room for a fixed window (typically 15 minutes) so no one else can take it mid-checkout. The behavior has four parts:

  • Fixed window — the countdown starts when the server acknowledges the POST /v1/reservations, and the backend returns an authoritative expiration timestamp.
  • Automatic release — miss the window and the room returns to inventory, instantly bookable again.
  • Visible countdown — the app shows remaining time, making the urgency clear.
  • Expiration cleanup — unpaid reservations are auto-canceled so inventory stays accurate.
⏱️
Why you can't just run a local timer

Three mobile realities break a naive countdown: users can change the device clock; network latency skews when the hold seems to start; and the app gets suspended when backgrounded or the phone locks. Trusting wall-clock time means the client and server disagree about when the room expires.

🎯
Decision: backend timestamp + device uptime

The backend is the source of truth — it sends an absolute expiration UTC timestamp. The client renders the countdown using device uptime (monotonic time since boot), not wall-clock time. Uptime keeps ticking correctly through app suspension and can't be manipulated by changing the clock. iOS: ProcessInfo.processInfo.systemUptime + Timer. Android: SystemClock.elapsedRealtime() + CountDownTimer. [+] Uptime is monotonic: it only moves forward at a steady rate, which is exactly what a countdown needs.

Backend source of truth expiration UTC timestamp CLIENT device uptime ✓ wall clock ✕ remaining = expiry − elapsed uptime
Figure 2 — Trust the server's absolute deadline; measure the passage of time with monotonic uptime, never the wall clock.

Architecturally this adds a Reservation Timing Controller in the data layer (the timing logic) and a Reservation Timer in the UI layer (the visible countdown) — a clean Separation of Concerns that keeps each testable.

🔍
In the wild

Expedia's API supports a two-step booking explicitly: a client can put a room "on hold" with no charge, then confirm within a time window. For the backend side of this problem, the "Hotel Reservation System" chapter of System Design Interview, Vol. 2 goes deep.

05

Autocomplete that works offline#

Autocomplete suggests cities, destinations, and hotels as the user types. It looks simple and hides four mobile constraints: suggestions must feel instant (a server round-trip per keystroke is too slow and burns data), must work offline, must stay light on battery/CPU/memory, and must forgive the typos that thumb-typing produces.

Where does the suggestion data come from?#

StrategyUpsideDownside
Local static datasetOffline; no network delay; stable.Bloats app size; needs updates; bad for evolving data.
Remote per keystrokeAlways current; no local storage.Latency hurts UX; heavy data use; needs error handling.
Third-party (e.g. Places)Rich curated data; less to maintain.Costs; privacy concerns; vendor lock-in.
Pre-fetch popular ✓Fast for common queries; efficient network use.May store unused data; needs refresh.
On-demand loading ✓Conserves resources; tailored; lean storage.Sparse for brand-new searches.
🎯
Decision: hybrid — pre-fetch popular + on-demand

Preload common terms (top cities, hotels) at startup for instant responses without a bulky embedded dataset, then fetch region-specific data on demand as the user's patterns emerge. It assumes people mostly search popular regions first, then personalizes over time.

The data flow#

  1. Initial load — on launch, check the cached data's version; if the server has newer data, download and update the local cache (via a ChangeList pattern or timestamp sync).
  2. Local query — as the user types, query the local dataset, with a debounce of ~200–300 ms so a burst of keystrokes triggers one search, not ten. [+]
  3. Network refinement — when the user selects a suggestion or searches, fetch richer data for that region in the background and cache it with a short TTL.
Backend version check 1 Local FTS store User types debounce 200–300ms 2 suggestions (no network) Backend region data · short TTL 3
Figure 3 — Version-checked cache in, debounced local lookups while typing, background refinement after a pick.
🗂️
Storage: SQLite + Full-Text Search

Names and locations call for fast text retrieval, so the local store uses Full-Text Search: prefix matching for suggestions, tokenization, relevance ranking, and fuzzy matching for typos. It costs index storage and complexity, but the UX justifies it. Android gets FTS via Jetpack Room; iOS works with SQLite directly or through a wrapper like GRDB (Core Data has no native FTS).

🔍
In the wild

Traveloka layers multi-language support and geographic awareness to cut typos and boost relevance. Tiket.com trained a learning-to-rank model that lifted autocomplete ranking quality by roughly 4–7.5%, cutting the keystrokes to a pick from three to two.

06

Taking payment without holding the risk#

Two payment methods: direct credit card, and third-party wallets (PayPal, Apple Pay, Google Pay). The guiding rule for cards is to touch as little sensitive data as possible.

🔒
Card-handling non-negotiables

Don't store card data on the device — keep it in volatile memory only during an active transaction. Transmit over HTTPS/TLS. Use tokenization: the processor swaps the real card number for a token, so you can offer "saved cards" by storing only a reference id and the last four digits — never the PAN. [+] A token is useless if stolen because it only means something to the processor that issued it.

Who integrates the payment: you, or a PSP?#

Once card data reaches the server via PUT /v1/reservations/{id}, there are two paths: connect your backend directly to payment processors (Worldpay, Fiserv), which means carrying the full, expensive burden of PCI DSS compliance; or partner with a Payment Service Provider (Stripe, PayPal) that shoulders that compliance and ships SDKs.

🎯
Decision: use trusted PSPs

Direct processor integration demands heavy, ongoing investment to meet PCI DSS. For most apps a PSP is safer and faster — it already solves compliance and security, and reinforces user trust.

PSP on the backend, or on the client?#

AspectBackend integrationClient-side integration ✓
PerformanceExtra latency via backend.Lower latency; direct client↔PSP.
SecuritySensitive data server-side only.Managed client-side; more to secure.
MaintenanceCentralized; easy to change.Per-platform; client updates for PSP changes.
ExperienceUniform but limited.Native, richer — biometric auth, full PSP features.
🎯
Decision: integrate the PSP on the client

All payment transactions run from the client UI through the PSP's SDK. It adds client-side complexity but delivers a native experience with device biometric authentication and the PSP's full feature set — and, crucially, keeps card data flowing client→PSP rather than through your servers.

Payment data flow#

  1. Collect — the client calls the PSP SDK to gather payment details, keeping sensitive data off your servers.
  2. Submit — post-payment, the client sends PUT /v1/reservations/{id} with the PSP's tokenized transaction identifiers.
  3. Verify — the backend confirms the payment with the PSP.
  4. Confirm — on success the backend locks in the reservation and returns confirmation.
Make Payment screen 3RD-PARTY SDK PSP Payment UI collect details 1 Reservations repository Backend token 2 PSP SERVER verify 3 confirmation 4
Figure 4 — The client collects and tokenizes with the PSP; only tokens reach your backend, which verifies before locking the booking.
💳
Authorize, then capture

PSPs split a charge in two: authorization reserves the funds, capture actually moves them. [+] If anything fails before capture, the money was only reserved, not taken — so the backend should always verify the reservation before capturing. Handle SDK realities too: robust error handling for declines/timeouts, retries with exponential backoff, and clear real-time status to the user.

🔍
In the wild

Expedia moved to a cloud-based microservices payment platform for speed and flexibility. Stripe documents payment tokenization and layers fraud prevention with 3D Secure — multi-factor authentication that also shifts fraud liability to the card issuer.

07

Recap#

Aggregator scopeBook across providers; offline reservations and a booking time-limit drive the design.
REST + JSONClient-initiated flow; Preview vs Details split keeps the list light.
Idempotency keyA client requestId de-dupes retried POSTs and prevents double-booking.
Layered clientScreens + state holders over repositories, one Network Dispatcher out.
Backend clock + uptimeServer owns the deadline; the client counts down with monotonic uptime.
Hybrid autocompletePre-fetch popular + on-demand, debounced local FTS over SQLite, offline-capable.
Client-side PSPNative, biometric-friendly payments; card data flows client→PSP, not your server.
Tokenize + verifyOnly tokens hit your backend; verify before the PSP captures funds.
💡
The single idea underneath it all

A booking app is a trust problem wearing a search-and-pay costume. The hard parts — the hold timer, the payment path — are all about deciding who is the source of truth and keeping risk off the device: the server owns the deadline and the inventory lock; the PSP owns the card data and the compliance. The client's job is a fast, honest interface to systems it deliberately doesn't fully trust.

08

Further reading#

Every link below was fetched and confirmed live on 24 August 2026. One standard has advanced since the chapter and is flagged.

Reservation holds

  1. Expedia Rapid — hold and resumeA production two-step booking API: place a room on hold with no charge, then resume/confirm within a window — exactly the §4 pattern.
  2. System Design Interview, Vol. 2 (Xu & Lam)The "Hotel Reservation System" chapter for the backend side of this problem.

Search & autocomplete

  1. Full-text searchThe retrieval concepts behind the local store: indexing, stemming, precision vs recall, fuzzy matching.
  2. Room — defining data (with FTS)How Android's Jetpack Room exposes SQLite Full-Text Search with minimal setup.
  3. GRDB.swiftThe iOS SQLite wrapper that gives Core-Data-free FTS support.
  4. Traveloka — high-quality autocompleteMulti-language, geo-aware autocomplete tuning from a real OTA.
  5. Tiket.com — the algorithm behind searchA learning-to-rank (XGBoost) autocomplete model that cut keystrokes-to-pick.

Payments & security

  1. Tokenization (data security)Why swapping card numbers for tokens shrinks what you must protect.
  2. Payment processorThe "direct integration" path and what it entails.
  3. Payment service providerThe PSP path chosen in §6, and how PSPs offload compliance.
  4. PCI DSSThe compliance burden that makes direct processor integration expensive. Now at v4.0.1 (June 2024), the only active version. updated
  5. Expedia — cloud payments case studyExpedia's move to a microservices payment platform.
  6. Stripe — payment tokenization 101A clear primer on tokenization in a real PSP's stack.
  7. Stripe — 3D SecureMFA for card-not-present payments and the fraud-liability shift to the issuer.

Rewritten in original wording from ByteByteGo's Mobile System Design Interview — Chapter 7, "Hotel reservation app." All diagrams are original SVG, not reproductions of the source figures.

[+] marks context added beyond the source. Links were verified live on 24 August 2026.