本章將設計一套飯店訂房系統 (hotel reservation system),讓使用者能夠搜尋、比較並預訂飯店。像 Booking.com 與 Hotels.com 這類平台,透過在行動應用中整合強大的搜尋能力與即時預訂功能,已經徹底改變了旅行規劃的方式。這類系統必須管理即時房間庫存、避免重複預訂 (double booking),並提供準確的價格資訊。

Figure 1: The Booking.com mobile app, as shown in the Google Play Store

本章所探討的架構模式同樣適用於其他預訂類系統,例如短租平台 (property rentals)、航班訂位 (flight reservations) 或票務系統 (event ticketing)。

Step 1: 理解問題並確立設計範圍#

在進入設計細節之前,必須先釐清系統的需求與邊界。想像以下與面試官的對話:

  • Candidate:我想先釐清範圍。我們是要專注於單一飯店集團,還是要做像 Booking.com 那種聚合多家供應商的平台 (aggregator)?
  • Interviewer:我們採用聚合模式,讓使用者可以瀏覽並預訂來自多個來源的飯店。
  • Candidate:了解。核心功能方面,使用者應該能依地點或目的地搜尋飯店、查看房型與價格,並在 App 內完成預訂。要不要考慮在使用者搜尋過程中,即時更新房間庫存?
  • Interviewer:這些功能作為基礎很不錯,不過為了簡化,先不考慮即時更新。搜尋時需要支援自動完成 (autocomplete) 建議。另一個關鍵需求是:使用者一旦開始輸入付款資訊,就要設定完成預訂的時間限制。
  • Candidate:好的。那讓我再多問幾個功能面的問題。使用者需要取消訂房或在 App 內辦理入住嗎?是否要規劃身份驗證流程,或是行程提醒類的推播通知?
  • Interviewer:讓我們聚焦。假設使用者已登入,不需處理驗證流程。取消與入住也先排除。付款要支援信用卡與第三方服務 (例如 PayPal)。推播通知也不需要。
  • Candidate:了解。為了引導技術選型,可以告訴我預期規模嗎?月活躍使用者 (MAU) 有多少?
  • Interviewer:目前全球約 500 萬 MAU,但系統必須能支援未來的成長。

以下整理本章要建構的系統。

需求#

根據討論,本章設計的飯店訂房 App 具備以下功能需求 (functional requirements):

  • 使用者可依地點或目的地搜尋飯店。
  • App 在搜尋時提供自動完成 (autocomplete) 建議。
  • 使用者可檢視飯店詳細資訊與可訂房型。
  • 使用者可在 App 內完成訂房。
  • 支援多種付款方式 (例如信用卡、PayPal)。
  • 使用者在離線狀態下仍可查閱已確認的訂房。
  • 系統對完成預訂流程強制套用時間限制。

非功能需求 (non-functional requirements) 方面,系統需達成:

  • 高可用性 (High availability):即使在假期等尖峰流量期間,App 仍須穩定可用。
  • 低延遲 (Low latency):訂房確認應在幾秒內完成,以維持良好體驗。
  • 資料一致性 (Data consistency):避免同一房間在重疊日期被重複預訂,確保訂房資料正確無誤。

UI 草圖#

Figure 2 呈現飯店訂房 App 的核心畫面:

  • Hotel List 畫面 (左):作為入口,讓使用者搜尋並顯示精選飯店。搜尋後 (中) 顯示搜尋結果。
  • Hotel Details 畫面 (右):顯示所選飯店的完整資訊 (含可訂房型),使用者可點擊 “Book” 按鈕開始預訂。

Figure 2: Basic sketch of the hotel reservation app

Step 2: API 設計#

釐清需求與 UI 後,接著設計行動端與後端之間的溝通 API。

網路協定 (Network protocol)#

由於系統不需即時更新或推播通知,多數互動由 client 主動發起。因此採用 HTTP + REST APIs,以 JSON 作為資料格式。

API endpoint 與資料模型#

以下聚焦在最關鍵的 endpoint 與資料模型,其他部分可在面試官提問時再展開。

Pro tip! 面試時應聚焦在最關鍵需求的 endpoint 與資料模型,通常對應 App 的主要使用者流程。其他功能可以點到為止,展現你對全貌的掌握,但有意識地取捨重點。

Hotel endpoints#

飯店是 App 的核心,使用者需要直覺的方式來尋找與探索飯店。主要 endpoint 如下:

  • GET /v1/hotels/search?page={page}&limit={limit}&latitude={x}&longitude={y}&…
    • 依地點與旅遊日期搜尋飯店。
    • Response:200 OK 回傳 HotelSearchResponse
    • 其他查詢參數:
      • check_in_date (Date):抵達日期
      • check_out_date (Date):離開日期
      • guests (Integer):入住人數
Kotlin / Swift 資料模型
// Kotlin
data class HotelSearchResponse(
    val hotels: List<HotelPreview>,
    val paging: PaginationMetadata,
)

data class HotelPreview(
    val id: String,
    val name: String,
    val location: String,
    val price: BigDecimal,
    val lowResImageUrl: String,
    // ...
)
// Swift
struct HotelSearchResponse {
    let hotels: [HotelPreview]
    let paging: PaginationMetadata
}

struct HotelPreview {
    let id: String
    let name: String
    let location: String
    let price: Decimal
    let lowResImageUrl: String
    // ...
}

Platform implementation details

本書將 API response 呈現為 Kotlin 與 Swift 資料模型,而非原始 JSON。實務上這些 JSON payload 會在網路層轉換成各平台的資料結構。

  • Android:常見方案為 kotlinx.serialization 或 Moshi。
  • iOS:Swift 內建的 Codable protocol 可優雅地將 JSON 欄位對應到 struct 屬性。
  • GET /v1/hotels/{id}?…
    • 取得特定飯店的詳細資訊。
    • Response:200 OK 回傳 HotelDetailsResponse
HotelDetails 資料模型
// Kotlin
data class HotelDetailsResponse(val hotel: HotelDetails)

data class HotelDetails(
    val id: String,
    val name: String,
    val location: String,
    val imagesUrls: List<String>,
    val amenities: List<Amenity>,
    val curatedRooms: List<RoomSummary>,
    val price: BigDecimal,
    // ...
)

data class Amenity(
    val name: String,
    val iconUrl: String,
    val category: String,
)

data class RoomSummary(
    val id: String,
    val name: String,
    val description: String?,
    val imageUrl: String,
    val maxOccupancy: Int,
    // ...
)
// Swift
struct HotelDetailsResponse { let hotel: HotelDetails }

struct HotelDetails {
    let id: String
    let name: String
    let location: String
    let imagesUrls: [String]
    let amenities: [Amenity]
    let curatedRooms: [RoomSummary]
    let price: Decimal
    // ...
}

struct Amenity {
    let name: String
    let iconUrl: String
    let category: String
}

struct RoomSummary {
    let id: String
    let name: String
    let description: String?
    let imageUrl: String
    let maxOccupancy: Int
    // ...
}

Reservation endpoints#

訂房流程的 endpoint 設計如下:

  • POST /v1/reservations
    • 透過鎖定房間來啟動訂房。
    • Body:ReservationRequest
    • Response:201 Created 回傳 ReservationResponse
    • 錯誤:400 Bad Request,例如 HotelNotAvailableRoomNotAvailableTooManyGuestsInvalidDates
Reservation 資料模型
// Kotlin
data class ReservationRequest(
    val requestId: String,
    val hotelId: String,
    val roomIds: List<String>,
    val checkInDate: String,
    val checkOutDate: String,
    val guests: Int,
)

data class ReservationResponse(
    val reservationId: String,
    val hotelId: String,
    val status: ReservationStatus,
    // ...
)
// Swift
struct ReservationRequest {
    let requestId: String
    let hotelId: String
    let roomIds: [String]
    let checkInDate: String
    let checkOutDate: String
    let guests: Int
}

struct ReservationResponse {
    let reservationId: String
    let hotelId: String
    let status: ReservationStatus
    // ...
}

ReservationRequest 中的 requestId 作為 idempotency key (冪等鍵),可在網路問題導致同一請求被重送時,避免重複訂房。

後端處理請求後回傳的 ReservationResponse 除了原始請求欄位之外,還包含:

  • reservationId:由後端產生,作為追蹤與更新訂房的權威識別碼。
  • status:表示訂房狀態為 pending、failed 或 succeeded。

Remember:在 POST 請求中加入 idempotency key 可讓後端去除重複請求。在本系統中,ReservationRequest.requestId 是防止房間被重複預訂的關鍵。

  • GET /v1/reservations/{id}:取得某筆訂房的詳細資訊與狀態。
  • PUT /v1/reservations/{id}:更新既有訂房 (例如個人資料、付款資料等)。
    • 錯誤:PaymentErrorPaymentProviderNotAvailablePriceMismatchUserInfoIncompleteCurrencyNotSupported

Step 3: 高階 client 架構#

有了 API 之後,接著設計 client 端架構。Figure 3 為本 App 的架構圖。

Figure 3: High-level architecture diagram of our hotel reservation app

外部 server-side 元件#

系統仰賴數個外部服務:

  • Backend:透過 HTTP 處理 client 互動的主要通訊點。
  • CDN:有效率地傳遞靜態內容,提升效能。
  • API Gateway:處理身份驗證與系統存取控制。
  • External Payment Providers:處理並驗證付款交易。

Client 架構#

Client 架構沿用先前章節的分層設計,UI 層與資料層協作,提供順暢的訂房體驗。

UI 層包含三個主要畫面,各自擁有獨立的 state holder:

  • Hotel List
  • Hotel Detail
  • Reservation

此外新增 Make Payment Screen 元件,負責處理付款邏輯,支援直接刷卡與第三方付款服務。

Data 層則透過專責的 repositories 管理兩種核心資料類型:

  • HotelsReservations repository,分別搭配 local 與 remote data source 以達成高效資料處理。
  • 所有網路通訊集中由 Network Dispatcher 元件調度,統一管理後端互動與存取控制。

Step 4: 設計深入探討#

接下來深入以下三個關鍵議題:

  • 實作訂房鎖定 (reservation holds)
  • 以自動完成強化搜尋體驗
  • 處理付款

實作 reservation holds#

本 App 必須妥善管理使用者在完成訂房前可保留房間的時間。這種機制在業界稱為 reservation holds,它在使用者便利性與房間庫存可用性之間取得平衡。

需求#

Reservation hold 功能圍繞以下關鍵行為:

  • 固定預訂視窗 (Fixed booking window):使用者開始訂房時,後端會暫時鎖定該房間一段固定時間 (通常為 15 分鐘)。計時從 POST /v1/reservations 被後端確認時開始,後端作為 source of truth,提供權威的過期時間戳記。
  • 自動釋放 (Automatic release):若使用者未在時限內完成訂房,系統會自動釋放鎖定,房間立即可供其他使用者預訂。
  • 顯示倒數計時 (Visible countdown):App 在整個訂房流程中持續顯示剩餘時間,提醒使用者緊迫性。
  • 過期清理 (Expiration cleanup):過期時會自動取消未付款的訂房,避免過期鎖定堆積,確保庫存反映實際狀態。

管理 reservation hold 計時器#

讓 client 與後端的計時保持同步是一大挑戰。行動裝置讓計時管理變複雜,因為:

  • 裝置時間不可信:使用者可手動調整系統時鐘,擾亂本地計時器。
  • 網路延遲:client 與 server 之間的延遲會偏移鎖定的起迄時間。
  • App suspend:當 App 進入背景或裝置鎖定時,計時器仍須維持正確。

單靠裝置時鐘行不通,需要以後端作為 source of truth。因此後端會把訂房過期的 UTC timestamp 傳給 client

為了在 client 端精準呈現剩餘時間,我們採用裝置運行時間 (device uptime)。Uptime:

  • 在 App 被 suspend 時仍然精準。
  • 不受使用者修改系統時鐘影響,與 wall clock time 相比更可靠。

Platform implementation details

iOS 與 Android 都提供處理時間追蹤與倒數的原生 API:

  • iOS:使用 ProcessInfo.processInfo.systemUptime 取得系統 uptime,並以 Timer 類別建立計時器。
  • Android:使用 SystemClock.elapsedRealtime() 取得系統 uptime,並以 CountDownTimer 實作倒數。

此作法兼顧伺服器精準度與行動端中斷情境的彈性。

Decisions made!

  • 後端 timestamp 作為追蹤 reservation hold 的唯一 source of truth。
  • Client 結合後端過期 timestamp 與本地 device uptime 來呈現倒數計時。

架構調整#

  • Reservation Timing Controller (data 層):處理核心的 reservation timing 邏輯。
  • Reservation Timer (UI 層):向使用者顯示倒數計時。

將兩個職責分別放在不同層級,符合 Separation of Concerns 原則 (Figure 4),使系統更易維護與測試。

Figure 4: Timer-related updates to the high-level architecture diagram

Industry insights

Expedia 的 API 明確支援兩階段訂房:client 可先將房間置於 “hold” 狀態 (不扣款),再於指定時限內確認預訂。若對後端系統設計有興趣,可參閱 System Design Interview Volume 2 的 “Hotel Reservation System” 章節。

以自動完成強化搜尋體驗#

Autocomplete 是行動系統設計面試中常見的主題,在飯店訂房 App 更是不可或缺。它讓使用者在輸入時快速找到城市、目的地或飯店,但在行動端實作會面臨不少技術挑戰。

本節專注在「如何有效載入 autocomplete 資料」這個面向,不深入演算法、個人化或擴展性等細節。

理解搜尋體驗#

目前 App 採用基本搜尋:使用者輸入查詢、按下 “Search”,client 透過 GET /v1/hotels/search 取得結果。這方式可行,但缺乏現代使用者期待的動態互動。

Autocomplete 能在輸入時顯示建議,但要做好需考量多項效能議題:

  • 網路延遲與頻寬:Autocomplete 回應必須幾近即時。每次按鍵都查後端會導致回應慢且耗流量。
  • 離線期望:使用者期待沒有網路時也能使用。Autocomplete 應能在離線時提供有意義的建議。
  • 資源限制:行動 App 必須控制電池、CPU 與記憶體消耗。
  • 輸入特性:行動鍵盤輸入速度較慢且錯字多,需要快速且容錯的 autocomplete。

實作 autocomplete 搜尋#

Table 1 列出取得 autocomplete 資料的多種策略與其優缺點:

選項說明優點缺點
本地靜態資料集將 autocomplete 資料全部內嵌於 App可離線,無網路延遲,效能穩定App 變大、需要更新、不適合快速變動的資料
即時遠端 API 擷取每次按鍵都向後端擷取建議資料即時、不占本地空間、靈活延遲破壞體驗、流量大、需強健的錯誤處理
第三方服務透過外部 API (例如 Google Places)資料豐富、減少維護成本、隱私、受制於外部
預先擷取熱門資料預先載入常見搜尋詞 (熱門城市、飯店)加速常見查詢、減少網路開銷可能存下用不到的資料、需定期更新
按需載入資料使用者完成搜尋後再向後端擷取節省資源、貼近使用者、儲存量少初始建議有限、新搜尋體驗稀疏

Table 1: Trade-offs for providing autocomplete data

本章採用混合策略:預先擷取熱門資料 + 按需載入使用者個人搜尋,原因如下:

  • 保持 App 輕量,避免內嵌龐大資料集影響安裝大小。
  • 降低網路負載:在啟動時預先取得常見詞彙,減少即時請求。
  • 個人化體驗:按需擷取會依使用者搜尋習慣逐步調整,提供貼近的建議。

資料流#

Autocomplete 採三步驟流程:

  1. 初始資料載入:App 啟動時檢查本地快取版本,若 server 有更新版本則下載並更新。可採用 ChangeList pattern 或時間戳同步機制。
  2. 本地查詢處理:使用者輸入時查詢本地資料集。為避免短時間大量查詢,加入 debounce 延遲 (通常為 200–300 ms),在使用者暫停輸入後再查詢。
  3. 網路端補強:當使用者選取建議或完成搜尋時,背景擷取該區域的額外資料,以較短 TTL 快取,維持新鮮度又不過度耗網路。

儲存方案#

Autocomplete 需要為文字檢索最佳化的儲存系統。針對名稱與地點查詢,Full-Text Search (FTS) 是理想選擇,因為它提供:

  • 快速 prefix matching,非常適合提供自動完成建議。
  • 智慧的 text tokenization,提升搜尋準確度。
  • 內建的 relevance ranking
  • 能處理行動端常見錯字的 fuzzy search

雖然 FTS 需要額外的 index 儲存空間,但換來的體驗值得投資。SQLite 搭配 FTS 擴充 是跨平台的選擇。

Platform implementation details

Android 與 iOS 都可透過 SQLite 實作 FTS:

  • Android:Jetpack Room 持久化函式庫內建 FTS 支援,處理設定與查詢最佳化。
  • iOS:Core Data 不原生支援 FTS。可直接使用 SQLite 實作,或透過 GRDB 等 wrapper library 簡化整合。

潛在挑戰與考量#

  • 資料同步:使用 ChangeList 等機制時需小心版本控制,只擷取增量可節省頻寬。
  • Throttle 調校:200–300 ms 對多數人合適,但可依輸入速度或裝置能力動態調整。
  • 資料新鮮度:對於變動快速的資料 (例如房間庫存),縮短過期時間以確保準確。
  • 請求最佳化:按需載入可能在尖峰時段加重 server 負擔;可透過 Wi-Fi 背景同步或預測式預取緩解。

Industry insights

若想了解其他公司的做法:

  • Traveloka.com 支援多語言與地理位置感知,以提升搜尋相關度並減少錯字。
  • Tiket.com 開發了機器學習模型,讓 autocomplete 排序結果的品質提升 4% 到 7.5%。

處理付款#

安全地處理付款是訂房 App 不可或缺的一環。設計付款整合時需要同時兼顧使用者體驗、資料安全、法規遵循,以及與外部服務的穩定互動。

本 App 支援兩種主要付款方式:

  • 直接信用卡付款:使用者手動輸入卡片資訊 (姓名、卡號、到期日、CVV)。
  • 第三方付款服務:例如 PayPal、Apple Pay、Google Pay,提供快速且安全的交易。

信用卡付款#

處理信用卡付款時,安全必須擺在第一位:

  • 避免在裝置上儲存敏感資料:卡片資訊應僅在交易期間短暫存在於 volatile memory。
  • 使用 HTTPS + TLS 加密傳輸。
  • Tokenization (代碼化):將敏感資料替換為 token,真正的資料由 payment processor 安全存放。Tokenization 讓使用者可安全地重複使用先前的付款方式。

運用 tokenization,client 可將已儲存付款方式的本地儲存最小化,只保留:

  • 指向該卡片的唯一 id
  • 卡號末四碼,方便使用者快速辨識。
整合信用卡付款#

當信用卡資訊透過 PUT /v1/reservations/{id} 送到 server 後,有兩條可行路線:

  1. 直接整合 payment processor:後端直接串接 Worldpay、Fiserv、Global Payments 等服務,掌控力高,但需達成 PCI DSS 合規,成本高昂。
  2. 透過 Payment Service Provider (PSP):與 Stripe、PayPal 等 PSP 合作,可將合規與安全責任外包;其 SDK 也能簡化後端與行動端整合。

Important security note

直接處理信用卡資料並串接 payment processor 需要龐大的安全投資才能符合 PCI DSS 規範。對多數應用而言,使用成熟的 PSP 更安全也更務實。

因此本章決定與信任的 PSP 合作,簡化開發也強化使用者信任。

整合第三方 PSP#

現代 PSP (例如 Stripe、PayPal) 不只處理基本卡片付款,也涵蓋數位錢包與其他替代方案 (例如 Klarna)。

整合 PSP 到行動 App 時,一個關鍵架構決策是:付款流程應由 client 直接發起,還是由後端轉送? Table 2 比較兩種作法:

面向後端整合Client 端整合
實作後端統一調度 PSP 互動Client 直接與 PSP 互動,管理付款 UI
效能多一段後端處理延遲延遲較低,付款較快
安全敏感資料只在 server 端處理,安全性較佳敏感資料在 client 端處理,需更強的安全措施
維護整合集中,變更較容易需維護多個平台端整合,PSP 變更需更新 client
體驗各平台體驗統一但受限於後端更原生、可使用生物辨識等進階功能
開發後端複雜度高,client 實作較單純Client 需要整合 PSP SDK,複雜度較高

Table 2: Trade-offs for integrating with PSPs

評估後,在 client 端直接整合 PSP 能提供更原生的付款體驗,並支援生物辨識等進階付款方式。雖然 client 管理變得較複雜,但符合高品質現代行動體驗的期待。

Decisions made!

本飯店訂房系統所有付款交易皆由 client UI 透過第三方 PSP 處理。

架構更新#

在架構圖 (Figure 5) 中加入付款相關元件:

  • Make Payment State Holder:負責所有付款邏輯,串接第三方付款 SDK 與 Reservations repository。
  • PSP Payment UI components:來自外部 SDK,處理付款 UI,安全地收集付款資訊並提供便利選項。

Figure 5: Payment client-related updates to the UI layer

付款資料流#

Figure 6 說明使用者訂房時的端到端資料流。client 先送出 POST /v1/reservations 並取得成功的 reservationId 後,接下來:

  1. 收集付款 (step 1):client 呼叫 PSP SDK 收集付款資訊,讓敏感資料不經過我方伺服器。
  2. 提交付款確認 (step 2):client 以 PUT /v1/reservations/{id} 送出 PSP 提供的 tokenized 交易識別碼。
  3. 後端驗證 (step 3、4):後端向 PSP 確認付款,成功後鎖定訂房並回傳確認給 client。

Figure 6: Payment data flow

PSP 的兩階段交易

PSP 通常以兩步驟處理交易:

  • Authorization:保留客戶帳戶中的款項。
  • Capture:交易確認後才真正扣款。

這讓付款流程更可靠:若交易未完成,款項仍在客戶帳戶中。使用 PSP SDK 時,後端應先驗證訂房才 capture 付款,以確保一致性。

在 client 整合 PSP SDK#

整合 PSP SDK 時需妥善處理行動端常見的付款問題:

  • 錯誤處理 (Error handling):處理交易被拒、逾時、網路問題等常見例外,並清楚告知使用者後續動作。
  • 交易重試 (Transaction retries):使用 exponential backoff 策略,避免短時間對 PSP 產生重複請求。
  • 使用者回饋 (User feedback):持續提供即時、清楚的付款進度資訊,確保透明度。

Industry insights

若想了解其他公司的做法:

  • Expedia 將付款平台遷移到雲端微服務,以取得彈性與速度。
  • Stripe 解釋 payment tokenisation 與透過 3D Secure 進行的詐欺防護。

Step 5: Wrap-up#

本章從零設計了一套完整的飯店訂房系統,核心在於順暢的搜尋、瀏覽與訂房體驗。我們特別著重於:

  • 管理訂房逾時 (reservation holds)。
  • 最佳化搜尋效能 (autocomplete)。
  • 確保付款流程順暢且安全。

若面試還有時間或希望探索進階議題,可考慮以下需求:

  • 即時庫存更新:實作房間庫存的 live update,讓使用者永遠看到最新狀態。
  • 智慧通知:設計行前提醒、入住提醒、訂房確認等推播通知系統。
  • 訂房管理:讓使用者修改或取消訂房,並處理取消政策與退款。
  • 促銷系統:設計彈性的折扣、會員忠誠度計畫與季節性優惠框架。