不要儲存「現在的狀態」,而是儲存「導致這個狀態的每一個事件」。狀態變成衍生物:對事件序列做 fold 就能重建出來。難點不在寫事件,而在讀與演化。
🧠 Intuition
TIP
把它想成銀行帳本,而不是帳戶餘額。你從不直接改餘額,你只 append 一筆筆交易(事件);餘額是把所有交易加總算出來的。事件是 source of truth,狀態只是一個 cache。
- 系統的 source of truth 是一條不可變、只 append 的事件序列(
OrderPlaced、ItemShipped…),事件描述「已經發生的事實」,過去式命名。 - 當前狀態靠重播 (replay) 事件、逐筆套用 reducer 重建;同一串事件可以重建出多種不同的讀模型。
- snapshot 是優化手段:定期把「重播到第 N 筆」的狀態存檔,之後只需從 snapshot 往後重播,避免每次都從頭算。
- 它和傳統 CRUD 的根本差異:CRUD 的 UPDATE 會就地覆寫並丟失歷史;事件溯源永不覆寫,歷史本身就是資料。
⚖️ Tradeoffs
優點 — 完整審計與時間旅行
- 每個狀態變化都有不可變紀錄,天生具備 audit log,金融/合規場景幾乎免費拿到。
- 可重播到任一時間點的狀態(time travel),方便除錯、重現 bug、做事後分析。
- 同一串事件可以重新投影出全新的讀模型——需求變了不必遷移舊資料,重播即可生成新視圖。
代價 — 查詢需要投影
- 事件 store 只擅長「給我某個 aggregate 的事件流」;要做「所有未出貨訂單」這種查詢,必須先把事件投影 (projection) 成讀模型。
- 這把複雜度推給了讀端:通常得搭配 CQRS,維護一份或多份物化的讀模型,且讀模型是最終一致的。
- 直接對 raw event store 做 ad-hoc 查詢通常很痛,不要這樣用。
代價 — Schema 演化困難
- 事件是不可變且永久保存的,第一版事件格式可能要被讀十年。
- 演化手段:upcasting(讀取時把舊版事件轉成新版)、weak schema(容忍多餘/缺欄位)、版本化事件型別。
- 千萬別「修改歷史事件」來改格式——那違反不可變性,也毀掉審計價值。要修正錯誤就 append 一筆補償事件 (compensating event)。
與 CDC 的差異 — 領域事件 vs 資料變更
- CDC (Change Data Capture) 捕捉的是資料庫層的列變更(before/after row),是技術產物、由 DB 結構決定,語意低。
- 事件溯源捕捉的是領域事件:帶業務意圖(
CartAbandoned而非「row 從 active 改成 inactive」),語意高、由領域建模產生。 - CDC 是把既有 CRUD 系統的變更「事後抽出來」;事件溯源是一開始就以事件為事實來源。兩者都產生事件流,但抽象層級與意圖完全不同。
🔑 Takeaways
- 事件溯源把 source of truth 從「可變狀態」換成「不可變事件序列」,狀態變成可重播的衍生物——歷史不再被覆寫。
- 拿到的是完整審計與時間旅行;付出的是查詢必須走投影(常配 CQRS、最終一致)與 schema 演化的長期負擔。
- snapshot 是純優化,解決「每次從頭重播太慢」,不改變事件才是事實的本質。
- 別把它和 CDC 混為一談:CDC 是資料層變更的事後捕捉,事件溯源是領域意圖的事前建模。
2026-07-01
- Q: 事件溯源和用 CDC(如 Debezium)把資料庫變更轉成 Kafka 事件,最後不是都得到一條事件流嗎?差在哪?
- A: 差在事實來源的方向與語意層級。CDC 系統裡,source of truth 仍是那張可變的資料表,事件是 DB 列變更的副產物,描述「欄位從 A 變成 B」,缺乏業務意圖、也無法回答「為什麼變」。事件溯源裡,事件本身就是 source of truth,狀態反而是衍生 cache,事件帶領域意圖(
OrderCancelled含取消原因),能完整重建「發生過什麼」。實務上 CDC 是改造既有 CRUD 系統、拿到變更流去餵下游的低成本路徑;事件溯源是從建模階段就承諾以事件為核心,成本更高但拿到完整審計與時間旅行。要審計與業務語意選事件溯源,只是要同步資料到下游選 CDC。