适用读者:要决定"系统拆几个服务/几个模块"的架构师;被"微服务拆过头"或"单体太乱"两头夹击的技术负责人。
前置:S18-J06(战术建模)、S18-J05(度量)。分布式协作机制见 S17。
覆盖旧文:A-1534(超市案例与业务案例、用例图、领域事件、边界划分)、A-1533(战略设计段)、A-1536(问题碰撞段,4 点)——合计 69 点;另有 3 张 0 字未发布卡(A-1532 架构演进、A-1539 账户域、A-1540 交易域)按missing_source登记,提纲见 §4。
扩展点:E-S18-014(事件风暴产出物)、E-S18-015(上下文划分信号)、E-S18-016(上下文映射九种关系)。
上一篇:S18-J06 · DDD 战术建模:实体、值对象、聚合
下一篇:S18-A02 · DDD 工程结构与脚手架。
1. 一句话结论
限界上下文的边界不是"功能清单"划出来的,是"语言 + 团队 + 一致性要求 + 变更频率"四个信号交集划出来的;划完之后真正的工作量不在拆分,而在"上下文之间怎么协作"——这就是上下文映射。
A-1533 对战略设计的定义值得先立在这里:战略设计"以应对复杂的业务需求,通过抽象、分治的过程,合理拆分为独立的多个微服务,从而分而治之"。紧接着它给了评价标准:"与之评价拆分的是否合理,则是在需求开发上线时候,是否每次都大量操作多个微服务开发和上线。这样的战略设计是一种失败的微服务单体设计。"
这句话是本文最重要的一句判据:如果一个小需求上线要同时改 5 个服务、发 5 次版,你的拆分就是失败的——不管架构图画得多漂亮。原文引的是 Justin Etheredge 的《You're not actually building microservices》:"少数几个中等规模单体,周围环绕着一个服务生态系统,这更有意义"。
1.1 四个划分信号(E-S18-015)
| 信号 | 问法 | 划出去的迹象 | 不该划的迹象 |
|---|---|---|---|
| 语言 | 同一个词在两边含义相同吗? | "订单"在履约侧指"履约单",在交易侧指"支付单" | 两边都用同一张表、同一套字段名 |
| 团队 | 边界是否与团队边界重合? | 两个团队为一个模块的所有权打架 | 一个 3 人团队维护 8 个服务 |
| 一致性 | 这些数据必须强一致吗? | 可以接受"最终一致 + 补偿" | 必须同一事务(→ 同一上下文/聚合) |
| 变更频率 | 两边改动节奏差多少? | 促销规则一周三改,账户体系一年一改 | 两边总是一起改 |
四个信号里,"语言"最难但最有效。DDD 的"统一语言"不是文档要求,是拆分工具:当你发现同一个词在两个子域里指两件事,那里就是一条边界(这也是 Evans 在《领域驱动设计》里"Ubiquitous Language"与"Bounded Context"两章连在一起讲的原因,见 BoundedContext )。
Conway 定律的现实含义:A-1536 引用它——"系统结构会与组织的沟通结构保持一致"。所以你无法设计出与团队结构矛盾的边界,只能调整其中之一。这一点决定了:拆分方案必须先和排班表对一遍。
2. 事件风暴:从便签到边界(E-S18-014)
A-1534 把建模过程讲得很具体,是可执行的:
| 步 | 动作 | 产出物 | 谁参加 |
|---|---|---|---|
| 1 | 画用例图(用户与系统交互的最简表示) | 参与者、用例清单 | 产品 + 研发 |
| 2 | 穷举领域事件(过去时) | 黄色便签墙 | 产品、测试、架构师一起 |
| 3 | 给事件补决策命令(用户动作) | 蓝色便签 | 同上 |
| 4 | 给命令补领域对象(棕色便签) | 领域对象清单 | 研发主导 |
| 5 | 标外部系统(粉色)、只读模型(绿色) | 依赖清单 | 架构师 |
| 6 | 串业务流程(红色)把命令连到事件 | 流程链 | 全员 |
| 7 | 圈边界,再单独出领域划分图 | 限界上下文草图 | 架构师收敛 |
四色(实际是六色)便签的含义,A-1534 原文给全了:
- 蓝色 决策命令:用户发起的行为动作(开始签到、开始抽奖、查看额度);
- 黄色 领域事件:过去时态(签到完成、抽奖完成、奖品发放完成),描述"终态";
- 粉色 外部系统:需要调外部接口才能完成的流程;
- 红色 业务流程:串联命令到事件;简单场景可命令直连事件;
- 绿色 只读模型:只读数据,不写库;
- 棕色 领域对象:每个命令都对应一个领域对象。
一句话概括整张图左下角的语义:"一个用户,通过一个策略命令,使用领域对象,通过业务流程,完成 2 个领域事件,调用 1 次外部接口"——建模就是把这些节点找齐。
超市案例(原文的类比,用来给非技术同事解释):媳妇下达指令=决策命令;推着购物车进超市=带实体进入流程;装满的购物车=领域事件(任务完成);手里的烟=第二个领域事件(一次行为可完成多个事件);出门→地铁→采购→打车回家=领域编排。
产出物清单(E-S18-014):一场 2 小时的事件风暴,合格的产出是 5 样东西,缺一样就说明没做完:
- 领域事件清单(去重、过去时、按业务流排序);
- 命令—事件映射(每条命令能引发哪些事件);
- 领域对象清单(谁承载这条命令的状态);
- 外部系统清单(哪些流程要跨出去);
- 边界草图(圈出的上下文 + 每两个上下文之间的关系类型)。
第 5 项最容易被跳过,而它恰恰是战略设计的交付物。没有它,风暴会退化成"贴了一墙便签的头脑风暴"。
真实案例的规模感:A-1534 的营销抽奖需求包含活动配置、签到奖励、活动账户、抽奖策略(责任链 + 规则树)、库存扣减、阶梯抽奖共 9 条业务规则。这种复杂度才值得开一场风暴;一个 CRUD 后台不值得。
3. 上下文映射:九种关系(E-S18-016)
边界划完,接下来是"两边怎么协作"。Evans 在《领域驱动设计》第 14 章("Maintaining Model Integrity")给了这组模式,BoundedContext 条目也复述了它们:
| 关系 | 中文 | 语义 | 谁承担耦合 | 适用 |
|---|---|---|---|---|
| Partnership | 合作关系 | 两边同生共死,一起改一起发 | 双方 | 两个团队目标一致、节奏同步 |
| Shared Kernel | 共享内核 | 共享一小块模型与代码 | 双方(需变更评审) | 模型高度重叠且稳定 |
| Customer/Supplier | 客户-供应商 | 下游有话语权,上游为其排期 | 上游 | 上下游关系明确、可协商 |
| Conformist | 顺从者 | 下游照抄上游模型,不改 | 下游 | 上游强势、下游无谈判力 |
| Anticorruption Layer | 防腐层 | 下游加一层翻译,隔离上游模型 | 下游 | 上游模型脏/易变(最常用) |
| Open Host Service | 开放主机服务 | 上游提供稳定协议供多方使用 | 上游 | 一供多需(对外 API) |
| Published Language | 发布语言 | 用共享的、文档化的语言(如 JSON Schema、行业标准) | 双方 | 跨组织、跨版本 |
| Separate Ways | 各行其道 | 不集成,各做各的 | 无 | 集成成本 > 收益 |
| Big Ball of Mud | 大泥球 | 反模式:无边界 | — | 只作为"要逃离的对象" |
三条实战判据:
- 默认选防腐层(ACL)。只要上游不是你控制的(第三方、遗留系统、另一个团队),就加翻译层。A-552 的"脆弱点隔离"就是同一思想在代码层的版本——把"会变的"关进一个小房间。
- Shared Kernel 是最高风险的关系。共享代码意味着共享变更节奏;只在"模型确实同一套且双方都能守评审"时用。多数团队的 Shared Kernel 最后变成"共享泥球"。
- Separate Ways 要主动选。很多"集成"其实是政治需求而非业务需求;能量化"集成成本 vs 收益"时,不集成往往是对的。
画法建议:上下文映射图不要画成"服务调用图"。服务调用图回答"谁调谁",上下文映射图回答"谁迁就谁的模型"。两者信息量完全不同:ACL 在调用图上是"多一跳",在映射图上是"下游承担翻译成本"。
4. 案例:账户域与交易域(补两张缺卡的提纲)
素材里 A-1539(DDD 架构场景-账户域)、A-1540(DDD 架构场景-交易域)未发布、正文 0 字,A-1532(DDD 架构演进)同样为空。三张卡都登记为 missing_source。按题名与原书大纲(A-1527 的"系统架构(5)"列出了 MVC / DDD / MVC2DDD / 账户域 / 交易域),这里给出可执行的提纲,推断内容明确标注,等原文补齐后替换。
4.1 账户域(推断提纲)
| 项 | 内容 |
|---|---|
| 聚合 | Account(聚合根:账户号、状态、余额快照)、CreditLine(授信额度,独立聚合,用 accountId 引用) |
| 不变式 | 余额不得为负;额度占用 ≤ 授信额度;状态迁移 开户→激活→冻结→销户 单向 |
| 领域事件 | AccountOpened、CreditGranted、LimitAdjusted、AccountFrozen |
| 与交易域的关系 | 交易域是下游(Customer),账户域提供 OHS + Published Language(账户/额度查询 API);交易域侧加 ACL 把"账户模型"翻成"下单可用额度" |
| 关键取舍 | 余额"实时扣减"还是"额度预占 + 事后结算"——前者强一致、吞吐低;后者最终一致、需补偿 |
4.2 交易域(推断提纲)
| 项 | 内容 |
|---|---|
| 聚合 | Order(根)、Payment(独立聚合,用 orderId 引用)、Refund |
| 不变式 | 已支付订单不可改金额;退款总额 ≤ 支付总额;状态机 CREATED→PAID→SHIPPED→CLOSED |
| 领域事件 | OrderPlaced、OrderPaid、OrderShipped、Refunded |
| 跨上下文协作 | 下单 → 账户域预占额度(同步,带超时);支付成功 → 发 OrderPaid(outbox)→ 履约域消费 |
| 关键取舍 | 支付回调幂等(见 A04 §4);订单状态机由谁驱动(交易域自己 vs 独立状态机服务,见 A04 §3) |
为什么要给出"推断"而不是留空:因为一张 0 字卡如果不给提纲,它在验收表里就只是一行 missing_source,读者不知道缺什么。给出提纲 + 标注推断,等于把缺口变成待办。
5. 评审清单(战略设计)
一个拆分方案上评审会,用下面 12 条过一遍,每条都要给出具体证据,不接受"我觉得":
| # | 检查项 | 通过标准 |
|---|---|---|
| 1 | 每个上下文有没有自己的统一语言表? | 至少 10 个词,且与其他上下文冲突的词已标注 |
| 2 | 边界与团队边界是否重合? | 一个上下文 ≤ 1 个团队负责 |
| 3 | 跨上下文调用链最长几跳? | 同步链 ≤ 3 跳,否则加异步/聚合服务 |
| 4 | 每个跨上下文关系是否标了类型? | 九种之一,不能是"随便调" |
| 5 | 是否默认加了 ACL? | 上游非自控则必须 |
| 6 | 强一致需求是否已收敛在同一聚合? | 同一事务内的写不跨聚合 |
| 7 | 一次需求上线要动几个服务? | ≤ 2;>3 说明边界错 |
| 8 | 数据所有权是否唯一? | 一张表只有一个上下文写 |
| 9 | 失败语义是否定义? | 超时/重试/降级/补偿各有归属 |
| 10 | 演进路径是否写了? | 有"先单体后拆分"或"绞杀者"路线(见 A03 §4) |
| 11 | 是否有反腐蚀测试? | ACL 有契约测试,上游改字段能被测出 |
| 12 | 拆分收益是否量化? | 至少一项:发布频率、故障隔离、团队并行度 |
6. 常见误区
| 误区 | 真相 |
|---|---|
| "一个上下文 = 一个微服务" | 可以是一个模块、一个包、一个单体里的子系统;部署单元 ≠ 边界 |
| "拆得越细越解耦" | 拆过头的成本是"每次上线动 N 个服务",原文直接称之为失败的微服务单体 |
| "上下文映射就是调用图" | 映射图描述"谁迁就谁的模型",调用图描述"谁调谁" |
| "统一语言是写文档" | 是建模工具:词义冲突处即边界 |
| "共享内核能省事" | 共享的是变更节奏,通常变成共享泥球 |
| "事件风暴就是开会贴便签" | 没有第 5 项产出(边界草图)的风暴等于没开 |
7. 自测
- 你系统里"订单"这个词,在几个团队口中含义不同?这就是几条边界?
- 一次需求上线要改几个服务?如果 >3,先改边界还是先改流程?
- 上游是第三方时,你选 Conformist 还是 ACL?给出成本对比。
- 为什么 Shared Kernel 的风险高于 Customer/Supplier?
- 事件风暴的第 5 项产出是什么?缺了会怎样?
- 你怎么向老板解释"不集成"(Separate Ways)是合理选择?
下一篇
S18-A02 · DDD 工程结构与脚手架
边界划完,代码怎么摆?下一篇讲六边形/洋葱/COLA 三个骨架的共同点、分层与 Maven 多模块的对应、依赖方向的可执行校验(ArchUnit 实测,含反向违规验证),以及脚手架(archetype)怎么落地。
本篇属于系列《S18 · 架构方法与领域建模》。旧文来源:A-1534、A-1533(战略段)、A-1536(问题碰撞段);A-1532/A-1539/A-1540 为 0 字未发布卡,登记 missing_source 并在 §4 给出推断提纲。上下文映射九关系口径出自 Evans《领域驱动设计》第 14 章与 Fowler bliki。