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.
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.
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.
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.
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.
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.
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 ofHotelPreview). - GET
/v1/hotels/{id}→HotelDetailsResponse— images, amenities, curated rooms, price.
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→201withReservationResponse. Secures a room; body is aReservationRequest. 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.
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).
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.
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.
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.
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.
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.
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.
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.
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?#
| Strategy | Upside | Downside |
|---|---|---|
| Local static dataset | Offline; no network delay; stable. | Bloats app size; needs updates; bad for evolving data. |
| Remote per keystroke | Always 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. |
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#
- 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).
- 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. [+]
- 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.
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).
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.
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.
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.
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?#
| Aspect | Backend integration | Client-side integration ✓ |
|---|---|---|
| Performance | Extra latency via backend. | Lower latency; direct client↔PSP. |
| Security | Sensitive data server-side only. | Managed client-side; more to secure. |
| Maintenance | Centralized; easy to change. | Per-platform; client updates for PSP changes. |
| Experience | Uniform but limited. | Native, richer — biometric auth, full PSP features. |
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#
- Collect — the client calls the PSP SDK to gather payment details, keeping sensitive data off your servers.
- Submit — post-payment, the client sends
PUT /v1/reservations/{id}with the PSP's tokenized transaction identifiers. - Verify — the backend confirms the payment with the PSP.
- Confirm — on success the backend locks in the reservation and returns confirmation.
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.
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.
Recap#
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.
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
- 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.
- System Design Interview, Vol. 2 (Xu & Lam)The "Hotel Reservation System" chapter for the backend side of this problem.
Search & autocomplete
- Full-text searchThe retrieval concepts behind the local store: indexing, stemming, precision vs recall, fuzzy matching.
- Room — defining data (with FTS)How Android's Jetpack Room exposes SQLite Full-Text Search with minimal setup.
- GRDB.swiftThe iOS SQLite wrapper that gives Core-Data-free FTS support.
- Traveloka — high-quality autocompleteMulti-language, geo-aware autocomplete tuning from a real OTA.
- Tiket.com — the algorithm behind searchA learning-to-rank (XGBoost) autocomplete model that cut keystrokes-to-pick.
Payments & security
- Tokenization (data security)Why swapping card numbers for tokens shrinks what you must protect.
- Payment processorThe "direct integration" path and what it entails.
- Payment service providerThe PSP path chosen in §6, and how PSPs offload compliance.
- PCI DSSThe compliance burden that makes direct processor integration expensive. Now at v4.0.1 (June 2024), the only active version. updated
- Expedia — cloud payments case studyExpedia's move to a microservices payment platform.
- Stripe — payment tokenization 101A clear primer on tokenization in a real PSP's stack.
- Stripe — 3D SecureMFA for card-not-present payments and the fraud-liability shift to the issuer.