架构与整洁代码(三):领域驱动设计读书笔记——从概念到架构实践
融合《领域驱动设计》(蓝皮书)和《实现领域驱动设计》(红皮书)两本经典著作的系统性读书笔记。从概念理解到架构实践,以电商平台为案例,详细讲解DDD的战略设计和战术设计,帮助中级开发者掌握领域驱动设计的核心思想和落地方法。
目录
一、引言
为什么需要 DDD?
在软件开发中,我们经常遇到这样的困境:
这些问题表面上是「代码难懂」,根因却常常是模型与协作方式没有随业务一起演进:需求一变就加接口、加表字段,却很少问「领域里的概念是否变了、边界是否该调整」。不同团队的表现形式不同——有的是接口爆炸,有的是报表与线上一套规则、运营另一套口径——但症结类似:缺少共享且精确的语言与结构。
业务复杂性:代码无法清晰表达业务意图
- 一个简单的”下单”功能,代码散落在多个 Service 中,很难理解完整的业务流程
- 业务规则隐藏在 SQL、if-else 堆砌中,修改一个规则需要改动多处代码
- 新人接手项目,看了一个月代码还是不懂业务
- 例如:促销叠加「满减 + 券 + 会员价」时,折扣计算分散在订单服务、营销配置和后台任务里,很少有人能一步说清成交价是如何算出来的。
- 例如:部分退款、换货、补发等变体流程各自加分支,领域概念(如「履约」「可退金额」)从未在代码里显式命名,排查问题只能靠打断点。
团队沟通:技术与业务的鸿沟
- 产品说”用户下单后锁定库存”,开发理解成”创建订单后更新库存状态”,两者不是一回事
- 技术术语污染业务讨论:”OrderEntity”、”OrderDTO”、”OrderVO”,业务专家听不懂
- 需求评审会变成”翻译大会”
- 例如:业务口中的「锁库」可能指预留、冻结或可售量扣减;开发实现的却是「下单后改库存状态字段」。若不共建 Ubiquitous Language(通用语言),接口、报表与客服话术会长期不一致。
代码腐化:随时间推移质量下降
- 最初设计优雅的系统,几年后变成”大泥球”
- 修改一处影响多处,不敢重构
- 技术债累积,维护成本越来越高
- 例如:为赶工期在
OrderService里直接调支付 RPC、写物流表、发消息,边界逐渐模糊后,任何小需求都可能牵一发而动全身,重构成本被无限推迟。
DDD(领域驱动设计)正是为了解决这些问题而生。
这些问题往往不是「再多写几个 Service」能解决的,而是需要让业务结构在模型里可见:哪些是核心域、边界画在哪里、跨团队协作时用什么语言描述规则。后文会反复用订单案例,把抽象概念落到可评审、可实现的粒度。
两本书的定位
蓝皮书:《领域驱动设计:软件核心复杂性应对之道》
- 作者:Eric Evans,2003年
- 地位:DDD 的奠基之作,建立了完整的概念体系
- 特点:
- 偏理论,概念性强
- 战略设计讲得深入(限界上下文、上下文映射)
- 战术模式作为基础介绍
- 适合:建立 DDD 的完整认知体系
- 强项:帮你把「限界上下文」「上下文映射」「战略精炼」等概念串成一张地图,避免只见战术模式、不见整体结构。
- 何时读:准备做系统拆分、治理大型单体或与产品共建领域语言之前;若团队连聚合、实体都还没概念,可先扫战术章节再回读战略部分。
- 阅读预期:部分案例偏传统企业信息化语境,初学者可主动把叙事换成互联网交易场景;本文以订单为主线,正是为了降低这种「时代感错位」带来的距离。
红皮书:《实现领域驱动设计》
- 作者:Vaughn Vernon,2013年
- 地位:蓝皮书的实践补充,被称为”IDDD”
- 特点:
- 偏实战,大量代码示例
- 战术设计讲得细致(尤其是聚合设计)
- 融入了现代实践(事件驱动、CQRS、微服务)
- 适合:学习如何落地 DDD
- 强项:聚合设计、应用服务、领域事件、有界上下文落地的代码组织方式写得很细,适合对照自己的项目做 checklist 式自查。
- 何时读:已经在做模块化或微服务、需要具体的类与包结构参考时;若尚无蓝皮书里的战略概念,建议先建立「上下文」与「映射」的直觉,再读红皮书战术细节会更省力。
- 阅读预期:示例代码量较大,不必逐行跟写;更建议用「对照清单」的方式记下:聚合边界、应用服务职责、领域事件发布点是否与你的模块相吻合。
两本书的关系:
- 蓝皮书建立认知框架,红皮书填充实现细节
- 蓝皮书告诉你”是什么”和”为什么”,红皮书告诉你”怎么做”
- 建议先读蓝皮书的战略设计,再读红皮书的战术设计
- 只读红皮书容易「只见分层与示例、不见为何如此切分」;只读蓝皮书又容易停在概念层,落地时缺少参照,因此两本交叉阅读更稳。
- 时间极紧时,可优先:蓝皮书中的限界上下文与上下文映射 + 红皮书中的聚合、领域事件与集成章节,再按项目痛点回补其余篇目。
本文的阅读地图
如何使用这篇笔记:
系统学习(推荐初学者)
- 按顺序阅读:引言 → 核心概念 → 战略设计 → 战术设计 → 架构落地 → 实施指南
- 每个章节都有电商案例和代码示例
- 预计阅读时间:2-3小时
- 建议边读边整理一页「术语表」:把本文出现的领域词与你们业务里的说法对齐,读后能直接用于评审或设计文档。
- 遇到战略与战术两章都出现的概念(如上下文与聚合边界),可用订单案例串起来,避免孤立记忆定义。
- 每读完一章试写三句话:本章针对哪种业务痛点、对应哪种 DDD 手段、订单案例里哪一步能印证;若写不出,通常说明还停留在「认名词」而非「能讨论」的阶段。
快速查阅(熟悉概念者)
- 跳转到具体章节查阅概念定义
- 使用 Q&A 部分快速找到问题答案
- 查看架构图和代码示例
- 可按关键词检索:限界上下文、聚合根、领域事件、防腐层等,把本文当词典;若与 30 号文对照阅读,可快速定位「模式组合」与「纯 DDD 概念」的边界。
- 查阅时优先看小节标题与加粗定义,再回到订单例子验证是否理解一致。
项目应用(实战导向)
- 先读”实施指南”了解何时用 DDD
- 再读”战略设计”了解如何划分上下文
- 最后读”战术设计”了解如何设计聚合
- 建议在迭代会上挑一个真实争议点(如「已支付未发货能否取消」),试用本文的通用语言与上下文划分方式推演一遍,再决定是否在代码里引入对应边界。
- 落地时不必一次上齐所有模式:先稳定上下文边界与聚合不变量,再逐步引入事件与集成方式。
与 30 号文章的关系:
本文专注于 DDD 本身,而 41-acc-clean-arch-ddd-cqrs.md 讲解了 DDD 与 Clean Architecture、CQRS 的关系。两篇文章互为补充:
- 30 号文章:架构模式的对比和组合
- 本文:DDD 的深入讲解和实践
- 若你关心「分层是否必须」「CQRS 是否与 DDD 绑定」这类问题,可先在 30 号文中看模式对比与取舍,再回到本文把领域模型与上下文讨论清楚,避免把架构风格误当成领域本身。
- 若团队已在实践 Clean Architecture,可把本文的聚合、领域事件看作内层规则如何暴露给外层用例;上下文映射则对应跨边界时如何防止外层概念泄漏进核心模型。
与电商系列文章的关系:
本文使用电商场景作为贯穿案例,与以下文章形成呼应:
- 20-ecommerce-overview.md - 电商系统概览
- 21-ecommerce-listing.md - 商品列表
- 22-ecommerce-inventory.md - 库存系统
- 29-ecommerce-payment-system.md - 支付系统
- 阅读系列文时,可对照本文中的上下文划分,看同一能力在概览、列表、库存、支付等文中分别落在哪个子域、由哪个团队主责。
- 若你当前只负责其中一条链路(例如支付回调),仍建议先浏览订单全路径,再深入自己的上下文,避免局部优化破坏全局一致性。
贯穿全文的电商案例
为了让概念更具体,本文使用电商订单场景作为主线案例:
业务场景:用户在电商平台下单购买商品
一条常见路径是:浏览商品 → 加购 → 结算 → 创建订单 → 锁/扣库存 → 发起支付 → 支付成功 → 通知履约与物流。途中会出现超时关单、库存不足、支付失败回滚、部分退款等分支;这些分支正好暴露跨上下文协作与领域规则该放在哪里讨论。
涉及的上下文:
- 订单上下文:订单生命周期管理(创建、取消、状态流转与订单级不变量)
- 库存上下文:库存扣减和锁定(可售量、预留与释放的语义需与订单语言对齐)
- 商品上下文:商品信息查询(价格、规格、上下架与订单快照如何解耦)
- 支付上下文:支付流程处理(支付单、渠道回调与订单状态如何映射)
- 物流上下文:物流单管理(出库、揽收与订单完成如何衔接)
为什么选择订单:
- 业务流程复杂:涉及状态机、跨上下文协调
- 聚合设计典型:Order 是经典的聚合根示例
- 实战价值高:几乎所有电商平台都有订单系统
- 容易理解:读者对电商下单流程都有直观认知
- 与日常经验贴近:秒杀、运费、发票、赠品等变体容易从生活场景切入讨论,降低领域建模的入门门槛。
- 不变量丰富:例如「已支付总额与明细一致」「取消后库存必须按规则释放」,适合讲清聚合内一致性边界。
- 集成点多:订单与库存、支付、物流之间的同步与异步协作,适合演示 Anti-Corruption Layer(防腐层)、领域事件等集成手段而不显得牵强。
案例在全文中的用法(读到对应章节时可对照本节路径思考):
- 战略设计:把订单、库存、支付、物流画成限界上下文,讨论上下游、合作关系与防腐层应落在哪里,避免「一个订单大表走天下」。
- 战术设计:以 Order 为聚合根,讨论订单明细、金额、状态流转中哪些规则必须同事务一致提交,哪些宜通过领域事件异步通知其他上下文。
- 架构落地与实施:当出现秒杀、回调延迟、对账不一致等工程现实时,如何把补丁写回模型(而不是在模型外再堆一层 if-else),是后文会反复对照订单场景说明的重点。
读后续章节时可带着的问题(答案分散在战略、战术与落地各节):
- 用户点击「提交订单」的瞬间,订单侧与库存侧各自必须成立的不变量是什么?哪一侧应是权威?
- 支付回调到达时,是更新订单状态为主,还是以支付上下文的状态为主再同步到订单?如何避免双重写入与乱序?
- 超时关单、库存释放、支付关单三者若由不同定时任务驱动,如何用事件或显式用例描述保证业务上「只关一次、关得对」?
从战略设计到战术设计,我们都会用订单场景来说明概念;你在各节看到的示意图与伪代码,都可以尝试替换为自己系统的名词做一遍「语言校验」。
可选小练习:用非技术语言写清你们系统里「下单成功」对顾客、客服、财务分别意味着什么,再与上文订单路径对照;标出含义不一致或一词多义的词——它们往往就是限界上下文与通用语言工作的起点。
二、核心概念
在深入战略设计和战术设计之前,我们需要先建立 DDD 的核心术语体系。这些概念是理解后续内容的基础。
2.1 统一语言(Ubiquitous Language)
定义:团队(包括开发者、业务专家、产品经理)共同使用的语言,贯穿需求分析、设计、代码实现的全过程。
价值:
- 消除「翻译成本」:业务说「下单」,代码里也是
PlaceOrder,而不是CreateOrderEntity - 提高沟通效率:技术与业务用同一套术语讨论问题
- 代码即文档:代码能被业务专家读懂
如何建立统一语言:
与业务专家协作
- 事件风暴工作坊:识别领域事件和命令
- 术语表维护:记录所有关键概念的定义
- 定期 Review:确保术语使用一致
在代码中体现
- 类名、方法名使用业务术语
- 避免技术术语污染
- 注释用业务语言描述
电商实践案例:
好的命名(体现业务语言):
1 | // 领域事件 |
不好的命名(技术术语污染):
1 | // ❌ 技术味太重 |
术语标准化:
在电商领域,同一个概念可能有多种说法,需要统一:
| 业务概念 | 可能的说法 | 统一后的术语 | 说明 |
|---|---|---|---|
| 用户下单 | 「下单」、「创建订单」、「提交订单」 | PlaceOrder |
选择最符合业务语言的术语 |
| 库存 | 「库存」、「可售库存」、「在途库存」 | AvailableInventory, InTransitInventory |
明确区分不同类型的库存 |
| 订单取消 | 「取消订单」、「关闭订单」 | CancelOrder |
与业务专家确认语义 |
反例:技术术语污染业务
1 | // ❌ 错误示例 |
1 | // ✅ 正确示例 |
统一语言的维护:
- 术语表(Glossary)
创建项目 Wiki 或文档,记录所有关键术语:
1 | ## 订单领域术语表 |
- 领域模型图
用 UML 类图或 Mermaid 图可视化领域模型:
classDiagram
class Order {
+OrderID id
+OrderStatus status
+PlaceOrder()
+Pay()
+Cancel()
}
class OrderItem {
+ProductID productID
+int quantity
+Money price
}
class OrderStatus {
<>
PENDING
PAID
CANCELLED
COMPLETED
}
Order "1" *-- "*" OrderItem
Order --> OrderStatus
要点总结:
- 统一语言不仅仅是命名,而是完整的概念体系
- 业务术语应该贯穿需求、设计、代码的全过程
- 代码即文档,代码应该能被业务专家读懂
- 避免技术术语(Entity、DTO、VO)污染业务语言
2.2 限界上下文(Bounded Context)
定义:模型的明确边界。一个模型只在一个上下文内有效,不同上下文中的同一个词可以有不同的含义。
核心思想:不要追求全局统一的大模型,而是在不同的边界内建立各自的模型。
为什么需要限界上下文:
想象一个电商平台,如果我们试图建立一个「全局统一的商品模型」:
1 | // ❌ 试图建立全局统一模型(注定失败) |
问题:
- 不同上下文关注点不同,但被迫共享一个模型
- 修改任何一个字段都可能影响所有上下文
- 模型越来越臃肿,难以维护
解决方案:限界上下文
在不同的上下文中,建立各自的模型:
商品上下文(关注商品信息):
1 | // 商品上下文中的 Product |
库存上下文(关注库存数量):
1 | // 库存上下文中的 Product(只关注数量) |
订单上下文(关注价格快照):
1 | // 订单上下文中的 OrderItem(保存下单时的快照) |
关键认知:
- 同一个词(「Product」)在不同上下文有不同含义
- 「Product」在商品上下文是聚合根,包含详细信息
- 「Product」在库存上下文只关注数量
- 「Product」在订单上下文只是一个快照
- 不要追求全局统一模型
电商平台的限界上下文划分:
graph TB
subgraph "用户上下文"
A1[用户管理]
A2[地址管理]
A3[权限管理]
end
subgraph "商品上下文"
B1[商品信息]
B2[SPU/SKU]
B3[分类管理]
end
subgraph "订单上下文"
C1[订单生命周期]
C2[订单状态流转]
C3[订单查询]
end
subgraph "库存上下文"
D1[库存管理]
D2[库存扣减]
D3[库存预警]
end
subgraph "支付上下文"
E1[支付单管理]
E2[对接支付网关]
E3[退款处理]
end
subgraph "物流上下文"
F1[物流单管理]
F2[物流跟踪]
end
C1 -->|OrderPlaced 事件| D1
C1 -->|OrderPaid 事件| E1
C1 -->|OrderPaid 事件| F1
各上下文的关注点:
| 上下文 | 核心聚合 | 主要职责 | 「Order」的含义 |
|---|---|---|---|
| 订单上下文 | Order | 订单生命周期管理、状态流转 | 聚合根,包含完整订单信息 |
| 支付上下文 | Payment | 支付流程、对接支付网关 | 只是一个外部引用(订单号) |
| 物流上下文 | Shipment | 物流单管理、轨迹跟踪 | 只是一个外部引用(订单号) |
关键点:
- 每个上下文有明确的边界和职责
- 同一个概念在不同上下文有不同模型
- 上下文之间通过 API 或事件通信,不直接共享数据库
上下文的识别方法:
基于业务能力
- 每个上下文对应一个业务能力
- 例如:浏览商品、下单、支付、发货是不同的业务能力
基于语言边界
- 术语含义发生变化的地方就是上下文边界
- 例如:「商品」在商品上下文和库存上下文中含义不同
基于数据一致性边界
- 需要强一致性的数据放在同一上下文
- 可以最终一致性的数据可以跨上下文
- 例如:Order 和 OrderItem 必须强一致,所以在同一上下文
基于团队结构(康威定律)
- 系统架构会反映组织结构
- 每个团队负责一个或少数上下文
- 例如:订单团队负责订单上下文
要点总结:
- 限界上下文是模型的边界
- 不要追求全局统一模型
- 同一个词在不同上下文可以有不同含义
- 上下文之间通过明确的接口通信
2.3 领域、子域分类
定义:根据业务价值和复杂度,将整个业务领域划分为不同类型的子域。
三种子域类型:
核心域(Core Domain)
- 定义:业务的核心竞争力,最复杂、最有价值的部分
- 特征:差异化优势、复杂业务规则、频繁变化
- 投资策略:自研,精心设计,持续投入
支撑域(Supporting Subdomain)
- 定义:支撑核心域运转,业务特定但不是竞争力
- 特征:必需但不产生差异化、中等复杂度
- 投资策略:简单设计,够用即可
通用域(Generic Subdomain)
- 定义:通用功能,行业标准,可以外购或使用开源
- 特征:无差异化、低复杂度、稳定
- 投资策略:外采或开源,不要重复造轮子
电商平台的子域分类:
核心域(投入 60% 人力):
订单履约
- 为什么是核心域:直接影响 GMV 和用户体验
- 复杂度:状态机、工作流、异常处理、跨上下文协调
- 投资策略:20 人团队,精心设计,持续优化
库存管理
- 为什么是核心域:防止超卖,影响用户信任
- 复杂度:分布式锁、高并发、实时扣减
- 投资策略:15 人团队,高可用设计
推荐算法
- 为什么是核心域:提升转化率的关键
- 复杂度:机器学习、实时计算、A/B 测试
- 投资策略:15 人团队,算法持续迭代
支撑域(投入 30% 人力):
- 用户管理:标准 CRUD,5 人
- 地址管理:地址解析和验证,3 人
- 优惠券系统:规则引擎,5 人
- 客服系统:工单管理,5 人
通用域(投入 10% 人力,主要外采):
- 消息通知:阿里云短信、SendGrid
- 文件存储:OSS、S3
- 日志系统:ELK Stack
- 监控告警:Prometheus + Grafana
判断标准矩阵:
| 子域 | 业务价值 | 复杂度 | 变化频率 | 差异化 | 类型 |
|---|---|---|---|---|---|
| 订单履约 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 核心域 |
| 库存管理 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | 核心域 |
| 推荐算法 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 核心域 |
| 优惠券 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | 支撑域 |
| 用户管理 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐ | 支撑域 |
| 消息通知 | ⭐⭐ | ⭐ | ⭐ | ⭐ | 通用域 |
| 文件存储 | ⭐ | ⭐ | ⭐ | ⭐ | 通用域 |
要点总结:
- 明确区分核心域、支撑域、通用域
- 核心域投入最多资源,精心设计
- 通用域优先外采,不要重复造轮子
- 每年重新评估,根据业务战略调整
2.4 上下文映射(Context Mapping)
定义:描述不同限界上下文之间的关系和集成方式。
价值:
- 明确团队协作方式
- 指导系统集成策略
- 管理上下文间的依赖关系
常见映射模式:
1. 共享内核(Shared Kernel)
定义:两个上下文共享一部分代码或模型。
适用场景:紧密协作的两个团队
电商案例:订单和库存共享商品基础信息
1 | // 共享内核:商品基础信息 |
优点:减少重复,保持一致性
风险:
- 修改需要双方协调
- 可能导致隐式耦合
- 团队自治性降低
最佳实践:
- 共享内核应该非常小
- 明确共享的范围
- 建立清晰的变更协调机制
2. 客户-供应商(Customer-Supplier)
定义:下游(客户)依赖上游(供应商),上游需要考虑下游的需求。
电商案例:订单上下文(客户)依赖商品上下文(供应商)
1 | // 商品上下文提供的 API(供应商) |
上游职责:
- 提供明确的 API 契约
- 考虑下游的需求(批量查询、性能要求)
- 保证 API 稳定性和版本兼容
下游职责:
- 明确表达需求
- 不要直接操作上游的数据库
3. 防腐层(Anti-Corruption Layer, ACL)
定义:下游建立隔离层,避免上游变化影响自己的领域模型。
适用场景:
- 对接外部系统(第三方支付、物流)
- 对接遗留系统
- 上游频繁变化
电商案例:订单上下文对接第三方支付(微信、支付宝)
1 | // 领域层:定义统一的支付接口 |
关键点:
- 领域层定义接口(依赖倒置)
- 防腐层在基础设施层实现
- 只暴露领域需要的抽象,隐藏第三方细节
- 不同支付渠道实现同一接口
4. 开放主机服务(Open Host Service, OHS)
定义:上游提供标准化的 API 服务,供多个下游使用。
电商案例:商品上下文提供标准的 REST API
1 | # 商品查询 API |
优点:
- 一对多的服务提供
- 统一的接口标准
- 易于集成
5. 遵奉者(Conformist)
定义:下游完全遵循上游的模型,不做转换。
适用场景:上游非常强势或无法改变(如税务系统、银行系统)
电商案例:对接税务系统,必须使用税务系统的数据格式
电商平台的完整上下文映射:
graph LR
A[订单上下文] -->|Customer-Supplier| B[商品上下文]
A -->|ACL| C[支付上下文
第三方]
A -->|Customer-Supplier| D[库存上下文]
A -->|Customer-Supplier| E[物流上下文]
B -->|OHS| F[搜索服务]
D -->|Shared Kernel| B
要点总结:
- 上下文映射明确了上下文间的集成方式
- 不同模式适用于不同场景
- 防腐层用于对接外部系统
- 客户-供应商模式最常用
三、战略设计
战略设计回答的是「边界在哪里、团队如何协作、集成关系如何表达」——在写聚合与仓储之前,先把子域类型、限界上下文与上下文映射说清楚,战术设计才有落点。本节仍以电商订单链路为主线,说明如何从业务中识别子域、如何划分上下文,以及如何把防腐层、共享内核与客户-供应商关系落到代码与 API 上。
3.1 如何识别和划分子域
子域(Subdomain)分类(核心域 / 支撑域 / 通用域)不是贴标签比赛,而是投资策略:决定把最优秀的人力和设计精力投在哪里,哪里可以「够用即可」,哪里应外采或开源。判断时建议同时看四个维度,而不是只看「是否赚钱」。
识别方法:四维度评估
每个子域从以下四个维度评估:
业务价值(Business Value)
- 高:直接影响核心竞争力和收入
- 中:支撑业务运转,但不是差异化优势
- 低:通用功能,不产生业务价值
复杂度(Complexity)
- 高:业务规则复杂,需要深度领域建模
- 中:有一定业务逻辑
- 低:简单 CRUD
变化频率(Change Frequency)
- 高:业务规则频繁变化
- 中:偶尔调整
- 低:基本稳定
差异化(Differentiation)
- 高:行业独有,竞争优势所在
- 中:行业通用做法,但有特色
- 低:行业标准,无差异化
判断标准矩阵
| 子域类型 | 业务价值 | 复杂度 | 变化频率 | 差异化 | 投资策略 |
|---|---|---|---|---|---|
| 核心域 | 高 | 高 | 高/中 | 高 | 自研,精心设计,持续投入 |
| 支撑域 | 中 | 中 | 中/低 | 中/低 | 自研,简单设计,够用即可 |
| 通用域 | 低 | 低/中 | 低 | 低 | 外采/开源,不要重复造轮子 |
电商平台子域完整分析
以下用星级(⭐)做相对刻度,便于工作坊对齐口径;重点是相对比较与结论,而非绝对分数。
核心域候选:
订单履约
- 业务价值:⭐⭐⭐⭐⭐(直接影响 GMV)
- 复杂度:⭐⭐⭐⭐(状态机、工作流、异常处理)
- 变化频率:⭐⭐⭐⭐(业务规则频繁调整)
- 差异化:⭐⭐⭐⭐(履约效率是竞争力)
- 结论:核心域
库存管理
- 业务价值:⭐⭐⭐⭐⭐(防止超卖、保障可售)
- 复杂度:⭐⭐⭐⭐⭐(分布式、高并发)
- 变化频率:⭐⭐⭐(库存策略调整)
- 差异化:⭐⭐⭐⭐(库存周转与准确性影响效率)
- 结论:核心域
推荐算法
- 业务价值:⭐⭐⭐⭐⭐(提升转化率)
- 复杂度:⭐⭐⭐⭐⭐(机器学习、实时计算)
- 变化频率:⭐⭐⭐⭐(算法持续优化)
- 差异化:⭐⭐⭐⭐⭐(推荐质量是核心竞争力)
- 结论:核心域
支撑域候选:
优惠券系统
- 业务价值:⭐⭐⭐(支撑营销活动)
- 复杂度:⭐⭐⭐(规则引擎、计算逻辑)
- 变化频率:⭐⭐⭐(营销策略调整)
- 差异化:⭐⭐(大部分平台都有)
- 结论:支撑域
用户管理
- 业务价值:⭐⭐⭐(必需但不是竞争力)
- 复杂度:⭐⭐(标准用户 CRUD)
- 变化频率:⭐⭐(较稳定)
- 差异化:⭐(行业标准)
- 结论:支撑域
地址管理
- 业务价值:⭐⭐(辅助功能)
- 复杂度:⭐⭐(地址解析、验证)
- 变化频率:⭐(很少变化)
- 差异化:⭐(无差异)
- 结论:支撑域
通用域候选:
消息通知(短信、邮件)
- 业务价值:⭐⭐(必需但通用)
- 复杂度:⭐(调用第三方 API)
- 变化频率:⭐(基本不变)
- 差异化:⭐(完全无差异)
- 结论:通用域 → 建议使用云厂商短信、SendGrid 等服务
文件存储
- 业务价值:⭐(基础设施)
- 复杂度:⭐(标准存储)
- 变化频率:⭐(不变)
- 差异化:⭐(无差异)
- 结论:通用域 → 建议使用 OSS、S3 等服务
日志系统
- 业务价值:⭐(运维必需)
- 复杂度:⭐⭐(日志采集、聚合)
- 变化频率:⭐(不变)
- 差异化:⭐(无差异)
- 结论:通用域 → 建议使用 ELK、Splunk 等
决策工作坊方法(团队协作识别子域)
步骤 1:列出所有子域
- 全员头脑风暴
- 列出系统的所有功能模块
步骤 2:四维度打分
- 每个子域从业务价值、复杂度、变化频率、差异化四个维度打分(例如 1~5 分)
- 团队投票,取平均值或讨论收敛
步骤 3:分类决策
- 根据矩阵判断子域类型
- 边界情况必须团队讨论,避免「默认核心域」
步骤 4:投资策略确定
- 核心域:分配最优秀的人力,精心设计
- 支撑域:够用即可,不过度设计
- 通用域:评估外采方案
电商案例的投资策略(示意):
1 | 核心域(60% 人力): |
常见错误
错误 1:把所有功能都当核心域
- 表现:每个模块都精心设计,过度投入
- 后果:资源分散,真正的核心域得不到足够重视
- 解决:强制排序,最多 3~5 个核心域
错误 2:低估支撑域的重要性
- 表现:支撑域设计太粗糙,成为瓶颈
- 后果:核心域被支撑域拖累(性能、可用性、数据质量)
- 解决:支撑域也要有基本的设计质量与 SLO
错误 3:自研通用域
- 表现:重复造轮子(自建消息平台、日志栈)
- 后果:大量人力花在非差异化能力上
- 解决:优先考虑外采和开源方案
错误 4:子域划分过于静态
- 表现:子域类型一成不变
- 后果:业务战略变化后,投资与组织仍按旧地图走路
- 解决:定期(例如每年)重新评估,与业务战略对齐
决策检查清单
- 是否从业务价值、复杂度、变化频率、差异化四个维度评估?
- 核心域数量是否控制在 3~5 个?
- 核心域是否分配了最优秀的人力?
- 通用域是否优先考虑了外采/开源?
- 投资策略是否与业务战略对齐?
3.2 如何划分限界上下文
限界上下文(Bounded Context)是模型的一致性边界与语言边界:在边界内术语含义稳定、规则可推敲;跨边界则通过映射与集成协作。划分不是一次性的「微服务切分」,而是对业务能力与协作现实的建模。
划分原则
基于业务能力
- 识别核心业务流程
- 按业务能力聚合功能
- 电商案例:浏览商品、下单、支付、发货是不同的业务能力
基于团队结构(康威定律)
- 系统架构反映组织结构
- 每个团队负责一个或少数上下文
- 电商案例:订单团队、商品团队、支付团队
基于数据一致性边界
- 强一致性要求通常落在同一上下文内
- 最终一致性可以跨上下文
- 电商案例:订单与订单项必须强一致;订单与库存可最终一致
基于语言边界
- 不同的业务术语体系
- 术语含义发生变化的地方往往是边界
- 电商案例:「商品」在商品上下文与库存上下文中的含义不同
实战:电商平台的上下文演进
阶段 1:单体架构
1 | [单一应用] |
阶段 2:初步拆分(按功能模块)
1 | [用户服务] [商品服务] [订单服务] [库存服务] |
阶段 3:DDD 上下文拆分(目标形态)
1 | [用户上下文] |
各限界上下文一览(电商示例)
将「阶段 3」中的上下文落到可评审的表格,便于工作坊对齐语言与责任边界;下表为示意,实际项目需替换为你们自己的统一语言与聚合名。
| 限界上下文 | 子域倾向 | 核心聚合(示例) | 主要职责 | 典型对外能力 | 与周边关系(示例) |
|---|---|---|---|---|---|
| 用户上下文 | 支撑域 | User、Address | 账户、认证、地址簿 | 查询用户与地址、地址校验与清洗 | 订单通过 API 拉取收货人与地址 |
| 商品上下文 | 视战略而定 | Product(SPU/SKU)、Category | 类目、商品详情、上下架 | 批量查询基础信息、按 ID 拉取售价与标题快照 | 订单 Customer-Supplier;库存可能共享极小内核 |
| 订单上下文 | 核心域 | Order、OrderLine | 下单、改单、取消、状态机 | 创建订单命令、订单查询、领域事件(已下单/已支付) | 依赖商品、库存、支付、物流、营销 |
| 库存上下文 | 核心域 | Stock、Reservation | 可售量、预留、扣减与释放 | 预留、确认扣减、释放预留 | 订阅订单事件;对商品信息需谨慎共享 |
| 支付上下文 | 支撑域 | Payment、Channel | 支付单、渠道路由、对账配合 | 创建支付、查单、回调处理 | 订单经 ACL 对接三方;内部可再分防腐 |
| 物流上下文 | 支撑域 | Shipment、Tracking | 运单、轨迹、承运商对接 | 创建运单、查询轨迹 | 订单 Customer-Supplier |
| 营销上下文 | 支撑域 | Coupon、Promotion | 券实例、活动规则 | 试算优惠、核销资格校验 | 订单在算价阶段调用 |
使用方式:把表格当作「上下文清单 v0.1」,在评审中不断改列名与关系,直到与团队口语一致;不要在第一次工作坊就追求填满每个单元格。
从单体走向限界上下文的评审问题
在画架构图之前,先用下面一组问题压一遍假设,能减少「按表拆微服务」式的假边界:
- 语言是否分叉:同一词在两个模块是否含义不同?(如「商品」在售前与在库存中)
- 一致性边界:哪些不变量必须同事务、哪些可异步最终一致?
- 变更主体:谁有权修改这条业务数据?跨团队改同一表往往是边界没划清。
- 发布节奏:两侧是否必须独立部署;若永远一起发,拆分紧迫性要重新评估。
- 失败隔离:一侧故障时,另一侧能否降级;若不能,是否应暂时同上下文或强化契约。
- 集成形态:更适合同步 API、异步事件,还是批量对账;这会反过来约束边界。
- 测试策略:能否为单上下文写可重复的领域测试,而不必起全链路集成环境。
- 数据所有权:每个业务表是否有唯一写入所有者;读模型若非所有者,是否走明确定义的查询 API。
落地建议:选 3~5 个最痛的跨模块需求,把它们从「谁改哪张表」还原成「哪两个上下文在协作、用哪种映射」,往往比一次性画全图更有效。
每个上下文建议写清四件事(可放进架构决策记录 ADR):
- 核心聚合:谁是聚合根,哪些不变量必须在同事务内成立
- 主要职责:对外承诺的业务能力(用统一语言写)
- 对外 API:查询、命令、事件的契约形态
- 与其他上下文的关系:客户-供应商、防腐层、共享内核等
架构图:电商平台上下文与协作(C4 风格示意)
graph TB
subgraph "用户上下文"
U1[用户/地址]
end
subgraph "商品上下文"
P1[SPU/SKU/类目]
end
subgraph "订单上下文"
O1[Order 聚合]
end
subgraph "库存上下文"
I1[库存扣减/预留]
end
subgraph "支付上下文"
PY1[支付单/渠道]
end
subgraph "物流上下文"
L1[运单/轨迹]
end
subgraph "营销上下文"
M1[券/活动]
end
O1 -->|Customer-Supplier| P1
O1 -->|Customer-Supplier| I1
O1 -->|ACL| PY1
O1 -->|Customer-Supplier| L1
O1 -->|Customer-Supplier| M1
U1 -->|Customer-Supplier| O1
I1 -.->|Shared Kernel 慎用| P1
拆分决策树
flowchart TD
A[是否有明确的业务边界?]
A -->|是| B[倾向独立上下文]
A -->|否| C[是否有不同的一致性要求?]
C -->|是| D[倾向拆分上下文]
C -->|否| E[是否有不同的团队负责?]
E -->|是| F[倾向拆分,并校对接口与发布节奏]
E -->|否| G[可暂时保留在同一上下文或模块化单体]
拆分的代价与收益
- 何时拆分:边界清晰、不同团队主责、不同发布节奏或技术栈诉求强、调用关系可治理
- 何时合并或暂缓拆分:分布式复杂性(运维、一致性、排障)明显超过拆分收益
- 判断参考:团队认知负荷、调用链复杂度、数据一致性成本、故障爆炸半径
3.3 上下文映射的落地
上下文映射(Context Mapping)把「谁依赖谁、如何集成」说清楚。下面三类在电商集成中最常落地:防腐层、共享内核、客户-供应商。
防腐层(ACL)的设计
场景:订单上下文对接第三方支付(微信支付、支付宝等)。
问题:
- 第三方 API 经常变化
- 不同支付渠道的模型不同
- 不希望外部变化渗透进领域模型
解决方案:在基础设施层建立防腐层,把外部模型与协议隔离在适配器内。
架构示意:
graph LR
OC[订单上下文
领域模型] --> IF[PaymentGateway
领域接口]
IF --> ACL[防腐层适配器]
ACL --> WX[微信支付 API]
ACL --> ALI[支付宝 API]
代码示例:
1 | // 领域层:统一的支付接口 |
关键设计原则:
- 领域层定义接口(依赖倒置)
- 防腐层在基础设施层实现
- 只暴露领域需要的抽象,隐藏第三方细节
- 不同支付渠道实现同一接口,订单领域只依赖接口
共享内核的适用场景和风险
适用场景:
- 两个团队紧密协作
- 有共同且稳定的概念(变化频率低)
- 共享范围可被文档与测试严格约束
电商案例:订单与库存共享极小的商品基础信息(如 SKU ID、商品名称快照策略的约定)——注意:共享的是「契约与少量类型」,不是把两个上下文的数据库绑在一起。
风险:
- 修改需要双方协调
- 容易产生隐式耦合
- 团队自治性降低
最佳实践:
- 共享内核应该非常小
- 明确共享范围与演进规则(版本、兼容策略)
- 建立清晰的协调机制(RFC、联合评审)
客户-供应商关系的 API 设计
场景:订单上下文(客户)依赖商品上下文(供应商)。
API 设计原则:
- 供应商提供明确的 API 契约
- 考虑客户的需求(批量查询、缓存策略、限流与降级)
- 版本管理和兼容性
- 性能与可用性保障(与 SLO 对齐)
电商案例:
1 | // 商品上下文提供的 API(供应商) |
关键点:
- 下游明确表达需求(需要哪些字段、怎样的批量接口)
- 上游设计面向客户的 API,而不是暴露内部表结构
- 避免把内部实现细节泄漏为「公共契约」
3.4 战略设计中的常见问题
问题 1:过度拆分 vs 拆分不足
过度拆分的表现:
- 微服务数量远超团队数量
- 简单功能需要跨多个服务
- 分布式事务或补偿到处都是
- 调用链路复杂,难以排障
电商反例:将订单拆成订单头服务、订单项服务、订单状态服务——下单路径被迫多次远程调用,一致性边界被人为打碎。
拆分不足的表现:
- 单个上下文职责过多
- 团队无法独立演进
- 代码库过大,变更冲突频繁
电商反例:订单、库存、支付、物流混在一个部署单元里却没有模块化边界,最终变成大泥球。
判断标准:
- 团队能否相对独立演进
- 是否有清晰的业务边界与统一语言
- 调用复杂度与故障半径是否可控
问题 2:上下文边界不清晰
表现:
- 职责混乱:订单服务直接操作库存表
- 数据泄露:对外暴露内部存储形态
- 循环依赖:订单依赖库存,库存又依赖订单
解决方案:
- 明确每个上下文的职责边界(写进文档与代码门禁)
- 使用领域事件解耦协作路径
- 禁止跨上下文直接共享数据库
电商案例:
- 错误做法:订单服务直接扣减库存表
- 正确做法:订单发布
OrderPlaced/OrderPaid等事件,库存上下文监听并执行预留或扣减(配合幂等与重试)
问题 3:团队结构与技术架构不匹配
康威定律:系统架构倾向于反映组织的沟通结构。
问题场景:
- 组织按技术栈分层(前端团队、后端团队、DBA 团队)
- 系统却按业务垂直拆分(订单、商品、支付)
- 结果:任何业务功能都要跨多团队排队
解决方案:
- 调整团队结构与架构对齐(业务全栈团队)
- 每个团队覆盖一个或少数上下文的端到端交付
- 明确接口负责人与 SLI/SLO
电商案例:
- 订单团队负责订单上下文的前后端、数据与运维协作界面
- 商品团队负责商品上下文全栈
- 避免「一个需求要拉五个团队开工会」成为常态
问题 4:忽略上下文映射,直接共享数据库
问题:多个服务直接读写同一张业务表。
后果:
- 隐式依赖,难以演进与重构
- 数据一致性与并发语义不清晰
- 无法独立部署与扩缩
解决方案:
- 每个上下文优先独立数据库(或至少独立 schema 与明确所有权)
- 通过 API 或事件集成
- 把映射关系画出来:客户-供应商、防腐层、开放主机服务等
本节小结:
- 子域分类服务于投资策略:先四维度评估,再用矩阵与工作坊收敛
- 限界上下文划分同时尊重业务能力、一致性、语言与团队现实
- 上下文映射要落地到接口与适配器:ACL 隔离外部,共享内核极度克制,客户-供应商要「面向客户设计 API」
- 常见问题的根因多是边界、组织与数据所有权没有对齐
四、战术设计
战术设计回答的是「领域模型在代码里长什么样」:如何用实体与值对象表达概念,如何用聚合画出一致性边界,如何用仓储隐藏持久化,以及何时引入领域服务与领域事件。本节仍以电商订单为主线,给出可直接对照实现的 Go 示例(为可读性会省略部分工程细节,如错误包装与日志)。
4.1 实体(Entity)
概念与特征
实体(Entity)是有唯一标识(Identity)且在时间上延续的对象:同一个订单在状态从「待支付」变为「已支付」之后,仍然是同一张订单。与标识相比,属性值可以变化。
| 特征 | 说明 |
|---|---|
| 唯一标识 | 用 ID 区分实例,而不是靠属性组合 |
| 生命周期 | 创建、变更、归档或删除 |
| 可变状态 | 业务操作会改变状态,但身份不变 |
| 封装规则 | 状态转换应由实体方法约束,避免「随处改字段」 |
贫血模型 vs 充血模型
贫血模型(Anemic Domain Model):struct 只有字段,业务规则散落在 Service 的 if-else 中。优点是上手快;缺点是规则分散、难以测试、模型无法表达「订单知道自己能做什么」。
充血模型(Rich Domain Model):实体持有与身份强相关的行为与不变量,应用层只做编排。DDD 鼓励在复杂核心域采用后者——不是每个 CRUD 模块都要「充血」,但订单、账户、合同这类对象通常值得。
反例:贫血的 Order
1 | // 仅数据载体,业务规则在 Service 中散落 |
问题一眼可见:Status 是裸字符串,取消规则与退款编排挤在应用服务里,订单自身不保证合法状态机。
正例:充血 Order 与状态值对象
1 | type OrderID string |
实体的关键设计原则
- 封装业务规则:哪些状态可以互转,由实体(或值对象)表达,而不是由调用方猜。
- 保证不变性:禁止外部绕过方法直接改关键字段;在 Go 中常用小写字段 + 构造/工厂 + 业务方法。
- 与值对象组合:金额用
Money,状态用OrderStatus,避免魔法字符串与float64金额。 - 领域事件(可选但常见):状态变更时记录事件,供基础设施在事务成功后发布(详见 4.4)。
生命周期(简图)
stateDiagram-v2
[*] --> Pending: CreateOrder
Pending --> Paid: Pay
Pending --> Cancelled: Cancel
Paid --> Shipped: Ship
Paid --> Cancelled: Cancel
Shipped --> [*]
Cancelled --> [*]
4.2 值对象(Value Object)
概念与特征
值对象(Value Object)没有独立身份,靠属性值描述事物;通常不可变,用整体替换表示变更。相等语义是值相等而非引用相等。
| 特征 | 说明 |
|---|---|
| 无标识 | 不需要 OrderID 这类独立 ID |
| 不可变 | 修改返回新实例,不原地改字段 |
| 可替换 | addr2 := addr1.WithStreet("…") |
| 自验证 | 构造失败即非法,避免「无效值对象在系统里传递」 |
何时使用值对象
- 描述性概念:
Money、Address、DateRange、Email。 - 需要值语义:两个「100 CNY」在业务上相等。
- 希望减少无效状态:构造时校验,方法内不破坏不变量。
电商示例:Money
金额用整数分存储,避免浮点误差;运算返回新 Money,不修改接收者。
1 | import "errors" |
电商示例:Address
1 | type Address struct { |
电商示例:OrderItem(作为值对象)
订单行通常不单独生命周期:外部不引用「第 3 行」的持久化 ID,而是由订单聚合统一修改。典型做法是 ProductID + 下单快照(名称、单价)。字段导出便于仓储映射;业务不变量仍由聚合根方法守护。
1 | type ProductID string |
值对象 vs 实体:判断口诀
「若两个对象属性完全相同,业务上是否仍要区分成两个东西?」
- 是 → 实体(两张订单即使金额相同也是不同订单)。
- 否 → 值对象(两张 100 元纸币在记账语义下可互换)。
4.3 聚合(Aggregate)
概念
聚合(Aggregate)是一组具有一致性边界的领域对象:聚合根(Aggregate Root)是对外唯一入口,外部只能通过根来修改内部;根负责维护边界内不变量。聚合也常被视为一个事务边界(在单体或同一数据库内)。
设计原则(摘自《实现领域驱动设计》)
- 在边界内保护业务规则不变量。
- 设计小聚合(Small Aggregates),降低锁竞争与并发冲突。
- 通过 ID 引用其他聚合(Reference by ID),不直接持有其他根的对象图。
- 边界外接受最终一致性:跨聚合用领域事件等方式同步。
聚合结构示意(Order)
flowchart TB
subgraph OrderAggregate["Order 聚合"]
Root[Order 聚合根]
OI[OrderItem 值对象集合]
SA[ShippingAddress 值对象]
ST[OrderStatus 值对象]
TP[Money 总价]
Root --> OI
Root --> SA
Root --> ST
Root --> TP
end
UserAgg[User 聚合]
ProductAgg[Product 聚合]
Root -.->|仅 UserID| UserAgg
OI -.->|仅 ProductID + 快照| ProductAgg
完整示例:Order 聚合根
1 | package example |
说明:
GenerateOrderID仅为示例;生产环境应使用发号器、UUID 或数据库序列,并处理时钟回拨与冲突。
关键设计决策
- **
userID而非*User**:用户是另一聚合,订单事务不应加载整张用户对象图。 OrderItem用快照:价格、名称在下单时刻固化,避免商品改价影响历史订单。- **不持有
*Product**:跨聚合引用用 ID + 快照,而不是 ORM 式「关联对象」。
常见错误
错误 1:聚合过大
1 | // 将库存、支付细节全部塞进 Order —— 事务臃肿、并发差、职责混乱 |
错误 2:跨聚合直接修改
1 | // 支付时直接扣库存:破坏边界,难以拆分服务 |
错误 3:绕过聚合根修改内部集合
1 | // 外部直接改行项目——不变量失守 |
正确做法:由 Order 提供 ChangeItemQuantity 等方法,在根内校验「已支付不可改件数」等规则。
聚合设计检查清单
- 该边界是否需要强一致一起提交?
- 聚合是否足够小,避免「大锁」?
- 是否只通过 ID 连接其他聚合?
- 外部是否只能通过根操作内部对象?
- 跨聚合协作是否首选最终一致(事件、Saga)?
4.4 仓储(Repository)+ 领域服务(Domain Service)+ 领域事件(Domain Event)
4.4.1 仓储(Repository)
仓储是面向聚合的持久化抽象:领域层声明「需要什么」,基础设施层决定「怎么存」。它不同于面向表的 DAO:查询方法宜带业务语义,如「待支付超时订单」。
| 对比 | DAO | Repository |
|---|---|---|
| 视角 | 表与行 | 聚合与不变量 |
| 方法 | Insert / Update |
Save / FindByID |
| 查询 | 通用 CRUD | 语义化查询 + 重建对象图 |
| 依赖方向 | 常泄漏到领域 | 接口在领域,实现在基础设施 |
领域层接口示例:
1 | import "time" |
PostgreSQL 实现(示意):以聚合为单位事务写入订单头与行表,FindByID 负责重建完整 Order。
1 | import ( |
注意:
Order若字段未导出,重建逻辑应放在domain包内的RehydrateOrder(...)工厂中,避免仓储跨包写入私有字段。上文为讲解方便采用同包示意。
4.4.2 领域服务(Domain Service)
领域服务承载不属于单一实体或值对象、但又纯粹属于领域的逻辑:通常无状态、不直接依赖数据库。典型场景:跨多个聚合的规则、或需要多种输入对象的计算。
与应用服务分工:
| 层次 | 职责 | 依赖 |
|---|---|---|
| 领域服务 | 领域规则与计算 | 其他领域对象、接口(由外层实现) |
| 应用服务 | 用例编排、事务、调用仓储与消息 | 基础设施 |
示例:订单计价(Promotion + User + Items)
价格计算依赖促销、用户等级等,放在 Order 上往往臃肿,可下沉为 PricingService:
1 | type Promotion struct { |
其他常见领域服务:转账(两端账户聚合)、库存分配策略、运费计算器等。
4.4.3 领域事件(Domain Event)
领域事件表示已发生的重要业务事实:命名多用过去时(OrderPaid),不可变,携带订阅方所需的最小充分信息,用于解耦聚合与上下文。
价值:
- 解耦聚合:不直接调用另一上下文的
Service。 - 最终一致性:支付成功后异步扣库存、发通知。
- 审计与分析:事件流即事实记录(是否做完整事件溯源另当别论)。
接口与事件定义:
1 | type DomainEvent interface { |
应用服务内:事务与发布协调(示意)
1 | type EventBus interface { |
订阅方(其他上下文):处理器应幂等(至少一次投递)。
1 | type InventoryService interface { |
下单—支付链路的事件编排(Saga / 进程管理器思路)
sequenceDiagram
participant User as 用户
participant OrderBC as 订单上下文
participant InvBC as 库存上下文
participant ShipBC as 物流上下文
participant Notify as 通知上下文
User->>OrderBC: 下单
OrderBC-->>InvBC: OrderPlaced / Created
InvBC-->>InvBC: 预留库存
User->>OrderBC: 支付
OrderBC-->>InvBC: OrderPaid
InvBC-->>InvBC: 扣减库存
OrderBC-->>ShipBC: OrderPaid
ShipBC-->>ShipBC: 创建运单
OrderBC-->>Notify: OrderPaid
Notify-->>User: 支付成功通知
事件设计原则小结
- 不可变:发布后不改写事件内容;纠错用补偿事件。
- 过去时命名:表达「已经发生」。
- 自足性:订阅者尽量少打回源系统;必要字段写在事件里。
- 投递语义:消息中间件上实现至少一次时,消费者必须幂等。
- 与事务:常见做法是事务提交成功后再发布(事务外发箱 Outbox 等模式可进一步保证一致性,此处不展开)。
本节小结:
- 实体标识生命周期,封装状态机与不变量;避免贫血模型在核心域泛滥。
- 值对象描述属性、不可变、值相等;金额与地址等应用值对象可显著减少 bug。
- 聚合定义一致性边界,小聚合 + ID 引用 + 事件协作是实践主基调。
- 仓储以聚合为读写单位;领域服务补齐跨对象规则;领域事件支撑解耦与最终一致。
五、架构落地
战略设计与战术设计解决「边界与模型」;架构落地则回答「目录怎么摆、分层怎么切、和 CQRS / 消息怎么配合」。本节给出经典四层、Go 目录示例、CQRS 读写分离思路,以及 Kafka 等消息设施上的事件驱动集成要点,可与 41-acc-clean-arch-ddd-cqrs.md 对照阅读。
5.1 DDD 的分层架构
经典四层
1 | ┌─────────────────────────────────┐ |
flowchart TB
subgraph layers["DDD 经典四层(依赖向内)"]
P["表现层
Presentation / Interfaces"]
A["应用层
Application"]
D["领域层
Domain"]
I["基础设施层
Infrastructure"]
end
P --> A
A --> D
I -.->|"实现仓储、消息等接口
(依赖倒置)"| D
各层职责与代码形态
1. 表现层(Interfaces / Presentation)
- 职责:接入协议(HTTP、gRPC、消息消费者),解析输入、调用应用层、组装响应。
- 不应包含:业务规则与不变量(只做适配与校验边界)。
1 | // HTTP Handler 示例 |
2. 应用层(Application)
- 职责:编排用例、控制事务、在提交后发布领域事件;薄薄一层。
- 包含:Application Service、DTO、应用级事件处理器(若团队这样划分)。
1 | type OrderApplicationService struct { |
3. 领域层(Domain)
- 职责:核心业务逻辑与不变量。
- 包含:实体、值对象、聚合、领域服务、仓储接口、领域事件。
- 特点:不依赖具体数据库、框架或消息 SDK。
1 | type Order struct { |
4. 基础设施层(Infrastructure)
- 职责:技术细节实现。
- 包含:仓储实现、Outbox、Kafka Producer、缓存、第三方 HTTP 客户端等。
1 | type PostgresOrderRepository struct { |
依赖方向与 Clean Architecture 对照
1 | 表现层 ──→ 应用层 ──→ 领域层 ←── 基础设施层 |
- 依赖向内:越外层越「面向用例与交付」,越内层越稳定。
- 依赖倒置:基础设施实现领域层定义的端口(接口)。
| DDD 分层 | Clean Architecture | 说明 |
|---|---|---|
| 表现层 | Interface Adapters | HTTP / gRPC / 消息适配 |
| 应用层 | Use Cases | 用例编排与事务 |
| 领域层 | Entities(核心企业规则) | 与框架无关的领域模型 |
| 基础设施层 | Frameworks & Drivers | DB、MQ、外部系统 |
更系统的对照与 CQRS 分层变体见 41-acc-clean-arch-ddd-cqrs.md 第四、五部分。
5.2 目录结构设计
下面给出一个典型的 Go 单体服务内按 DDD 分层 + 按聚合分包 的目录骨架(可按团队规范微调 internal 与 pkg 的边界)。
1 | order-service/ |
目录原则小结:
- 按层分:
domain/application/infrastructure/interfaces一目了然。 - 按聚合分:
domain/order、domain/inventory等,避免「一个大 package 装所有实体」。 - 依赖方向:
domain不 import 其他层;外层依赖内层。 - 测试贴近源码:
order_test.go与order.go同目录,降低阅读成本。
命名习惯(示例):
- 领域对象:
order.go、order_item.go - 仓储接口:
order_repository.go;实现:postgres_order_repo.go - 应用服务:
order_service.go;领域服务:pricing_service.go
与 Java / Spring Boot 常见布局对照(概念等价,语法不同):
1 | order-service/ |
5.3 DDD + CQRS
CQRS(Command Query Responsibility Segregation)把「改状态的写模型」和「查数据的读模型」在模型与存储上拆开,常与事件驱动的读模型投影结合。
何时引入
适合:
- 读写比例悬殊(读多写少)或 SLA 不同。
- 查询要跨聚合、跨上下文拼宽表,直接在写库上 join 成本高。
- 需要独立扩展读路径(缓存、搜索、物化视图)。
谨慎:
- 典型 CRUD、读写都简单且一致性强需求集中在单表。
- 团队尚无「最终一致」运维与监控经验时,不要一上来全站 CQRS。
电商订单:写模型规范化、读模型宽表
矛盾:
- 写:下单要保证
Order与OrderItem等同聚合(或同一事务边界)内强一致。 - 读:订单列表要展示用户昵称、商品主图、物流摘要等,来自多上下文;写库范式化则查询痛苦。
思路:写侧维持聚合与事务;读侧用事件增量维护投影(Elasticsearch、Redis、专用读库均可)。
1 | 写模型(订单上下文) |
写模型(示意):
1 | type Order struct { |
读模型(投影构建示意):
1 | type OrderListView struct { |
flowchart LR WR[写请求] --> BC[订单上下文] BC --> PG[(PostgreSQL
规范化写库)] BC --> EVT[发布领域事件] EVT --> PROJ[读模型投影器] PROJ --> IDX[(Elasticsearch / 读库 / 缓存)] QR[查询请求] --> QRY[查询 API] QRY --> IDX
设计要点:
- 写模型负责事务与不变量;读模型可滞后,但要可观测(延迟、积压)。
- 同一业务可有多套读模型(列表、详情、运营报表)。
- 读写可独立扩缩与选型(OLTP + 搜索 / 分析引擎)。
更多分层与 CQRS 变体仍推荐对照 41-acc-clean-arch-ddd-cqrs.md 第五部分。
5.4 DDD + 事件驱动架构
领域事件在限界上下文之间传递「已发生的事实」;落地时通常配合 Kafka(高吞吐、持久化、可回放)、RabbitMQ(灵活路由)、NATS(轻量)等中间件。选型取决于顺序性、投递语义、运维形态,这里不展开产品对比。
下单—支付链路的逻辑视图
flowchart TB U[用户下单] --> O1[订单上下文:创建 Pending 订单] O1 --> K1[OrderPlacedEvent → Kafka] K1 --> I1[库存上下文:锁定库存] I1 --> K2[InventoryReservedEvent] P[用户支付] --> O2[订单上下文:支付成功] O2 --> K3[OrderPaidEvent → Kafka] K3 --> I2[库存:扣减] K3 --> S[物流:创建运单] K3 --> N[通知:触达用户] K3 --> A[分析:行为流水]
发布与订阅(接口 + Kafka 示意)
1 | type EventBus interface { |
1 | type OrderPaidEventHandler struct { |
长流程与 Saga(补偿)
事件编排实现最终一致;若需要显式「多步远程调用 + 补偿」,可引入 Saga / 流程管理器(与消息驱动可并存)。
1 | 订单已创建 → 锁定库存 → 支付 → 扣减库存 → 创建运单 |
1 | type OrderSaga struct { |
关键工程要点
- 消费者幂等:至少一次投递下,重复消息不得破坏不变量。
- 顺序与分区键:同一聚合或业务流程使用稳定
key映射到分区,避免乱序破坏状态机假设。 - 重试与死信:可重试错误与不可重试错误要区分; poison message 要隔离。
- 补偿与对账:跨上下文失败路径要可观测、可人工介入。
本节小结:
- 四层架构划定职责与依赖方向,依赖倒置把技术细节挡在领域之外。
- 目录按层 + 按聚合组织,有利于演进与代码导航。
- CQRS分离写模型与读模型,读侧多用事件投影换查询性能与扩展性。
- 事件驱动用中间件连接上下文,配合幂等、顺序、重试、Saga 才能长期运维。
六、实施指南
从书本概念到团队日常交付,还需要回答:要不要上 DDD、遗留系统怎么迁、工作坊怎么开、哪些坑别踩。本节给出一套偏工程落地的 checklist 与阶段化路径,仍以电商为叙事背景。
6.1 何时使用 DDD
往往值得投入的场景
- 业务复杂:规则多、变更多,状态机 / 促销 / 履约链路长。
- 长生命周期:系统会持续迭代,模型需要可演进、可讨论。
- 多团队协作:需要清晰的上下文边界与接口契约,降低「口口相传」成本。
- 领域专家可参与:能共建统一语言与验收示例(哪怕从简版术语表开始)。
不太划算的场景
- 简单 CRUD:后台配置、纯表单管理,战术 DDD 全套易过度。
- 短周期交付:例如小于 3 个月的工具型项目,学习曲线摊不薄。
- 技术主导、领域稀薄:日志管道、纯基础设施类系统,DDD 核心收益有限。
- 团队零铺垫硬上:没有教练或共读,容易学成「伪 DDD」。
决策矩阵(业务复杂度 × 周期)
| 业务复杂度 / 项目周期 | 短期(少于 6 个月) | 中期(6~18 个月) | 长期(大于 18 个月) |
|---|---|---|---|
| 简单(CRUD 为主) | 不必强行 DDD | 不必强行 DDD | 可在核心域轻量战术 DDD |
| 中等(有明显规则) | 以战术设计为主,边界先行 | 推荐 DDD | 推荐 DDD |
| 复杂(状态机、工作流) | 推荐 DDD | 强烈推荐 | 强烈推荐 |
快速自检(满足 3 条以上可认真考虑 DDD)
- 业务规则超出「单表 CRUD + if-else」可维护范围
- 系统预期持续演进而非一次性交付
- 能拉到业务方定期评审模型与术语
- 团队规模与模块边界需要显式治理(通常多于 3 人协作同一产品)
- 未来会有多个子系统 / 上下文集成(支付、库存、营销等)
6.2 从既有系统迁移到 DDD
遗留单体 + 贫血服务 + 共享大库是常见起点。建议采用绞杀者模式(Strangler Fig):新能力用新结构承接,旧能力渐进搬迁,全程保持可发布。
阶段 0:现状(典型问题)
1 | [单体应用] |
- 业务规则散落、难以单测;团队不敢改「核心路径」。
阶段 1:识别边界(少改代码,多对齐认知)
目标:用统一语言描述「聚合、上下文、事件」,形成共识图纸。
行动:
- 组织 Event Storming(见 6.3),先事件后命令再聚合。
- 标出核心聚合(如
Order+OrderItem)与上下文(订单、库存、支付、商品)。 - 画上下文映射(客户-供应商、防腐层、开放主机服务等)。
产出:领域草图、上下文边界说明、聚合设计备忘。
周期感:约 1~2 周(视领域规模与参与人可用性)。
阶段 2:在代码里「收口」到聚合
目标:把订单相关不变量迁回 Order 聚合,服务层变薄。
之前(贫血):
1 | func (s *OrderService) CancelOrder(orderID string) error { |
之后(充血 + 应用服务编排):
1 | func (o *Order) Cancel(reason string) error { |
周期感:2~4 周(取决于测试防护与耦合程度)。
阶段 3:引入领域事件,拆掉直连
之前:订单服务直接调用库存、退款接口,失败策略缠在一起。
1 | func (s *OrderService) CancelOrder(orderID string) { |
之后:聚合内产生事件,提交后发布;库存 / 支付上下文各自订阅。
1 | func (s *OrderApplicationService) CancelOrder(orderID string) error { |
周期感:2~3 周(含幂等、重试与监控)。
阶段 4:服务化 / 数据库拆分(在边界验证之后)
目标:订单上下文独立部署、独立数据存储,通过 API + 事件集成。
1 | [单体] |
周期感:4~6 周起,高度依赖数据一致性与流量迁移策略。
阶段 5:持续演进
- 继续按上下文拆分;为读路径引入 CQRS / 投影;完善可观测性与对账工具。
成功要素与风险
- 渐进:禁止「停机半年重写」。
- 可运行:每个迭代都可发布、可回滚。
- 对齐:产品、研发、测试对术语与边界一致。
- 价值优先:从核心域下手,支撑域允许简单模式。
- 风险:双写期要有对账;灰度与特性开关;保留回滚剧本。
6.3 团队协作
Event Storming(事件风暴)简版流程
参与者:领域专家、开发、产品、测试(可选运维)。
- 橙贴:领域事件 —— 「订单已创建」「库存已锁定」「订单已支付」。
- 蓝贴:命令 —— 谁触发?来自用户还是策略?
- 黄贴:聚合 / 策略 —— 哪个模型负责执行命令、维护不变量?
- 边界与上下文 —— 在哪里术语含义变化、事务必须切开?
- 关系 —— 客户-供应商、防腐层、发布语言等。
产出:端到端流程墙、候选聚合列表、上下文地图草稿。
1 | [下单] → OrderPlaced → [锁定库存] → InventoryReserved → [支付] → OrderPaid → ... |
工具:实体墙 + 便利贴;远程可用 Miro、FigJam 等白板。
与领域专家共建统一语言
- 维护术语表(Glossary):中英文、禁用同义词混用。
- 代码即文档:类型名、方法名尽量用业务词(
PlaceOrder而非SubmitData)。 - 定期 Review:新需求先问「改的是哪个上下文、哪个聚合」。
- 可视化:上下文图、核心序列图挂在团队可见处。
电商术语表示例:
| 术语 | 含义 |
|---|---|
| 下单(PlaceOrder) | 用户提交购买意图,生成待支付订单 |
| 锁库存(ReserveInventory) | 为订单预留可售库存,防超卖 |
| 订单已支付(OrderPaid) | 支付成功后的领域事实,触发履约链路 |
6.4 常见陷阱
陷阱 1:为 DDD 而 DDD(过度设计)
现象:简单配置模块也硬拆聚合、事件、六边形,团队抱怨「样板比业务还多」。
对策:用子域分类投资;核心域厚建模,支撑域允许贫血或事务脚本。
陷阱 2:贫血模型回潮
现象:实体只有 getter/setter,所有规则在 *Service;领域层名存实亡。
1 | type Order struct { |
对策:反复问「这条规则属于哪个对象的生命周期?」;把状态机放进聚合;服务只做编排。
陷阱 3:聚合切错(过大或互相践踏)
过大:把订单、库存、支付塞进同一聚合,事务与并发锁灾难。
跨聚合直接改:order.Inventory.Deduct() 破坏边界。
1 | // 不推荐:库存不应作为订单聚合的内部可变部分 |
对策:小聚合、ID 引用、跨聚合用领域事件或显式应用层编排 + 反腐蚀。
陷阱 4:忽略上下文映射与数据所有权
现象:多服务读写同表、隐式依赖、无法独立部署。
对策:一上下文优先一库;集成只走 API / 事件;把映射关系画成团队契约。
陷阱 5:过早微服务化
现象:边界未验证就拆十几个服务,分布式事务与运维成本爆炸。
对策:模块化单体先固化上下文;验证协作与数据边界后,再拆部署单元。
本节小结:
- 是否采用 DDD 看复杂度、周期、团队与演进预期,用矩阵与 checklist 收敛决策。
- 迁移用绞杀者模式分阶段:认清边界 → 聚合收口 → 事件解耦 → 服务与数据拆分 → 持续演进。
- 协作靠 Event Storming 与术语表,让模型可讨论、可验收。
- 避坑的核心是:别过度、别贫血、别大聚合、别共享数据库、别过早拆分。
七、常见问题Q&A
下面八个问题来自团队在落地 DDD 时反复遇到的困惑:从贫血模型、服务边界、一致性到跨聚合协作、代码结构、迁移路径、框架适配以及何时不必用 DDD。每个问题都给出可操作的判断标准、电商语境下的例子和示意代码,便于对照本文前文的战略 / 战术 / 架构章节阅读。
Q1:如何避免贫血模型?
问题现象
- 实体只有 getter/setter,几乎没有表达业务含义的方法。
- 业务规则全部堆在
*Service里,实体沦为「数据库行的内存镜像」。 - 新人读代码时只能顺着 Service 的调用链猜规则,领域语言在类型系统里缺席。
为什么会出现贫血模型
- 长期习惯 Controller–Service–DAO 三层,默认「Service 写逻辑」。
- 不清楚不变量与生命周期应该由谁守护。
- 部分框架或代码生成器鼓励「纯数据类 + 注解」,进一步固化贫血形态。
DDD 的解决思路:数据与行为合一
原则:与某概念强相关的规则,应落在拥有该状态的对象上;应用服务只做用例级编排(加载、调用领域对象、提交、发布副作用)。
对比示例:电商「取消订单」
贫血模型(不推荐):
1 | // Order 只有数据 |
充血模型(推荐):
1 | type Order struct { |
判断标准(一句话)
问自己:「这条规则究竟属于谁的生命周期?」
- 属于
Order的业务规则 → 放进Order(实体 / 聚合根)。 - 需要协调多个聚合或外部系统 → 应用服务或领域服务编排。
- 持久化、消息、HTTP 等 → 基础设施,通过接口接入。
Q2:如何划分服务(或模块)边界?
问题现象
- 微服务很多,但一次需求要改三四个仓库,协作成本比单体还高。
- 同步调用链过长,延迟与故障面放大。
- 「按表拆服务」或「按技术层拆」导致事务被迫分布式化。
常见错误划分
- 按技术职能:例如单独的「订单头服务」「订单项服务」「订单状态服务」,一次下单三次 RPC。
- 按数据表机械映射:表即服务,忽略业务能力与语言边界。
- 拍脑袋拆分:没有上下文地图与数据所有权共识。
DDD 方案:以限界上下文为边界
划分原则:
- 业务能力:每个上下文最好对应一条清晰的业务能力(如「接单计价」「库存承诺」「收款」)。
- 语言边界:同一词在不同团队含义不同处,往往是上下文分界线。
- 数据一致性:需要强一致维护的不变量尽量落在同一聚合 / 同一上下文内。
- 团队结构:理想情况下一个团队主要 owning 一个上下文,减少扯皮。
电商示意
不推荐:
1 | [订单头服务] [订单项服务] [订单状态服务] |
更合理:
1 | [订单上下文] |
决策清单(拆分前自检)
- 是否有清晰的业务边界与独立演化故事?
- 是否可以独立发布,且不依赖「偷偷读别家库表」?
- 团队能否端到端负责该能力(含 SLA、监控、数据)?
- 拆分后是否仍能用最终一致性讲清楚跨上下文协作?
更完整的电商平台划分可对照本文第三部分(战略设计)的上下文地图与集成关系。
Q3:如何保证聚合内一致性?
问题现象
- 并发下库存超卖,或订单状态与支付结果不一致。
- 「最后写入获胜」掩盖了业务冲突,对账时才发现错账。
- 长事务锁表,吞吐下降。
DDD 观点:聚合是事务与一致性边界
- 一个事务内只提交一个聚合的变更(惯例);聚合内用模型保证不变量。
- 并发控制应落在聚合粒度(版本号、乐观锁、必要时
SELECT FOR UPDATE)。 - 数据库约束(如
CHECK (available_qty >= 0))是最后一道防线,不能替代模型。
示例:订单上的乐观锁
1 | type Order struct { |
示例:悲观锁(同一聚合内的临界区)
1 | func (s *OrderApplicationService) CancelOrder(orderID string) error { |
库存预留与超卖
1 | type Inventory struct { |
1 | -- 数据库层约束示例(具体语法随库而定) |
小结
- 聚合边界 ≈ 事务边界(实践中的默认假设)。
- 版本号 / 锁解决并发写冲突;约束兜住极端竞态。
- 跨聚合的协调交给事件与最终一致性(见 Q4),而不是把多个聚合硬塞进同一事务。
Q4:如何处理跨聚合(跨上下文)操作?
问题现象
- 下单要同时动订单、库存、支付,团队第一反应是「上分布式事务」。
- XA / 2PC 带来可用性与性能问题,运维与排障成本高。
- 失败路径不清晰,补偿逻辑散落在各处。
DDD 方案:聚合内强一致,聚合间最终一致
- 单个聚合内:本地事务 + 模型不变量。
- 多个聚合 / 上下文:领域事件、消息中间件、必要时 Saga / 补偿。
- 明确接受:跨边界的一致性通常是最终一致,用业务规则与对账兜底。
反例:把一切都绑进分布式事务
1 | func (s *OrderService) PlaceOrder(...) error { |
正例:事件驱动解耦
1 | // 订单上下文:创建订单并发布事实 |
1 | func (s *InventoryService) ReserveInventory(orderID OrderID, items []LineItem) error { |
Saga 与补偿(示意)
1 | 订单创建 → 库存锁定 → 支付 → 完成 |
1 | type InventoryReservationFailedHandler struct { |
关键点
- 用事件表达「已发生的事实」,降低模块耦合。
- 为失败设计显式补偿与幂等,配合监控与人工介入通道(见本文 5.4、6.2)。
Q5:如何组织代码结构,避免循环依赖?
问题现象
domain包import了infra或具体 ORM,依赖方向倒置失败。- 包之间互相引用,编译器报错或被迫用「接口下沉到奇怪位置」的 workaround。
- 单元测试必须启动数据库或全局容器。
DDD + 依赖倒置
- 领域层只依赖本层抽象与语言标准库(理想情况)。
- 应用层依赖领域接口,组织用例。
- 基础设施层实现领域定义的 Repository / Gateway 等接口。
- 依赖方向:外层依赖内层;装配在
main或 composition root 完成。
推荐目录(示意)
1 | internal/ |
领域层定义接口
1 | // domain/order/order_repository.go |
基础设施实现接口
1 | // infrastructure/persistence/postgres_order_repo.go |
应用层依赖接口
1 | // application/order_service.go |
在 main 中装配
1 | func main() { |
小结
- 接口归属领域,实现归属基础设施;这是 Clean Architecture 与 DDD 常见的结合点(详见本文 第五部分 与 41-acc-clean-arch-ddd-cqrs.md)。
Q6:如何从单体渐进演进到 DDD 与微服务?
核心矛盾
- 一次性重写风险极高;不停机迁移又容易被历史耦合拖死。
推荐策略:绞杀者模式 + 分阶段验证
与本文 6.2 一致,这里给出路线图浓缩版:
1 | 阶段 1:识别聚合与限界上下文(以工作坊与文档为主,少改代码) |
原则
- 渐进:每一迭代都可发布、可回滚。
- 从核心域开始:先让赚钱路径模型清晰,再推广到支撑域。
- 每阶段验证价值:用缺陷率、需求吞吐、沟通成本度量,而不是「是否更多类文件」。
电商迁移的前后代码对比与周期感见 6.2 各子阶段。
Q7:DDD 如何与 ORM / 框架共存?
问题现象
- ORM 要求导出字段、无参构造,与「封装 + 工厂创建」冲突。
- 把 JPA 注解直接贴在「领域实体」上,领域层被持久化细节污染。
思路:领域模型与持久化模型分离(适配器)
领域对象保持封装与不变量;PO / Entity / Document 面向框架;仓储负责双向转换。
Go + GORM 示意
1 | // domain/order/order.go |
1 | // infrastructure/persistence/order_po.go |
1 | func (r *PostgresOrderRepository) Save(o *order.Order) error { |
Java + JPA 示意
1 | // domain — 纯业务构造与行为 |
1 | // infrastructure — JPA 专用 |
小结
- 框架约束留在最外层;领域保持可测试、可阅读、可讨论。
- 转换成本通常远低于「领域与数据库 schema 锁死」带来的长期利息。
Q8:何时不应该用 DDD?
明确不太划算的场景
- 简单 CRUD 为主:后台配置、元数据管理,业务规则稀薄。
- 报表 / 分析为主:读多写少、以 SQL / OLAP 为核心,领域行为弱。
- 纯技术或管道型系统:日志、监控、同步工具,价值在工程而非领域模型。
- 极短周期项目:例如少于数月且一次性交付,学习与设计成本摊不薄。
- 团队条件不成熟:无人能与业务共建统一语言,却强行套用战术模式样板。
判断口诀
1 | 业务复杂度低 + 短期交付 → 通常不必上全套 DDD |
可替代方案
- 经典 MVC + Service + 事务脚本 足以支撑许多后台系统。
- 读路径复杂时,SQL + DTO + 专用查询服务往往更直接。
- 工具类系统可用函数式管道、配置驱动等更简单结构。
核心原则
不要为了 DDD 而 DDD。 先判断复杂性与生命周期,再选择建模深度;本文 6.1 的矩阵与 checklist 可与本问对照使用。
八、总结
8.1 DDD 的价值
应对复杂性:把业务规则从隐式的 if-else 与 SQL 片段中抬升为显式模型;用统一语言压缩产品、研发、测试之间的翻译成本;用聚合守护不变量,使修改局部化。
改善协作:限界上下文为团队提供自治边界与清晰的上下游关系;集成模式(防腐层、开放主机服务等)把「怎么对接」说清楚,减少口头协议。
提升工程质量:充血模型带来更高内聚;领域事件削弱模块间耦合;分层 + 依赖倒置让核心逻辑可单测、可替换基础设施。
电商叙事收束:战略上划分订单、库存、支付等上下文;战术上用 Order 聚合承载生命周期;架构上用四层、CQRS、事件驱动落地——这与 41-acc-clean-arch-ddd-cqrs.md 中的实践相互印证。
8.2 两本书的阅读建议
蓝皮书(Eric Evans)——抓主线
建议精读:第 1~2 章(价值与统一语言);第 4 章(分离领域);第 5~7 章(实体、值对象、领域服务);第 14~17 章(限界上下文与上下文映射,战略核心)。
可按需翻阅:第 8~13 章(聚合、工厂、仓储);第 18~19 章(重构与精炼)。
顺序建议:若你更关心「先画边界再填模型」,可先读 14~17 章建立地图,再回读 5~13 章补战术细节。
红皮书(Vaughn Vernon)——偏落地
建议精读:第 1 章(回顾与动机);第 2~3 章(上下文细化);第 5~6 章(实体与值对象);第 10 章(聚合设计,实践必读);第 8 章(领域事件)。
可按需翻阅:第 4 章(架构)、第 7 章(领域服务)、第 11~12 章(工厂与仓储)、第 13 章(集成)。
顺序建议:红皮书章节编排相对线性,可按目录顺序读,遇到与蓝皮书重叠处互相印证。
如何两本一起读
- 蓝皮书 14~17 章:建立上下文与映射的大图。
- 红皮书 2~3 章:看案例化表述与落地注意点。
- 蓝皮书 5~13 章:补齐战术概念。
- 红皮书 5~7、10 章:尤其是聚合设计实操。
- 红皮书 第 8 章:补齐事件驱动视角。
- 红皮书 4、13 章:架构与跨上下文集成。
8.3 DDD 学习路径
| 阶段 | 目标 | 建议动作 | 参考 |
|---|---|---|---|
| 1. 概念(约 1~2 周) | 搞清术语与边界思维 | 读本文第二、三部分;翻蓝皮书前 4 章 + 红皮书第 1 章 | 统一语言、上下文、聚合 |
| 2. 战术练习(约 2~4 周) | 手写小模型 | 用转账、下单等小题练习实体、值对象、聚合与单测 | 红皮书 5~7 章 |
| 3. 项目试点(约 1~3 月) | 在真实代码中验证 | 选一模块收口不变量、引入仓储接口、逐步充血 | 本文第六部分 |
| 4. 战略深化(持续) | 团队级对齐 | Event Storming、上下文地图、集成关系评审 | 蓝皮书 14~17 章;红皮书 2~3 章 |
| 5. 架构演进(持续) | 规模与性能 | 事件、CQRS、读模型、拆分服务与数据 | 红皮书 8、13 章;本文第五部分 |
8.4 与其他架构模式的关系
- DDD + Clean Architecture:DDD 回答「模型如何表达业务」;Clean Architecture 回答「依赖如何向内收敛」。领域层大致对应最内圈的实体与用例规则。详见 41-acc-clean-arch-ddd-cqrs.md。
- DDD + CQRS:写模型由聚合守护不变量;读模型可旁路优化;领域事件常用于投影。见本文 5.3。
- DDD + 事件驱动:事件既是业务事实的载体,也是上下文之间解耦集成的手段。见本文 5.4。
- DDD + 微服务:限界上下文提供拆分依据;上下文映射指导集成与治理。见本文 6.2。
收束建议:DDD 不是银弹;从小范围、可验证的改进开始;坚持统一语言与持续重构,模型会随业务一起演进。
本专题衔接:建议先读 架构与整洁代码(一) 与 (二);落地评审可配合 架构与整洁代码(四):Code Review Checklist。
参考资料
- Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software, Addison-Wesley, 2003(中文版:《领域驱动设计:软件核心复杂性应对之道》,清华大学出版社,2006)。
- Vaughn Vernon, Implementing Domain-Driven Design, Addison-Wesley, 2013(中文版:《实现领域驱动设计》,电子工业出版社,2014)。
- Martin Fowler, Patterns of Enterprise Application Architecture, Addison-Wesley, 2002.
- 本站:Clean Architecture + DDD + CQRS。
- 本站:电商系统概览。
- 本站:电商商品列表 / 订单相关。
- 本站:电商库存系统。
- 本站:电商支付系统。
- Martin Fowler, Event Sourcing(在线短文)。
- Microsoft Learn, Domain-Driven design(Azure Architecture Center,英文)。