股票交易 App(Stock trading app)面臨一些獨特的技術挑戰。它們不只是另一種普通的應用程式,而是節奏快速的交易平台,在這裡時間就是一切;能否即時將準確的資訊傳遞給使用者,往往直接影響他們的財務決策。圖 1 展示了業界幾款熱門的股票交易 App。
核心挑戰在於處理高頻率的資料串流(high-frequency data streams)。每一次價格變動與市場波動都必須以最低延遲(latency)送達使用者,因為即便是幾秒鐘的延遲,都可能意味著錯失交易機會。
而這不只是「快」而已。這類 App 處理的是真金白銀與敏感的金融資訊,因此安全性與可靠性必須同樣穩固。我們需要打造強健的安全機制、可靠的 API,並確保遵循金融法規(financial regulations),讓系統平穩且安全地運作。
Step 1:理解問題並定義設計範圍#
為了釐清我們要打造什麼,讓我們想像一段與面試官的簡短對話。這段來回有助於我們定義 App 的主要功能並設定清楚的邊界。
- 候選人:在開始之前,我想先了解交易功能的範圍。我們這個股票交易 App 聚焦在什麼?只有股票,還是也要考慮選擇權(options)和期貨(futures)等其他金融商品?
- 面試官:先保持單純,專注在股票就好。
- 候選人:好的。我想主要功能會包括下單與取消訂單、查看投資組合(portfolio)、顯示即時價格、以及歷史資料。這樣涵蓋到了嗎?
- 面試官:差不多了!再加上一個簡單的歷史股價走勢圖(chart),讓使用者更能掌握行情。
- 候選人:要加上價格警示(price alerts)的推播通知(push notifications)嗎?另外規模大致是多大?是跨多個市場的大型使用者基數嗎?
- 面試官:通知一定要。規模上大約是 500k 日活躍使用者(DAU),橫跨不同市場。App 需要處理數千支股票的行情並即時更新價格。
- 候選人:考量到安全與合規要求,我建議盡量不要在裝置上儲存敏感資料,並實作強化的安全措施。認證(authentication)要納入範圍嗎?
- 面試官:好主意。我們假設使用者已經完成認證。技術上就當作是一個全新(greenfield)專案來設計。
功能需求(Requirements)#
根據上述對話,我們的股票交易 App 需要提供:
- 使用者可以查看自己的投資組合與股票的即時價格。
- 使用者可以透過下單與取消訂單來買賣股票。
- 使用者可以在圖表中查看歷史股價資料。
- 系統能夠發送價格警示通知。
**非功能性需求(non-functional requirements)**方面,我們需要建立一個能夠確保以下特性的系統:
- 即時資料(Real-time data):系統必須以最低延遲推送市場更新,確保使用者在做交易決策時看到的是最新資訊。
- 效能(Performance):即使在處理串流資料與複雜視覺化時,UI 也必須維持流暢;交易需要快速且可靠地執行。
- 安全(Security):金融資料必須全程受到保護,具備強健的身份驗證機制,並完全符合金融監管標準。
**超出範圍(out of scope)**的項目:
- 選擇權與期貨交易。
- 使用者註冊。
UI 草圖#
讓我們想像一下 App 的樣貌。圖 2 呈現主要畫面:
- Home 畫面(左):一眼呈現使用者的財務快照、投資組合與近期訂單。
- Stock Detail 畫面(中):深入呈現特定股票的歷史資料與交易選項。
- Place Order modal(右):提供精簡的買入與賣出流程。
Step 2:API 設計#
接下來我們要確立 Client 與 Backend 之間的通訊方式,聚焦在通訊協定、互動方式與核心資料模型。
選擇合適的通訊協定#
股票交易 App 需要兩種主要互動模式:
- 標準的 Client-Server 交換:例如送出訂單或取得投資組合詳情。
- 即時更新:例如即時股價串流。
為了高效滿足這些需求,我們採用混合協定策略(hybrid protocol strategy):
- HTTP + REST APIs:適合由 Client 發起的動作,如下單、查詢帳戶、取得歷史資料。REST 提供結構化的 request-response 模型,在行動開發中被廣泛採用。
- WebSockets:適合交付即時資料,例如股價變動與訂單狀態通知。透過維持持久性雙向連線(persistent bidirectional connection),Server 可以即時將更新推播到 Client,確保時效性資訊低延遲送達。
這種組合在標準的請求-回應模式與連續資料串流之間取得良好平衡。許多交易平台都採用這套技術堆疊,證明其在規模上的有效性。
Endpoints 與資料模型#
Stocks#
以下 endpoints 提供股票資訊與歷史走勢的存取:
- GET /v1/stocks/
{symbol}:取得指定股票的詳細資料,包括當前價格、當日高低點、漲跌百分比。 - GET /v1/stocks/
{symbol}/history?interval={interval}&period={period}:提供歷史價格資料,可透過參數自訂時間間隔(如1m代表一分鐘、1d代表一天)與時間範圍(如1wk代表一週、3mo代表三個月)。
對應的資料模型:
| Kotlin | Swift |
|---|---|
data class Stockid: Stringsymbol: Stringname: StringcurrentPrice: BigDecimalchangePercent: BigDecimalhighPrice: BigDecimal… | struct Stockid: Stringsymbol: Stringname: StringcurrentPrice: DecimalchangePercent: DecimalhighPrice: Decimal… |
在股票交易平台這類金融 App 中,務必使用 Kotlin 的
BigDecimal或 Swift 的Decimal,而不是Float或Double。浮點數型別(
Float/Double)會引入微妙的捨入誤差,這在金融情境中是無法接受的。BigDecimal提供精確的十進位表示與精確的算術運算。雖然這些運算比原生型別稍慢,但在維護資料完整性(data integrity)上,這份保證是必要的。
Portfolio 與 Orders#
以下 endpoints 負責投資組合管理與交易:
- GET /v1/portfolio:取得使用者當前的投資組合,包含持有的股票與數量。
- GET /v1/orders?status=
{status}&page={page}&limit={limit}:回傳分頁的訂單列表,可依狀態過濾(如 open、executed、canceled)。 - POST /v1/orders:建立一筆新的買入或賣出訂單。
- DELETE /v1/orders/
{orderId}:取消一筆既有訂單。
訂單歷史採用 offset pagination(偏移量分頁)。這種方式在此場景運作良好,因為:
- 訂單在建立後很少變動。
- 使用者通常關心近期交易活動,offset pagination 能有效率地存取最新訂單。
資料模型定義如下:
| Kotlin | Swift |
|---|---|
data class Portfoliostocks: List<PortfolioStock> | struct Portfoliostocks: [PortfolioStock] |
data class PortfolioStockstock: Stockquantity: BigDecimalaveragePrice: BigDecimal | struct PortfolioStockstock: Stockquantity: DecimalaveragePrice: Decimal |
data class Orderid: StringstockSymbol: Stringquantity: BigDecimalstatus: OrderStatuscreatedAt: String … | struct Orderid: StringstockSymbol: Stringquantity: Decimalstatus: OrderStatuscreatedAt: String … |
enum class OrderStatusOPEN, EXECUTED, CANCELED | enum OrderStatuscase open, executed, canceled |
Notifications#
為了支援價格警示,我們提供:
- GET /v1/notifications:取得所有已建立的警示。
- POST /v1/notifications:為某支股票建立一個新的價格警示。
- DELETE /v1/notifications/
{id}:刪除既有警示。
WebSocket 連線#
為支援即時功能,我們在 wss://api.stocktrading.com/ws 建立 WebSocket 連線,支援的訊息類型包括:
stock-prices:串流訂閱股票的即時價格更新。order-updates:回報訂單狀態變化。portfolio-updates:反映因市場波動造成的投資組合價值變動。
Client 透過送出如下事件來訂閱更新:
{
"action": "subscribe",
"channels": [
{
"name": "stock-prices",
"symbols": ["AAPL", "GOOGL", "MSFT"]
}
]
}Step 3:高階 Client 架構#
API 設計完成後,讓我們發展高階的行動端架構。圖 3 呈現此架構,箭頭顯示資料在系統中的流動方向。
外部 Server 端元件#
在進入 Client 本身之前,先看看它依賴的外部元件:
- Backend:透過 REST API 與 WebSocket 與 Client 溝通,管理請求並維持資料一致性。
- CDN:對靜態內容(如歷史資料或圖表資源)進行邊緣快取,加速使用者存取。
- Push Notifications Provider:即使在 App 背景,也能及時將價格變動或訂單更新通知使用者。
- API Gateway:作為系統的守門員,負責認證與流量管控,保護 Backend 不被過載或惡意攻擊。
API Gateway#
在股票交易 App 中,安全是不可妥協的。我們需要強健的身份驗證、謹慎的請求監控以及嚴格的政策執行,同時維持流暢的使用者體驗。這正是 API Gateway 發揮關鍵作用的地方。
作為系統的「前門(front door)」,API Gateway 驗證並路由所有進入的 Client 請求至相應的 Backend 服務,並在任何可疑或未授權的請求到達核心系統前加以阻擋。
API Gateway 負責幾項關鍵職責:
- 安全(Security):管理使用者認證與授權、實施速率限制(rate limiting)、偵測可疑 Client。
- 流量管理(Traffic management):跨服務做負載平衡、控制請求量,避免尖峰交易時段系統過載。
- 請求處理(Request processing):驗證所有進入的請求、必要時進行資料格式轉換、管理 request/response 標頭。
- 監控(Monitoring):追蹤 API 使用量、效能指標與錯誤率,以維護系統健康並及早發現潛在問題。
- 簡化(Simplification):提供乾淨、統一的 API,讓行動 Client 不需要直接面對 Backend 架構的複雜性。
Client 架構#
我們的 App 採用清晰的分層架構(layered architecture),明確分離 UI 層與 Data 層的職責,讓程式碼更易維護與測試。
UI 層(UI layer) 由三個主要畫面組成,每個畫面都有自己的 state holder 來管理展示邏輯:
- Home screen:作為 App 的中央樞紐,顯示使用者的投資組合概覽與近期交易活動。
- Stock Detail screen:提供個別股票的完整資訊,包括價格圖表與關鍵指標。
- Place Order modal:提供精簡的買賣交易介面。
Data 層(data layer) 在幕後由三個專責的 repository 處理核心業務資料:
- Portfolio repository:作為使用者持股的單一真實來源(single source of truth),並持續與即時更新同步。
- Stocks repository:管理市場資料,確保使用者總是看到最新的報價資訊。
- Orders repository:處理所有交易的建立、追蹤與執行狀態。
此外,Push Notifications Client 元件負責監聽 Backend 發出的重要警示,確保使用者即時收到價格變動、訂單完成與其他關鍵事件的通知。
資料儲存#
在股票交易 App 中,不同類型資料對新鮮度的要求各不相同:
- 即時市場資料:僅採用記憶體內快取(in-memory caching),避免寫入磁碟。這個方式能:
- 確保使用者在做交易決策時看到最新報價。
- 維持金融 App 所要求的資料完整性。
- 時效性較低的資訊(如歷史圖表與使用者偏好):採用有明確過期策略的磁碟快取(disk caching),在效能與準確度之間取得平衡。
將即時行情只放在記憶體、不落地磁碟,是金融 App 避免顯示過期價格的常見做法,也能降低敏感資料外洩風險。
Step 4:深入探討#
App 中有些獨特面向值得特別關注。讓我們仔細探討建立歷史股價圖表與即時股價資料兩個主題。
建立歷史股價圖表#
視覺化歷史價格資料對任何股票交易 App 都是必要的。歷史股價資料通常提供多種時間區間選擇,例如日、週、月、年與 Max 視圖,每種都提供不同的交易洞察(如圖 4)。

擷取自 eToro Android App
資料密度考量#
每種時間區間都有獨特的資料處理挑戰。例如:
- 1 天的圖表可能需要 390 個資料點(6.5 小時交易時段內每分鐘一筆)。
- 5 年的圖表理論上可能包含超過 1,250 個日資料點或超過 260 個週資料點。
實作歷史圖表的一個關鍵挑戰是妥善管理資料密度(data density)。資料點越多細節越豐富,但也帶來效能成本:
- 高密度資料(如一個月內每分鐘的價格):可能造成渲染效能問題與過高的記憶體使用。
- 資料點太少:可能錯過重要的價格波動,造成誤導的視覺呈現。
產業洞察:不同股票交易 App 提供不同的使用者體驗,許多產品決策會影響圖表的設計與呈現。Robinhood 與 eTrade 在其官方說明中詳細介紹各自的圖表運作方式,值得深入參考。
圖表的實作方式#
在行動裝置上呈現歷史股票圖表,結合了大量資料集與有限螢幕空間,帶來獨特挑戰。表 1 比較幾種可行方案:
| 選項 | 說明 | 優點 | 缺點 |
|---|---|---|---|
| 原生繪圖(Native graphics rendering) | 使用平台繪圖 API(如 Android Canvas、iOS Core Graphics)手動繪製圖表元素 | 對視覺與效能有完全控制;記憶體足跡最小;直接硬體加速 | 實作複雜,需要平台專精;開發時間長;跨平台程式碼重複 |
| 第三方函式庫(Third-party libraries) | 使用成熟的行動圖表函式庫(如 MPAndroidChart、ChartsOrg/Charts)提供現成元件 | 實作快速,內建縮放、手勢等功能;文件齊全且維護良好 | 相較原生方案有效能開銷;與原生元件整合可能有挑戰 |
| WebView 渲染(WebView-based rendering) | 在 WebView 嵌入 Web 工具(如 D3.js、Chart.js)或專業產品(如 TradingView) | 可使用豐富的 Web 圖表生態系;跨平台實作一致 | 效能開銷與較高的記憶體使用;WebView 與原生整合有挑戰;原生感較弱 |
| Server 端渲染(Image API) | 在 Server 產出圖表並以圖片形式傳給 Client | Client 端處理需求最小;跨裝置行為一致 | 互動性有限;依賴網路;Server 成本與頻寬較高 |
Table 1:在 Client 呈現歷史股價圖表的各種取捨
考量我們的商業情境中多種 Client 共用同一 Backend,我們決定採用 WebView-based rendering 實作圖表。這個方式讓我們能跨平台維持一致的視覺呈現,並利用強大的 Web 端金融圖表函式庫。
最佳化 WebView 歷史圖表的實作#
使用 WebView 實作圖表帶來幾個重要挑戰,我們需要一一面對。
效能影響#
WebView 雖有彈性,但使用 D3.js 或 ECharts 等複雜 JavaScript 圖表函式庫可能拖累裝置資源。緩解策略:
- 將圖表與高優先流程(如下單執行)隔離開。
- 在 Server 端預先處理資料,減輕 Client 負擔。
- 精簡更新流程,避免重複重繪。
- 在初始完整載入後,對圖表執行 delta updates(差異更新)。
歷史股價資料密度也帶來獨特挑戰,建議採用:
- 在資料傳入 WebView 前先做預處理,減少 JavaScript 處理時間,特別是長時間範圍時。
- 採用分階段載入(phased loading):先載入低解析度資料,接著依需求載入更高解析度細節。
互動與反應速度#
WebView 所呈現的圖表在處理複雜互動時,可能不如原生實作順暢。為確保表現:
- 把運算盡量卸載到 Backend 或背景執行緒。
- 在多種裝置上驗證效能。
視覺整合#
WebView 元件有時會與原生 UI 顯得脫節。為了創造一致的體驗:
- 讓圖表樣式對齊 App 的設計系統。
- 調校 WebView 屬性以達成一致的視覺。
- 遵循平台 UI 最佳實踐,讓使用者感到熟悉。
平台特定挑戰#
每個作業系統都有獨特的障礙:
- Android:
- WebView 獨立於 App 更新。
- 行為可能因裝置與 Android 版本而異。
- 需要跨不同 WebView 實作進行測試。
- iOS:
WKWebView有特定的效能特性。- 本地儲存(local storage)的持久化需要謹慎處理。
- 記憶體管理需特別留意。
產業洞察:
- eToro 選擇整合 Web 端的 TradingView 圖表工具,透過 Web 元件在 App 中提供進階互動圖表。
- Robinhood 則使用原生繪圖 API 以最大化效能與控制,並在 Android 上開源了一個自訂的 sparkline 圖表函式庫 Spark。
即時股價資料#
股價在交易時段內快速變動,經常每秒變化多次。為呈現這些動態變動,股票交易 App 通常採用盤中(intraday)圖表,如圖 5 所示。

擷取自 Trade Republic Android App
為了維持 App 內一致性,我們將歷史與盤中即時資料視覺化都採用相同的 WebView-based 方式。這提供統一的視覺體驗,也簡化維護,因為我們只需專注在同一套圖表技術堆疊。
即時圖表更新的設計選項#
要把即時資料整合進 WebView 圖表,我們需要一個有效率的更新策略。每種方式對電池、處理能力與記憶體使用都有不同影響。表 2 列出各選項:
| 選項 | 說明 | 優點 | 缺點 |
|---|---|---|---|
| Direct UI updates(直接更新) | 每收到 WebSocket 新資料點就立即更新 UI | 實作最簡單;使用者永遠看到最新資料;延遲最小 | 若更新太頻繁會造成 UI jank;高耗電;可能壓垮主執行緒 |
| Buffered updates(緩衝更新) | 在 buffer 中收集資料變化,固定間隔更新 UI | 降低 UI 更新頻率;對電池更友善;平滑頻繁變動 | 顯示最新資料會有小延遲;可能錯過中間價格點;需要仔細調校更新間隔 |
| Priority-based updates(基於優先度的更新) | 對不同 UI 元素分配不同更新優先度 | 將資源聚焦在最重要的資料;關鍵資訊可更頻繁更新;整體體驗更佳 | 實作較複雜;需要精細調校優先度;若相關資料更新速率不同可能造成困惑 |
Table 2:即時 UI 更新的各種取捨
對於我們 App 的盤中圖表,我們採用 buffered updates 策略。對單一圖表元素而言,這在反應速度與資源效率間取得最佳平衡。
這個決策假設我們一次只顯示一個帶有即時更新的盤中圖表。如果產品需求改為同時顯示多個圖表,我們就需要重新考慮策略,實作一個結合 priority-based updates 的混合方案。
實作策略#
我們針對 WebView 盤中圖表的 buffered update 實作包含幾個關鍵元件:
- Stocks repository 包含 data buffering 邏輯,從 WebSocket 連線收集價格更新並放進 queue。
- **排程更新器(scheduled updater)**以固定間隔(例如每 1–2 秒)將緩衝的價格資料傳給 WebView 圖表。
- JavaScript bridge 在原生程式碼與 WebView 間高效傳輸市場資料更新。
理解 JavaScript Bridge#
股票圖表在 WebView 中以 JavaScript 執行。由於圖表渲染發生在此 JavaScript 環境,我們需要一種方式把處理過的資料從原生 App 傳進 WebView,這正是 JavaScript bridge 的角色。
JavaScript bridge 在原生 App 與 WebView 圖表之間建立通訊通道。即時股票資料首先透過 WebSocket 進入原生程式碼,而不是直接進入 WebView。這個設計有幾項好處:
- 原生程式碼更有效率地處理網路連線與資料處理。
- 可在送出視覺化前,將資料格式化妥當。
- 可在原生層實作更好的 buffering 策略。
這產生明確的職責分離:
- 原生程式碼(Native code) 負責:管理 WebSocket 連線、接收與驗證資料、緩衝更新以減少處理開銷、預處理顯示資料、決定何時送出更新。
- WebView 程式碼 負責:渲染圖表、處理使用者互動(zoom、pan、tap)、處理圖表動畫與轉場。
JavaScript bridge 支援雙向資料流動:
- Native → WebView:送出股票更新、設定變更、使用者偏好。
- WebView → Native:傳遞使用者互動事件,例如縮放、點擊資料點、或切換功能。
平台實作細節
Buffered updates 需要有效率地管理資料串流,再透過 JavaScript bridge 傳送以更新 WebView。
Android:
- 使用
JavascriptInterface機制在原生程式碼與 WebView 圖表間建立可靠通訊。- 利用 Kotlin
Flow搭配適當的 dispatcher,在主執行緒之外處理 buffering 與 throttling。iOS:
- 使用
WKUserContentController與WKScriptMessageHandler實現 native 到 JavaScript 的通訊。- 利用 Combine 框架的
throttle與debounce運算子控制更新頻率。
即時圖表更新的資料流#
圖 6 描繪使用者打開某支股票的盤中圖表並即時更新時的資料旅程:
- Stocks remote data source 透過 REST API 取得當日歷史資料,作為基準圖表資料。
- 同時,remote data source 建立 WebSocket 連線以接收即時價格更新。
- Stocks repository 結合這兩個來源並實作 buffered updates 策略:
- Repository 建立資料串流,並以當日歷史資料作為初始值發送。
- 當 WebSocket 更新到達時,收集到 repository 內的 buffer。
- Repository 並不立即把每筆更新送給訂閱者,而是批次這些更新並以固定間隔發送。
- Stock detail state holder 訂閱此緩衝資料串流,並負責透過 JavaScript bridge 更新圖表。
- WebView 圖表 渲染這些更新,呈現出平滑、即時的資料視覺化效果。
常見瓶頸與最佳化#
幾種常見問題仍可能影響即時 UI 效能:
- WebView 相關瓶頸:頻繁更新 WebView 會觸發昂貴的渲染操作。緩解方式:
- 設定圖表函式庫使用部分重繪(partial redraws),只更新有變動的元素。
- 在高頻率更新期間停用動畫以降低渲染開銷。
- 資料處理瓶頸:處理進來的市場資料可能成為瓶頸,尤其涉及複雜計算時:
- 把所有資料轉換搬到原生程式碼並在背景執行緒執行。
- 預先計算衍生值,而非在 JavaScript 中計算。
- 對計算值實作快取,避免重複計算。
- Bridge 通訊開銷:原生與 WebView 間的每次呼叫都有成本:
- 批次多筆價格更新,合併為一次 bridge 呼叫。
- 使用精簡的資料格式以降低序列化/反序列化成本。
- 維持在新鮮度與效能間取得平衡的一致更新間隔。
- 間歇性連線(Intermittent connectivity):行動網路天生不可靠:
- 當連線中斷時,清楚地提示資料已過時。
- 採用**指數退避(exponential backoff)**重連策略。
- 電池影響:即時更新會顯著影響電池壽命:
- 當 App 進入背景時停止即時更新。
- 根據電池電量實作自適應節流(adaptive throttling)。
隨著 App 複雜度與使用者量增加,即時更新系統也需要能夠水平擴展。我們設計的幾個面向確保可擴展性:
- WebSocket 訊息過濾:在 Server 端做過濾,確保 Client 只收到它真正需要的資料,即使 App 加入更多功能,也能降低 Client 端處理量。
- Buffer 大小:Buffer 需要夠大以容納兩次刷新間的所有價格更新,即使在市場劇烈波動時。對多數股票,10–20 個價格點的容量應已足夠。
- 自適應排程(Adaptive scheduling):根據市場狀況、裝置狀態與使用者參與度調整更新頻率(當使用者積極查看資料且 App 在前景時):
- 在高波動市場時段提高頻率。
- 允許使用者依據電池電量與裝置效能調整頻率。
產業洞察:
- J.P. Morgan、Commerzbank 等機構以及 Binomo 等交易平台,使用 SciChart 的原生引擎來驅動即時股票儀表板。
- Coinbase 的交易所平台提供 WebSocket feed,廣播訂單與交易的即時市場資料。
Step 5:總結#
在本章,我們設計了一個行動端的股票交易 App,提供投資管理的核心功能。系統支援:
- 查看即時股價。
- 透過下單與取消訂單執行交易。
- 透過圖表分析歷史價格資料。
- 接收即時價格警示。
架構上,我們結合 REST API(用於標準操作)與 WebSocket(用於即時更新)。採用分層的 Client 架構,將 UI 與資料關注點分離,讓程式碼易於維護與測試。我們也深入探討了歷史與即時盤中股價圖表的實作。
若準備在行動系統設計面試中討論此題,以下延伸主題值得進一步探索:
- 離線功能(Offline functionality):設計在網路連線有限時的快取與本地操作策略。
- 跨平台考量:討論此架構如何調整以支援 Web 與其他平台。
- 法規遵循(Regulatory Compliance):針對不同地區股票交易的合規需求做具體探討。
- 個人化 UI:提供可自訂的儀表板或觀察清單,以量身打造體驗提升使用者參與度。