限界上下文与 DDD 战略设计¶
30 秒版(开场)¶
架构师先识别 限界上下文(Bounded Context),再讨论部署边界:它首先限定某套领域模型和统一语言在哪个边界内成立。数据所有权、团队和服务拆分通常应与之对齐,但不是 DDD 规定的一上下文一库/一团队/一服务。
3 分钟版(精讲深度)¶
- 是什么:DDD 战略设计 = 子域划分(核心/支撑/通用)+ 限界上下文 + 上下文间关系。
- 为什么:架构师常见要求「电商怎么划域」;没有上下文边界会导致 模型污染(同一词在不同域含义不同,如「用户」在营销 vs 交易)。
- 怎么做:Event Storming/领域 workshop → 识别子域、语言冲突和聚合不变量 → 画 Context Map → 再根据团队与部署约束决定模块/服务边界。跨上下文通过明确契约集成,避免直接依赖对方内部表结构。
10 分钟版(原理 + 图示)¶
flowchart LR
subgraph core[核心域:交易]
Order[订单上下文]
Pay[支付上下文]
end
subgraph support[支撑域]
Inv[库存上下文]
Promo[营销上下文]
end
subgraph generic[通用域]
Notify[通知上下文]
end
Order -->|Customer-Supplier| Pay
Order -->|ACL| Inv
Promo -->|Conformist| Order
Order -->|Published Language 事件| Notify
子域分类(必讲)
| 类型 | 投入 | 示例 |
|---|---|---|
| 核心域 | 自研、最优团队 | 交易、定价 |
| 支撑域 | 必要但非差异化 | 库存、报表 |
| 通用域 | 买/开源 | 短信、对象存储 |
聚合与一致性边界
- 聚合根是唯一事务入口(如
Order改状态,不直接改OrderLine外库字段) - DDD 通常鼓励单事务修改一个聚合,并用事件协调其他聚合;但这不是“跨聚合永远只能最终一致”的物理定律,边界应由业务不变量和存储能力决定
- Go 实现:领域层纯 struct + repo 接口;应用层 orchestrate;基础设施层 GORM/MySQL
上下文映射关系(常考)
| 关系 | 含义 |
|---|---|
| Shared Kernel | 共享少量模型(慎用,耦合高) |
| Customer-Supplier | 下游依赖上游 SLA |
| ACL 防腐层 | 翻译外部模型,保护内部域 |
| Open Host Service | 对外统一 API/事件 schema |
生产场景¶
- 大促前划域:营销临时需求不能污染订单核心模型 → 营销上下文通过 ACL 调订单
- 多 BU 合并:两套「会员」定义 → 先统一 Published Language,再渐进合并
- Go 单体先模块化:按 package/context 分包,再物理拆服务(见 S-SOL-02)
排查与工具¶
- Event Storming 贴纸墙、Miro 上下文图
- 架构决策记录 S-ARCH-20 ADR
- 代码:
internal/order/vsinternal/billing/包边界审查
架构取舍¶
| 过度拆分 | 不足拆分 |
|---|---|
| 分布式事务爆炸 | 单体部署风险 |
| 团队沟通成本 | 领域模型互相践踏 |
何时不强行 DDD:CRUD 后台、PoC、生命周期 < 1 年的项目。
深挖问答¶
- 聚合多大合适? → 一次事务内保持一致;通常 1 个根 + 少量实体。
- 和微服务一一对应吗? → 不要求。一上下文可由多个协作服务实现,也可先作为模块化单体;更重要的是不要让一个服务混合彼此冲突的领域模型。
- Go 没有继承怎么做 DDD? → 组合 + 接口;聚合根方法封装不变量。
- 与 S-ARCH-14 关系? → 14 讲拆不拆;本题讲 按什么拆。
反模式与事故¶
- 按技术层拆(user-dao-service)→ 改需求跨全栈发版
- 多上下文直接共同读写同一内部表、绕过契约 → 模型与发布强耦合;物理共库不必然错误,但必须有清晰 ownership 和访问边界
- 贫血模型 → 业务规则散落在 Gin handler,架构师无法治理
代码示例¶
// 订单聚合根(示意)
type Order struct {
ID string
Status OrderStatus
Lines []Line
}
func (o *Order) Pay() error {
if o.Status != StatusPending {
return ErrInvalidTransition
}
o.Status = StatusPaid
return nil
}