Saga Pattern

What — 把跨服務的分散式事務拆成一系列本地事務,每步成功後發布事件/訊息觸發下一步;失敗則觸發補償事務(compensating transaction)。

Why — 微服務架構下無法使用跨 DB 的 ACID 事務;2PC(Two-Phase Commit)雖然存在,但鎖定時間長、協調者單點故障風險高,在高流量系統裡是性能殺手。

When — 訂單處理、付款流程、預訂系統等需要跨多個 bounded context 的業務操作。

  • Anti-use-case:單一服務內部操作;需要強一致性的金融核心交易(這種情況考慮單體或分散式鎖);流程步驟少於 2 步(直接用本地事務)。

Where — 跨多個微服務邊界的業務流程層;訊息 broker 驅動的事件流。

Who

實踐

兩種變體的取捨:Choreography Saga(事件驅動,無中央協調者,耦合低但 debug 難)vs Orchestration Saga(有 Saga orchestrator,流程清晰但多一個元件)。

注意事項與常見坑:

  • 補償事務必須冪等——補償本身也可能被重試。
  • Saga 日誌需要持久化,crash 後才能從中間步驟回復而不是重頭跑。
  • Choreography saga 一定要有全局 correlation ID,否則跨服務 debug 等於看天書。
  • 不要把 Saga 當成分散式鎖用——它處理業務一致性,不是並發互斥。
  • 補償 != 回滾:補償是「新的正向操作」(退款),不是資料庫層面的 rollback。

Outbox Pattern

What — 把「要發布的事件」和「domain 資料」寫進同一個 DB 事務(outbox table),再由獨立的 relay 程序把 outbox 裡的事件轉發給訊息 broker。

Why — 雙寫問題(dual-write)——先寫 DB 再寫 Kafka,或反之,都有一半失敗的窗口。Outbox 讓「業務資料 + 事件意圖」的一致性由 DB 的 ACID 保證,徹底消除視窗。

When — 任何「DB 操作 + 訊息發布」需要原子性的場景。

  • Anti-use-case:事件允許丟失(fire-and-forget metrics);broker 本身提供事務性發布(Kafka 的 transactional producer 可替代部分場景)。

Where — 寫入路徑的持久化層與訊息 broker 之間的橋梁;Domain event 的發布機制。

Who

實踐

兩種 relay 策略:

  1. Polling relay:定期 SELECT 未處理行,簡單但有輪詢延遲與 DB 壓力。
  2. CDC(Change Data Capture)relay:監聽 binlog/WAL(如 Debezium),延遲低、不侵入業務,但需要 binlog 權限與額外基礎設施。

注意事項與常見坑:

  • outbox table 要有 processed 欄位或直接刪除已發送行——刪除更簡單,processed 欄位需要額外 sweep job。
  • relay 是 at-least-once delivery——接收端必須冪等(見冪等性(Idempotency))。
  • 別在 outbox 裡存大 payload;只存 event ID + type + aggregate ID,payload 讓接收端從主表查或從 event store 取。
  • 多個 relay 實例要做分散式鎖或 leader election,避免重複投遞。

Circuit Breaker

What — 包裝對外部服務的呼叫;偵測連續失敗後「開路(open)」,在 open 狀態下直接 fail-fast 不發出請求;冷卻後進入 half-open 測試是否恢復。

Why — 分散式系統的上游服務崩潰時,下游若持續重試會耗盡執行緒/連線池,引發 cascading failure 讓整個服務群連鎖崩潰。

When — 任何呼叫外部 HTTP API、DB、快取、gRPC 服務的地方。

  • Anti-use-case:同一程序內部方法呼叫;對自己完全控制的本地資源(local file system 等)。

Where — 服務之間的呼叫邊界;API gateway 與下游服務之間;gRPC client interceptor。

Who

實踐

三狀態轉換:CLOSED(正常)→ OPEN(開路 fail-fast)→ HALF_OPEN(試探)→ 回到 CLOSEDOPEN

Threshold 設計的常見誤解:

  • 固定次數閾值在低流量時反應過慢(10 次失敗在高流量是 1 秒,在低流量是 1 小時)。
  • 正確做法是時間窗口比例閾值(sliding window,如 60 秒內 50% 失敗率觸發)。
  • resilience4j 兩種都支援:COUNT_BASEDTIME_BASED

注意事項與常見坑:

  • open 狀態要有明確的 fallback(cache 舊資料、降級回應、預設值)——fail-fast 沒有 fallback 等於把問題往上推。
  • 不要把 Circuit Breaker 與 Retry 疊加而不調整參數:retry 3 次 × failureThreshold 5 次 = 要 15 次失敗才開路,保護效果大打折扣。
  • half-open 狀態只放少量試探請求,別把整個 traffic 一次切回去。

Bulkhead

What — 把資源(執行緒池、連線池、semaphore)依消費者/服務隔離;一個艙壁破損,其他艙壁不受影響。名稱來自船舶的水密艙壁設計。

Why — 共享資源池下,一個慢下游佔滿全部執行緒,其他正常呼叫全部排隊超時。Bulkhead 讓資源競爭侷限在單一隔間,避免單點故障擴散為全局故障。

When — 多租戶系統;一個服務同時呼叫多個不同重要性的下游(核心付款 vs 邊緣推薦服務)。

  • Anti-use-case:資源本就富餘的低流量系統(過早優化增加複雜度);所有下游等重要性時(隔離反而造成資源浪費)。

Where — 服務的出口呼叫層;多租戶 API 的入口;istio/linkerd service mesh 的 connection pool 設定。

Who

實踐

兩種 Bulkhead 類型的選擇:

  • ThreadPoolBulkhead:適合呼叫阻塞式 I/O(傳統 JDBC、同步 HTTP client);隔離性強但執行緒有 context switch 成本。
  • SemaphoreBulkhead:適合 reactive/non-blocking;overhead 低,但所有呼叫共享同一執行緒池,只限制並發數量。

注意事項與常見坑:

  • Bulkhead 大小(執行緒數或 semaphore 上限)要靠壓測決定,不是憑感覺——每個下游的延遲特性不同。
  • 與 Circuit Breaker 搭配效果最好:Bulkhead 限制並發,Circuit Breaker 偵測失敗模式。
  • 別讓 bulkhead queue size 無限大——queue 滿了要 reject,否則 bulkhead 變成一個超大緩衝區,沒有保護效果。

Hexagonal Architecture(Ports & Adapters)

What — 業務核心(application/domain)定義 port(介面);外部世界(DB、HTTP、訊息佇列、CLI)透過 adapter 實作 port;核心不依賴任何外部框架或基礎設施。

Why — 測試困難、框架升級成本高、業務邏輯被框架污染。Hexagonal 讓核心可以獨立測試(用 in-memory adapter),也讓切換基礎設施(換 ORM、換 broker)不需改業務邏輯。

When — 預期長達數年的應用程式;業務邏輯複雜的服務;需要多種測試層次(unit/integration/e2e)的系統。

  • Anti-use-case:CRUD-only 服務;短生命週期腳本;prototype 階段——這些場景下 Hexagonal 是過度設計。

Where — 整個應用的架構層次劃分;特別是 domain logic 與 infrastructure 的邊界。

Who

實踐

Port 的方向性:

  • Primary port(driving port)= 外部驅動服務的入口,如 HTTP controller、CLI、gRPC server。
  • Secondary port(driven port)= 服務驅動外部資源的出口,如 UserRepositoryEventPublisher

注意事項與常見坑:

  • Port 命名要用 domain 語言,不是 DB 語言:OrderRepository(domain)而不是 OrderJpaRepository(infrastructure 洩漏)。
  • Hexagonal 不等於強制三層(presentation/application/domain)——可以更扁,重點是核心不依賴外部。
  • Adapter 測試要分兩種:unit test 用 in-memory adapter 測 domain logic;integration test 才啟動真實基礎設施測 adapter 本身。
  • AI 生成程式碼更容易混淆 port/adapter 邊界——考慮用 ArchUnit(Java)或 dependency-cruiser(Node)加 linting 規則強制邊界。

Strangler Fig

What — 在舊系統前加一個 facade(通常是 API gateway 或 proxy);把請求逐步路由到新系統,直到舊系統被完全「絞殺」替換。命名來自絞殺榕——先寄生舊樹,最終取而代之。

Why — 大爆炸式重寫(big-bang rewrite)歷史上失敗率極高(Netscape 6、Google 多個放棄的重寫項目);Strangler Fig 讓新舊並行、增量遷移、每步可驗證、可回退。

When — 任何需要替換遺留系統的場景。

  • Anti-use-case:系統太小,直接重寫更快;遺留系統無法被 HTTP/event 層攔截(緊耦合批次作業、無 API 層的直連 DB 系統)。

Where — 系統遷移的整個生命週期;facade 通常在 API gateway 或 reverse proxy 層。

Who

實踐

Facade 路由策略:

  • Feature flag 路由:同一個 endpoint 可以 A/B 路由到新舊系統,方便灰度放量。
  • 流量百分比路由:先 1%,驗證後 10%、50%、100%。
  • 特定用戶/租戶優先遷移(canary 用戶)。

注意事項與常見坑:

  • 設定明確的完成里程碑——防止「永遠在 strangling」,舊系統殭屍化但沒人負責下線。
  • DB 遷移是最難的環節:要考慮雙寫(write to both)+ 漸進切讀;資料格式轉換要在 facade 層或 adapter 層處理。
  • 要有 metrics 追蹤新系統流量佔比,可視化遷移進度。
  • 別忽略舊系統的 batch job、定時任務、non-HTTP 入口——這些往往是最後被遷移的長尾。

冪等性(Idempotency)

What — 相同操作執行多次,結果與執行一次相同。對冪等 API 的重試是安全的,不會產生重複副作用。

Why — 網路不可靠,訊息佇列是 at-least-once delivery——接收端必須處理重複;支付、訂單建立等操作若不冪等,重試就是災難。

When — 任何暴露給外部的 mutation API;任何 message consumer;任何定時任務。

  • Anti-use-case:純讀取操作(GET 天生冪等)——額外的冪等機制反而增加複雜度;純日誌記錄(append-only,重複是期望行為)。

Where — API 層(HTTP mutation endpoints);message consumer 層;事件處理器;定時批次任務。

Who

  • Stripe — https://stripe.com/docs/api/idempotent_requests(`Idempotency-Key` header,DB-backed 存儲 key→response 映射,業界標準範例,有 24h TTL)
  • AWS SQS FIFO — at-least-once + MessageDeduplicationId,broker 層去重。
  • Kafka consumers — consumer offset commit 本身就是 idempotent read progress 的設計。

實踐

幾種常見的冪等實作策略:

  1. DB unique constraint — 最簡單,INSERT IF NOT EXISTS(Postgres 用 ON CONFLICT DO NOTHING),讓 DB 保證唯一性。
  2. Idempotency key 映射表 — 服務端存儲 key → response,key 由呼叫方生成(UUID v4),有 TTL 自動清理。
  3. 狀態機冪等 — 操作前檢查當前狀態,已到目標狀態則直接回傳成功(訂單已付款,重複付款請求直接回傳已付款)。
  4. 事件消費冪等 — 用 processed_events 表,收到事件前先查 event_id 是否已處理。

注意事項與常見坑:

  • Idempotency key 必須由呼叫方生成(不是服務端),這樣重試時 key 相同。
  • Key 的 TTL 設計要考慮最長合理重試窗口(Stripe 24h 是業界常見參考值)。
  • 冪等 key 存儲要和業務操作在同一個 DB 事務——否則 key 寫入成功但業務操作失敗,下次重試被誤判為已處理。
  • 分散式快取(Redis)做冪等 key 存儲要考慮 cache eviction——TTL 到期前 key 被驅逐,重試會被當新請求處理。

Actor Model

What — 計算的基本單元是 Actor;Actor 只能透過訊息通訊,沒有共享記憶體;每個 Actor 有獨立的 mailbox,訊息按序處理;Actor 可以建立子 Actor、監督(supervise)子 Actor 的失敗。

Why — 多執行緒共享記憶體的並發問題(race condition、deadlock)在高並發系統中難以正確實作。Actor 用訊息隔離狀態,讓並發推理回歸線性——每個 Actor 的邏輯是單執行緒的。

When — 高並發、分散式狀態管理(IoT device 管理、遊戲伺服器、即時協作、電信系統)。

  • Anti-use-case:簡單的 request-response 服務(Actor overhead 不值得);CPU-bound 批次計算(shared-memory 並發更快,避免訊息序列化成本)。

Where — 有狀態服務的並發控制層;分散式系統的節點狀態管理;事件驅動的 domain object 生命週期管理。

Who

實踐

Actor supervision tree 設計是核心架構決策:

  • 誰監督誰(supervisor hierarchy)決定故障隔離的邊界。
  • 失敗策略:restart(重啟 actor,清空狀態)、stop(停止,讓父 actor 決定)、escalate(向上傳遞)。
  • restart 會丟失 actor 記憶體狀態——需要持久化的用 Akka Persistence(event sourcing)。

Orleans vs 傳統 Akka:

  • Orleans virtual actor(grain)的優勢是不需手動 activate/deactivate,框架自動管理——適合雲原生、大量 actor 實例(百萬級)。
  • Akka 更底層,控制更細緻,適合需要精確控制 actor 生命週期的系統。

注意事項與常見坑:

  • 訊息要設計成不可變(immutable)——共享可變訊息會把 Actor model 的安全性保證破壞殆盡。
  • Actor mailbox 滿了要有 backpressure 或 drop 策略(Akka 的 bounded mailbox)。
  • Actor model 不是萬靈丹——event sourcing 配 Actor(Akka Persistence)才能做持久化狀態,否則 actor restart 後狀態全失。
  • 避免在 Actor 的訊息處理中做阻塞 I/O——這會讓 actor dispatcher 的執行緒耗盡,效果比不用 Actor 更糟。

Materialized View

What — 預先計算並持久化查詢結果(把複雜 join/聚合結果存成一張「假表」);讀取直接命中預計算結果,寫入時更新 materialized view。

Why — 讀寫比懸殊的系統裡,每次讀取都跑複雜 join 是資源浪費;Materialized View 是「用儲存空間換查詢時間」的典型取捨,讀取複雜度從 O(join) 降至 O(1)。

When — CQRS read side;dashboard;報表;全文搜索索引——任何「查詢複雜但更新頻率可接受延遲」的場景。

  • Anti-use-case:資料高頻更新且需要強一致性(materialized view 通常 eventually consistent);view 的計算代價比查詢本身更高(反而增加總成本)。

Where — CQRS 的 read model 層;報表與 analytics 資料庫;搜索引擎索引;Redis 作為預計算快取。

Who

實踐

三種更新策略的取捨:

策略一致性寫入延遲適用場景
Eager(同步更新)讀一致性要求高的核心功能
Lazy(讀時更新)低(寫),高(首次讀)讀頻率低的冷資料
Scheduled(定期批次刷新)最終一致報表、dashboard

注意事項與常見坑:

  • Invalidation 邏輯比 cache 更複雜——要追蹤 source table 與 view 的依賴關係,多表 join 的 view 需要監聽多個來源。
  • PostgreSQL 的 REFRESH MATERIALIZED VIEW 非 CONCURRENTLY 版本會鎖表,生產環境要用 CONCURRENTLY 版本(需要 unique index)。
  • 在 CQRS 架構裡,materialized view 就是 projection;Event Sourcing 天然支援重播所有事件重建 view(見 event-sourcing-cqrs-後端落地)。
  • 多個 materialized view 之間若有依賴(view A 依賴 view B 的結果),刷新順序要正確管理,否則產生不一致的中間態。

與既有專欄的關係

CQRS 與 Event Sourcing 的深度討論(Event Store 設計、Aggregate 狀態機、schema evolution)收錄在 Event Storming + Event Sourcing + Pattern Language 專欄,本文不重複。

本文聚焦在上述九個 pattern 的進階取捨與實際落地,與既有事件驅動專欄可交叉對照。


待解問題 / 持續追蹤

  • Saga orchestration vs choreography 的選擇標準業界沒有定論——有無可量化的複雜度或可維護性指標?
  • Circuit Breaker 的 threshold 參數業界如何自動調整?有 adaptive circuit breaker 的成熟實作嗎?
  • Hexagonal Architecture 在 AI coding 時代的意義:AI 生成的 code 更容易把邊界混淆——是否需要 linting 工具強制 port/adapter 分離?
  • Actor model 的 Orleans virtual actor 和傳統 Akka actor 的性能邊界在哪裡?有公開 benchmark 嗎?

更新紀錄

  • 2026-07-03 — 建立。涵蓋 Saga、Outbox、Circuit Breaker、Bulkhead、Hexagonal、Strangler Fig、冪等性、Actor Model、Materialized View 九個後端 pattern。