为什么选择 Wow
Wow 让团队只编写领域模型,就得到一个可以运行的系统:它的路由、契约、客户端、读模型,以及让它们保持一致的机制。领域模型之外的一切,都是 Wow 欠你的代码。
它做什么
每个 Wow 系统都做三件事,其中的每个机制都源自其中之一:
- 写入。 命令基于最新提交的状态做出决定,变成事件,在一次原子提交中追加。对同一个聚合的两个写入者永远不会都成功:由版本决定,失败的一方会重新做一次决定。
- 投递。 每个提交的事件都至少一次到达每个观察者:Saga、投影、反应器、其他上下文、数仓。任何观察者都可以从头再来。
- 使用。 从同一个领域模型得到 HTTP 路由、OpenAPI 文档、类型化的 TypeScript 客户端、对每个聚合的状态和历史的查询、数据视图和仪表盘,无需更多代码。
原则
| 原则 | 实际意味着 | |
|---|---|---|
| 1 | 一次写入 | 追加到日志是唯一的写入。发布、快照、投影、索引和数仓都读取日志 |
| 2 | 日志即总线 | 订阅从它自己的检查点读取日志。在上下文之间,事件以发布的事件流传递 |
| 3 | 先决定,再演化 | 决定产生事件;演化把事件应用到状态上,不做 I/O。状态可以是普通的可变对象:内核让每个版本的视图保持稳定 |
| 4 | 构建时绑定 | 编译器生成模型:运行时没有反射或扫描 |
| 5 | 协程优先 | 处处是 suspend 和 Flow;Reactor 代码仍可通过适配器工作 |
| 6 | 身份是一个值 | 聚合的上下文、类型、租户和 ID 在准入时一次性决定。每个唯一键都包含租户 |
| 7 | 读模型可重放 | 每个读模型都可以从日志重建 |
| 8 | 失败即数据 | 失败记录在台账中,并让失败的那一个聚合停放。不会跳过任何东西 |
| 9 | 一份契约 | 编译器写出的 Manifest 是路由、OpenAPI、客户端、视图和兼容性检查的来源 |
| 10 | 不绑定宿主 | 运行时与领域代码不依赖任何 Spring 类型,为其他宿主留着门。10.0 中受支持的宿主是 Spring Boot:在它之外组装节点仍是内部 API(不用 Spring 运行) |
| 11 | 状态即读模型 | 每个聚合的最新状态都可以查询,无需编写投影 |
这些原则合起来,就是每个版本都必须保持的保证:
- 任何时刻的崩溃都不会丢失已提交的事实;
- 每个读模型,包括快照,都可以从头重建;
- 使用相同 ID 的两个租户永远不会相互影响;
- 在稳定状态下,存储的读取负载不随订阅数量增长;
- 在领域模型之外,你不编写任何基础设施代码。
概念从第一性原理解释其中的每一条。
它的前提
- 每个系统一个集群。 一个系统的所有服务运行在同一个集群中,能直接相互访问:跨服务跟随命令的等待,以及服务节点之间的通道,都建立在这一点之上。内部端口从不暴露到集群之外,集群内部的流量也要认证(保护部署)。
- 持久化存储,以及上下文之间的 Kafka。 每个服务把它的日志保存在 PostgreSQL 或 MongoDB 上。上下文之间通过 Kafka 获取彼此的事件和命令。
- JDK 21 和 Gradle。 模型和契约在构建时由 KSP 和 Wow Gradle 插件绑定,这个插件没有 Maven 的对应物。
什么时候它不是合适的工具
- 几张表上的简单 CRUD。 Wow 的价值体现在决定、历史和后果都很重要的地方。规则用一个数据库约束就能表达的服务,要为它用不上的日志付出代价。
- 必须抹除的历史。 事件从不删除,Wow 也不提供抹除或粉碎事件的方式。必须能按请求删除的个人数据,应放在事件之外、放在你可以从中删除的读模型里(事件中的个人数据)。
- 分布在多个集群中、无法直接相互访问的服务,或者用 PostgreSQL 或 MongoDB 之外的存储保存日志。
- 只有 Maven 的构建,或者低于 21 的 JVM。
从 9.x 而来
Wow 10 是对 9.x 的重新设计,而不是升级:一条日志取代了各种总线,编译时绑定取代了反射,失败台账取代了被确认的失败,Manifest 取代了 OpenAPI 扩展。9.x 服务只需迁移一次,源码有 Agent Skill,数据有工具(从 9.x 迁移)。
下一步:在快速开始中构建一个服务。