Event Storming 把領域事件(Domain Events)帶上了白板。Event Sourcing 把同一批事件帶進資料庫——不是以「現在的狀態」儲存,而是以「導致現在狀態的所有事件」儲存。CQRS(Command Query Responsibility Segregation)接著把讀和寫徹底分開,讓每一邊都能用最適合它的模型。
可佐證:Martin Fowler 在 martinfowler.com/bliki/CQRS.html 有對 CQRS 的定義說明。Microsoft Azure Architecture Center 有 Event Sourcing Pattern 與 CQRS Pattern 的詳細說明(含取捨分析)。CQRS 術語由 Greg Young 在 2010 年定義並在社群廣泛傳播。
Event Sourcing:事件是真相來源
傳統資料庫儲存「現在的狀態」:一筆 orders row,status = 'shipped'。你看不到它怎麼從 pending 到 shipped,除非另外建了稽核表。
Event Sourcing 倒過來:只儲存導致現在狀態的所有事件,把現在的狀態當作派生結果。
Event Store(append-only)
─────────────────────────────────────────────────────
order-123 | OrderCreated | {customerId, items, total}
order-123 | PaymentReceived | {paymentId, amount}
order-123 | OrderConfirmed | {confirmedAt}
order-123 | OrderShipped | {trackingNumber}
─────────────────────────────────────────────────────
當前狀態 = replay 以上四個事件後的 Order aggregate
核心特性
Append-Only:事件只加不改不刪。一旦 OrderPlaced 被寫入,它永遠在那裡。
完整稽核軌跡:你永遠知道「什麼時候、誰、做了什麼、導致了什麼變化」——不需要另外設計稽核機制。
時間旅行:可以重放事件到任意時間點,重建過去某個時刻的狀態。
新 Projection 可後建:如果下週需要一個新的報表視圖,可以從頭重放所有歷史事件來建立它。
Aggregate 與事件的關係
Aggregate 是 Event Sourcing 的核心執行單元:
Client → Command(PlaceOrder)
│
▼
Order Aggregate
├─ 驗證業務規則(不變量)
│ 「庫存是否足夠?」「用戶是否已驗證?」
├─ 如果合法:emit Domain Event(OrderPlaced)
└─ 如果不合法:throw Domain Exception(不寫任何 event)
│
▼
Event Store(append event)
Aggregate 的狀態(例如 order.status)不是直接儲存的欄位,而是 apply 所有該 aggregate 歷史事件後的計算結果。order.status = 'shipped' 是因為先後 apply 了 OrderCreated、PaymentReceived、OrderShipped 這三個事件。
Snapshot 機制:如果一個 Aggregate 有幾千個歷史事件,每次 replay 成本太高,可以定期儲存狀態快照,replay 時從最近的快照加上後續事件。這是效能優化,不影響語義。
CQRS:讀寫分離
CQRS 的核心思路很簡單:寫模型(Write Model)與讀模型(Read Model)是不同的模型,用不同的資料結構、可能跑在不同的資料庫。
┌────────────────────────┐ ┌──────────────────────────────┐
│ Write Side │ │ Read Side │
│ │ │ │
│ Command Handler │ │ Projection / Event Handler │
│ → Aggregate │──▶ │ 消費 Domain Events │
│ → emit Events │ │ → 建立非正規化 Read Models │
│ → Event Store │ │ → 寫入查詢用資料庫 │
│ │ │ │
│ 一致性:強一致 │ │ 一致性:最終一致 │
└────────────────────────┘ └──────────────────────────────┘
Write Side(指令側)
負責處理所有改變狀態的操作:接收 Command、載入 Aggregate、執行業務規則、發出 Event、寫入 Event Store。
Write Side 的資料結構以領域不變量為中心設計,不考慮查詢效率。
Read Side(查詢側)
Projection 是 Read Side 的核心機制——它是一個事件處理器,監聽 Event Store,把事件翻譯成查詢用的非正規化視圖:
OrderProjection 監聽:
on OrderCreated → INSERT INTO order_summaries(id, status='pending', ...)
on OrderShipped → UPDATE order_summaries SET status='shipped', trackingNumber=... WHERE id=...
on OrderCancelled → UPDATE order_summaries SET status='cancelled' WHERE id=...
每一個前端頁面可以有自己專屬的 Projection——OrderListProjection(清單頁需要的欄位)與 OrderDetailProjection(詳情頁需要的欄位)可以是完全不同的 read model,各自優化查詢效率。
Read Side 可以用不同技術:Write Side 用 PostgreSQL 的 Event Store,Read Side 可以用 Elasticsearch(全文搜索需求)、Redis(快取需求)、MongoDB(複雜結構需求)。
最終一致性
Write Side 寫完事件到 Read Side 完成 Projection 更新之間,有一段短暫的延遲(通常毫秒到秒級)。這是 CQRS 架構的固有取捨,不是 bug。前端需要能夠處理「剛剛下訂單,馬上查訂單清單可能還沒出現」的情況。常見解法:
- 樂觀 UI 更新(先在前端假設成功,等事件來了再同步)
- 命令回應中包含預期結果(讓前端先顯示)
- 在 UI 上明確顯示「處理中」狀態
與 Event Storming 的對應
Event Storming 的輸出直接映射到 Event Sourcing 的結構:
| Event Storming | Event Sourcing / CQRS |
|---|---|
| Domain Event(橘色) | Event Store 裡的事件型別 |
| Command(藍色) | Command Handler 的輸入 |
| Aggregate(淡黃色) | Aggregate class(處理 Command、emit Event) |
| Actor(黃色) | API 呼叫者(AuthN/AuthZ 邊界) |
| Policy(紫丁香色) | Process Manager / Saga |
| Read Model(綠色) | Projection 的輸出 |
| External System(粉色) | Anti-Corruption Layer 的對端 |
這個對應是雙向的:在 Event Storming 的 Design Level,可以帶著 Event Sourcing 的概念結構回去,補強 Aggregate 邊界的設計決策。
何時用 / 何時不用
適合用 Event Sourcing + CQRS 的情境:
- 需要完整稽核軌跡:金融交易、醫療記錄、法規合規場景
- 讀寫需求差異極大:讀 10000 次/秒但寫 1 次/秒,或讀模型形狀和寫模型差異很大
- 領域複雜度高:多個 Bounded Context、複雜業務規則、多步驟流程
- 需要時間旅行或事件重放:debug 歷史狀態、後建報表、資料遷移
不適合的情境(方法論主張,非可量化門檻):
- 簡單 CRUD 系統:如果 Aggregate 只有一兩個欄位、業務規則平凡,Event Sourcing 的複雜度不值得
- 小團隊早期探索:Schema 頻繁變化時,事件版本控制(Event Versioning)的成本很高
- 查詢為主的系統:如果系統幾乎只讀不寫,CQRS 的讀寫分離帶來的收益很有限
主要代價(已記錄的已知問題)
Event Versioning:事件結構改變時,舊事件仍然在 Event Store 裡,Projection 必須能處理新舊版本。常見模式(方法論主張):Upcaster(把舊版本事件升級成新版本)、Weak Schema(只讀取自己關心的欄位)、Event Folding。
Eventual Consistency 的使用者體驗:需要前端設計配合,否則使用者看到「下單後清單沒更新」會誤以為系統壞了。
Projection 同步失敗:Projection 是非同步的,如果它崩潰,Read Model 會落後。需要監控、replay 機制、以及「Read Model 可以從頭重建」的設計。
認知負荷:Command、Event、Aggregate、Projection、Read Model 這些概念需要整個團隊都理解,否則只有一兩個人懂架構,維護風險高。
延伸
- 從 Event Storming 帶出 Aggregates → Event Storming:協作式領域探索
- Read Models 如何驅動前端 → 前端從領域模型推導
- 實戰中的 Event Store + Projection 設計 → 完整流程實戰:電商訂單系統走一遍
- 把 Aggregate 不變量寫成形式化規格 → kiro-frame:Problem Frames 形式化工具
- 回到全景 → Event Storming + Event Sourcing + Pattern Language 專欄首頁