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

本章所探討的架構模式同樣適用於其他預訂類系統,例如短租平台 (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” 按鈕開始預訂。
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 內建的
Codableprotocol 可優雅地將 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,例如HotelNotAvailable、RoomNotAvailable、TooManyGuests、InvalidDates。
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}:更新既有訂房 (例如個人資料、付款資料等)。
- 錯誤:
PaymentError、PaymentProviderNotAvailable、PriceMismatch、UserInfoIncomplete、CurrencyNotSupported。
- 錯誤:
Step 3: 高階 client 架構#
有了 API 之後,接著設計 client 端架構。Figure 3 為本 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 管理兩種核心資料類型:
- Hotels 與 Reservations 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),使系統更易維護與測試。
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 採三步驟流程:
- 初始資料載入:App 啟動時檢查本地快取版本,若 server 有更新版本則下載並更新。可採用 ChangeList pattern 或時間戳同步機制。
- 本地查詢處理:使用者輸入時查詢本地資料集。為避免短時間大量查詢,加入 debounce 延遲 (通常為 200–300 ms),在使用者暫停輸入後再查詢。
- 網路端補強:當使用者選取建議或完成搜尋時,背景擷取該區域的額外資料,以較短 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 後,有兩條可行路線:
- 直接整合 payment processor:後端直接串接 Worldpay、Fiserv、Global Payments 等服務,掌控力高,但需達成 PCI DSS 合規,成本高昂。
- 透過 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 6 說明使用者訂房時的端到端資料流。client 先送出 POST /v1/reservations 並取得成功的 reservationId 後,接下來:
- 收集付款 (step 1):client 呼叫 PSP SDK 收集付款資訊,讓敏感資料不經過我方伺服器。
- 提交付款確認 (step 2):client 以
PUT /v1/reservations/{id}送出 PSP 提供的 tokenized 交易識別碼。 - 後端驗證 (step 3、4):後端向 PSP 確認付款,成功後鎖定訂房並回傳確認給 client。
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,讓使用者永遠看到最新狀態。
- 智慧通知:設計行前提醒、入住提醒、訂房確認等推播通知系統。
- 訂房管理:讓使用者修改或取消訂房,並處理取消政策與退款。
- 促銷系統:設計彈性的折扣、會員忠誠度計畫與季節性優惠框架。