Chapter 10. Event sourcing: a functional approach to persistence
覆盖functional storage view、event sourcing basics、write/projection/snapshot architecture与immutable storage tradeoffs,把state fold和append effect分开。
学习目标
- 能比较overwrite、immutable snapshots、event log与hybrid storage保留的信息、query和evolution成本
- 能设计
fold events -> state与state + command -> eventspure core,复现replay/idempotency - 能分析expected-version append、projection、snapshot与event evolution architecture,设计fault/rebuild gates
机制总览
Chapter 10. Event sourcing: a functional approach to persistence:机制路径
- 1
为什么Persistence也可以保存Change而不只保…
传统CRUD覆盖latest row,过去发生什么需另建audit。Event sourcing把domain facts append为immutable log,current state由ordered events fold得到。它天然适合functional transition/repl…
- 2
Thinking functionally about d…
Functional view把storage reads当facts/snapshots,把writes当new facts/versions,而非远程mutable variables。Overwrite简单、query直接;immutable snapshots保留versions;event…
- 3
Event sourcing basics
核心分两functions: Fold(initial, events) - state 与 Decide(state, command) - events/error 。Apply event应deterministic且无I/O;Decide只产生facts,不append。Shell load…
章级决策实验
Chapter 10. Event sourcing: a functional approach to persistence:机制与证据
切换《Chapter 10. Event sourcing: a functional approach to persistence》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么Persistence也可以保存Change而不只保…
传统CRUD覆盖latest row,过去发生什么需另建audit。Event sourcing把domain facts append为immutable log,current state由ordered events fold得到。它天然适合functional transition/repl…
可核验证据
以确定输入重复运行「为什么Persistence也可以保存Change而不只保…」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
学完《Chapter 10. Event sourcing: a functional approach to persistence》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
Chapter 10. Event sourcing: a functional approach to persistence:失效与核验
为什么Persistence也可以保存Change而不只保…
典型失效
若把「为什么Persistence也可以保存Change而不只保…」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「为什么Persistence也可以保存Change而不只保…」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
Thinking functionally about d…
典型失效
若把「Thinking functionally about d…」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「Thinking functionally about d…」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
Event sourcing basics
典型失效
若把「Event sourcing basics」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「Event sourcing basics」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
为什么Persistence也可以保存Change而不只保存Current State
传统CRUD覆盖latest row,过去发生什么需另建audit。Event sourcing把domain facts append为immutable log,current state由ordered events fold得到。它天然适合functional transition/replay,但引入event schema permanence、projection lag、concurrency和operational complexity,不是通用升级。
先预测:append-only就等于event sourcing吗;replay同events必然同state吗;snapshot可随意覆盖吗;projection exactly-once是否由framework保证;event可按class rename直接迁移。答案都是否。
↡以immutable domain events作为source of truth,通过ordered fold重建aggregate state的persistence model。Thinking functionally about data storage
Functional view把storage reads当facts/snapshots,把writes当new facts/versions,而非远程mutable variables。Overwrite简单、query直接;immutable snapshots保留versions;events保留intent/history;hybrid用events+snapshots/projections平衡load和query。
overwrite: id -> latest row
snapshot: (id, version) -> state
events: (streamId, sequence) -> immutable fact切换overwrite、snapshot、events与hybrid
Event sourcing basics
核心分两functions:Fold(initial, events) -> state与Decide(state, command) -> events/error。Apply event应deterministic且无I/O;Decide只产生facts,不append。Shell load stream、fold、decide,再以expected version atomic append。
AccountState Fold(IEnumerable<AccountEvent> events) =>
events.Aggregate(AccountState.Empty, Apply);
Either<Error, IEnumerable<AccountEvent>> Decide(
AccountState state, Withdraw command) =>
state.CanWithdraw(command.Amount)
? Right(new FundsWithdrawn(command.Id, command.Amount))
: Left(Error.InsufficientFunds);切换initial、apply、decide与replay
Architecture of an event-sourced system
Write path:load snapshot/tail、fold、decide、append(expectedVersion)。Conflict表示concurrent command基于stale state,需reload/redecide或返回conflict,不能盲重试旧events。Read path由projection消费events构建optimized views,通常eventually consistent。
Snapshot只是optimization,必须能验证stream/version和schema;删除events会改变source of truth。Projection consumer需checkpoint、idempotent update、duplicate/out-of-order policy与full rebuild。Outbox/subscription保证也要明确,不宣称神奇exactly-once。
var stream = await store.Load(streamId, token);
var state = Fold(stream.Events);
var pending = Decide(state, command);
await store.Append(streamId, stream.Version, pending.Right, token);切换write、projection、snapshot与migration
Operational replay gate
上线前把rebuild写成可重复runbook:在隔离read model从零消费golden streams,记录events/sec、lag、poison event、retry与checkpoint advance,完成后以golden queries和aggregate totals对比live projection。遇到unknown event不能跳过后继续宣称成功;应隔离stream、保留offset和原因,修复reader/upcaster后从安全checkpoint恢复。容量评估同时覆盖全量replay时长、双写切换窗口、磁盘峰值与回滚路径,确保“能重放”不是只在unit test成立。
Comparing different approaches to immutable storage
Event sourcing适合history本身有业务价值、复杂state transition需audit/replay、多projections受益的domain。Versioned snapshots适合point-in-time但不需intent;append audit+CRUD适合记录变更者;temporal DB提供system-time query。按requirements、team ops和schema evolution选择。
Event schema一旦发布就是长期contract。Prefer stable semantic events;rename/internal refactor不改wire name。需要upcaster/new event version时保留old reader与golden streams。
↡Event schema、meaning和ordering跨多年code versions保持可replay,必要时通过upcast/version兼容。本章回顾:Pure Fold不等于简单Operations
- Event sourcing保存domain facts,state由ordered deterministic fold重建。
- Decide产生events,shell以expected version append,I/O不进入Apply。
- Projection、snapshot是derived optimization,需要checkpoint、idempotency和rebuild。
- Event schema是长期contract,internal rename不能破坏history。
- CRUD/audit/snapshot/events各有tradeoff,按domain history价值选择。
练习
问题 1:并发两次withdraw怎样避免lost update?
问题 2:Projection rebuild需要哪些证据?
问题 3:何时不应选择Event Sourcing?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- event-sourced state
- immutable storage model
- deterministic event fold
- expected-version append
- event evolution contract
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
Architecture of an event-sourced system:机制、边界与证据
Chapter 10. Event sourcing: a functional approach to persistence中的Architecture of an event-sourced system应写成可组合的输入—输出契约,并把环境读取、状态改变与失败显式放在边界。用确定输入运行正常、空值和失败样本,同时记录返回值与副作用轨迹,证明结论不依赖隐藏状态。