Mobile System Design Interview

Course Content

Mobile System Design Interview

11 sections · 23 lessons

Hotel Reservation: requirements, the offline split and the API contract


Prompt: "Design a hotel reservation app."

The distinctive thing about this problem is not search and it is not the booking form. It is that the user moves through five or six screens over several minutes, the operating system can destroy the app between any two of them, and at the end of the sequence real money moves.

Every other case study in this course can lose a little state and recover. This one cannot. This lesson sets up the problem and the API that makes it safe; the next designs the persisted flow, the payment step and the follow-ups.

Scoping a booking clientBooking scopeSearch, book, manage?Payment in app?Offline for what?Guest checkout?Cancellation in scope?
Whether payment is in scope decides if this is a search problem or a write-safety problem.

The questions worth asking

"Search, book, or manage existing bookings — or all three?" Take all three, and note that they have completely different offline requirements. Search needs live availability and cannot work offline. Viewing a booking you already hold absolutely must.

"Is payment in-app, or handed to a provider?" This changes the design substantially. A hosted provider flow — a web view or a platform payment sheet — means card details never touch your code, which removes a large compliance burden. Ask, then assume a provider software development kit, because that is what most teams do and it is the more interesting client problem.

"What are the offline requirements?" Answer it yourself with the split above: searching offline is meaningless because availability changes minute to minute, but a traveller standing at a hotel reception desk with no signal needs their confirmation number. That contrast is worth stating explicitly — it demonstrates that "works offline" is a per-feature decision, not an app-level one.

"Guest checkout, or account required?" Guest checkout means the booking is not attached to an account, so the confirmation on the device may be the user's only copy. That raises the stakes on local persistence considerably.

"Multiple rooms, multiple travellers, loyalty programmes?" Ask, and scope them out unless the interviewer wants them. They add form complexity without adding design insight.

The scope to state back

In scope: search with filters, a hotel and room detail screen, a booking flow of several screens ending in payment, a confirmation, and a bookings list that works offline.

Out of scope: the availability and pricing engine, the payment processor's internals, and inventory management. In System Design Interview, Design a Hotel Reservation System designs the reservation server and Design a Payment System designs the payment system.

Requirements and constraints

Functional requirements

  1. Search hotels by location and dates, with filters and pagination.
  2. View a hotel, its rooms, and current prices.
  3. Complete a booking across several screens: room, guest details, payment, review, confirm.
  4. Pay, and receive a confirmation with a reference number.
  5. View existing and past bookings, including with no network.
  6. Cancel a booking within its policy.

The non-functional targets

TargetValueWhy
Search results to screenUnder ~1.5 s on 4GThe user is comparing options and will retry elsewhere
Flow state persistenceEvery step transition, under ~50 msIt must be on disk before the screen changes
Price quote validityTypically 10–15 minutes, set by the serverLong enough to finish, short enough to be honest
Booking outcome certaintyAlways resolvableThe user must never be left wondering whether they paid
Existing booking availability100% offlineThis is when it is needed

The four constraints

Network. Search fails gracefully; booking must not. The dangerous moment is the payment request, where a lost response is ambiguous rather than merely inconvenient.

The offline story, feature by featureMay fail gracefully• Search returns cached results• Hotel detail from last fetch• Map tiles degrade to a listMust not fail ambiguously• Booking with an idempotency key• Payment confirmation retrievable• Existing booking readable offline
The dangerous moment is a lost payment response, which is ambiguous rather than merely inconvenient.

Battery and storage. Modest here. Search results are text and a few images; bookings are a handful of rows. This is one of the few designs where these are not the interesting constraints, and saying so is better than manufacturing concern.

Memory. Also modest, with one exception: hotel photo galleries. Apply Images and media — downsample to display size, one full-resolution image at a time in a pager.

Process death. The constraint that defines the design. A user tapping between the booking flow and their email app to find a passport number is a completely ordinary sequence, and the app may not survive it.

The constraint in one sentence

The offline story, per feature

Being explicit per feature is the strong answer here, and it is worth writing on the board:

FeatureOffline behaviour
SearchUnavailable. Show the last results with their capture time and a clear "prices and availability may have changed — reconnect to search" state, and disable booking from them
Hotel detailCached text and images render; the price panel is replaced by a reconnect prompt
Booking flowCannot start. If already in progress, state is preserved and the flow waits at the current step with a retry
PaymentBlocked. Never queued — see Payment safety on the client
My bookingsFully available: reference number, dates, address, cancellation policy, and a map snapshot stored as an image

That last row is the one that matters to a real traveller, and it is the one candidates forget.

The API contract

With the constraint named, the API follows from it: four endpoints, and one structural decision that carries the whole design.

Four endpoints, with the quote in the middleSearchavailabilityQuote holdsthe priceBook with a keyRetrieve bookingThe quote is a separate call so the price the user agreed to is the price that is charged.
Splitting quote from book gives the server a token to bind the charge to, and the client something to retry against.

Search

Text
GET /v1/hotels/search?lat=&lng=&radius_km=&check_in=&check_out=                     &guests=&filters=&limit=20&cursor=<opaque>→ { items: [ { hotel_id, name, rating, thumbnail: {url,w,h},               from_price: { amount, currency }, distance_m } ],    next_cursor, search_id, server_time }

Cursor pagination as always. search_id is worth asking for: it lets the server keep the result set stable while the user pages, so a price change does not reshuffle results underneath them. Prices here are indicative — the field is named from_price deliberately, because a search result is not an offer.

Quote, and why it is a separate call

Text
POST /v1/quotes  { hotel_id, room_type_id, check_in, check_out, guests }→ { quote_id, total: { amount, currency },    breakdown: [ { label, amount } ],    expires_at: "2026-08-30T09:29:00Z",    cancellation_policy: { … } }

The naive design skips this: the user picks a room and the app books it at the price shown in search. It fails in a way that is worse than a bug, because it is a legal and trust problem — the search price was indicative, computed minutes earlier, possibly before taxes and fees, and possibly for a room that has since sold. Charging a different amount than the one displayed is not acceptable, and neither is holding a price forever.

So the quote step exists to convert an indicative price into a binding, expiring offer. The client shows exactly the quoted total, displays the expiry, and must handle it lapsing — a visible countdown in the last couple of minutes, and a re-quote if it expires, with the new price shown as a change rather than substituted silently.

Booking, and the key that makes it safe

Text
POST /v1/bookings  Idempotency-Key: 6f2a1e88-…          ← generated on the device, once  { quote_id, guest: {…}, payment_token: "tok_…" }→ 201 { booking_id, reference: "HX7K2M", status: "confirmed", … }→ 409 { error: "quote_expired", new_quote: {…} }→ 200 { … }  // same key seen before: the original booking, not a new one

The idempotency key is generated once when the user taps Confirm, stored with the flow state, and reused on every retry of that attempt. It is what makes the ambiguous failure in Payment safety on the client recoverable, and it must be asked for explicitly — a server that ignores the header gives you nothing.

Retrieving a booking

Text
GET /v1/bookings?idempotency_key=6f2a1e88-…    → the booking, or 404GET /v1/bookings                               → the user's bookings

The first form is the reconciliation endpoint. It is the single most valuable thing to request from the backend team in this design, and Design a Payment System in System Design Interview covers how the server side of it works.

Offline: search and quote fail with a network error and a retry. POST /v1/bookings is never attempted offline — the flow blocks at the review step with a clear message. GET /v1/bookings responses are stored, so the bookings list is served from the database always.