DDD 领域驱动设计
用战略设计划定业务边界、用战术设计沉淀领域模型,把复杂业务翻译成可落地的代码结构,并为微服务拆分提供边界依据。
一、DDD 概述与核心思想
1.1 什么是 DDD
DDD(Domain-Driven Design,领域驱动设计)是 Eric Evans 提出的一套以领域为核心的软件建模方法论。它不绑定任何技术框架,主张"软件复杂度应该镜像业务复杂度":先理解业务,再设计模型,最后才考虑技术实现。
它与传统"数据驱动"开发(先画表、再做 CRUD)的本质区别:数据驱动把业务规则散落在 Service 脚本里;DDD 把规则收拢到领域对象中,让代码结构本身就是业务结构的投影。
1.2 领域与子域
- 领域(Domain):软件要解决的问题空间,如电商领域、支付领域。
- 子域(Subdomain):对领域的细分,是问题空间的切分。按对业务的重要性分三类:
| 子域类型 | 定义 | 例子 | 投入策略 |
|---|---|---|---|
| 核心域 | 让公司区别于竞争对手、最值得投入的逻辑 | 电商的订单履约、推荐算法 | 自研、重点建模、最高质量 |
| 支撑域 | 核心域必需但非差异化 | 库存调度、权限 | 可自研但不必过度投入 |
| 通用域 | 行业通用、买现成更划算 | 短信、支付渠道、邮件 | 优先采购/开源/外包 |
建模力度的排序:核心域要精雕细琢(战术设计全套),通用域直接接第三方,不值得为它做复杂建模。
1.3 通用语言(Ubiquitous Language)
通用语言是业务专家与开发团队共同使用、无歧义的词汇表。例如"下单后 30 分钟未支付自动关闭",业务上叫"超时关单",代码里就叫 closeByTimeout(),而不是叫 updateStatusToCanceled()。
通用语言的意义:消灭"业务说 A、代码写 B、测试测 C"的三方翻译损耗。建模工具(事件风暴)的核心产出之一就是统一术语表。
二、战略设计
2.1 战略设计解决什么问题
战略设计回答"边界画在哪":系统这么大,先承认不可能用一个统一模型描述所有业务,再把领域切成多个限界上下文,各自维护独立模型。战术设计则回答"边界内怎么建模"。
2.2 限界上下文(Bounded Context)
限界上下文是解决方案空间的单位:一个上下文 = 一个自治的模型边界 = 一套明确的术语 = 独立的代码与数据。
经典例子:"订单"在交易上下文里是"待支付、已支付、已发货"的状态机对象;在仓储上下文里是"待拣货、已出库"的物流对象。同一个词,两个模型,不必强行统一成一个"大订单"。强行统一正是大型系统腐化的根源之一。
2.3 上下文映射(Context Mapping)
限界上下文之间需要协作,协作关系即上下文映射,常见的七种关系:
| 关系 | 含义 | 使用场景 |
|---|---|---|
| 合作关系(Partnership) | 两个团队同步开发、共同演进 | 强依赖且节奏一致 |
| 共享内核(Shared Kernel) | 共享一小块模型与代码 | 关系紧密、想减少重复 |
| 客户-供应商(Customer-Supplier) | 上游定契约,下游按契约开发 | 大多数上下游服务关系 |
| 防腐层(ACL) | 在下游侧翻译上游模型,保护自己 | 对接外部系统/老系统 |
| 开放主机服务(OHS) | 上游把能力发布为公共协议(REST/gRPC) | 服务化后的标准输出 |
| 已发布语言(PL) | 用公开标准协议通信(如 JSON Schema) | 跨组织/跨技术栈 |
| 分离方式(Separate Ways) | 完全解耦、无集成 | 互不相关或重复成本低于集成成本 |
2.4 上下文映射图示例
注意防腐层箭头方向:下游依赖上游的翻译,防腐层属于下游,向上游方向依赖,箭头指向被依赖方。
2.5 战略设计产出物
- 事件风暴/领域分析结果:业务事件流(用户下单 → 订单已创建 → 已支付 → 已发货);
- 子域清单:核心/支撑/通用域及归属;
- 上下文地图:所有限界上下文与映射关系;
- 通用语言术语表。
战略设计阶段不出现任何表结构或技术选型,先于技术做业务切分。
三、战术设计
3.1 实体(Entity)
有唯一标识且跨生命周期状态连续变化的对象。实体不靠属性相等来判断同一性,而靠 ID。
// 订单 ID 相同即同一订单,即使内容被修改
public class Order {
private OrderId id; // 身份标识
private OrderStatus status; // 状态随时间变化
}// 订单 ID 相同即同一订单,即使内容被修改
public class Order {
private OrderId id; // 身份标识
private OrderStatus status; // 状态随时间变化
}3.2 值对象(Value Object)
描述属性本身、无标识、靠属性整体相等判断同一性、不可变的对象。金额 + 币种、地址、时间段都是典型值对象。
public record Money(BigDecimal amount, String currency) {
public Money {
if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负");
}
}
public Money add(Money other) {
if (!currency.equals(other.currency)) throw new IllegalArgumentException("币种不一致");
return new Money(amount.add(other.amount), currency);
}
}public record Money(BigDecimal amount, String currency) {
public Money {
if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负");
}
}
public Money add(Money other) {
if (!currency.equals(other.currency)) throw new IllegalArgumentException("币种不一致");
return new Money(amount.add(other.amount), currency);
}
}值对象把"无标识但有约束"的小概念从贫血散落字段里拯救出来:amount 和 currency 成对出现、不可变、自带校验,不可能出现"只有金额没有币种"的非法状态。
3.3 聚合与聚合根(Aggregate & Aggregate Root)
- 聚合:把一组必须保持一致性的对象作为一个整体修改的边界;
- 聚合根:聚合对外的唯一入口,外部只能通过聚合根方法修改聚合内部对象;
- 一致性规则:一次事务只应修改一个聚合;跨聚合的一致性问题领域内不管,交给应用层/最终一致性。
设计判断:"修改订单商品数量时,是否需要同时修改订单金额?"——需要,所以 OrderItem 和 Order 属于同一聚合,由 Order 聚合根统一计算金额。
聚合设计的两个常见错误:
- 超大聚合:把订单 + 全部商品 + 全部操作日志塞进一个聚合,导致并发写互相阻塞、事务过大——应拆小(订单主聚合 + 独立的操作日志聚合);
- 误设聚合根:让内部实体直接暴露给外部修改,绕过聚合根校验。
3.4 仓储(Repository)
仓储是"聚合的存取仓库",接口定义在领域层,实现在基础设施层:
- 语义是"拿聚合、存聚合",不是"拼 SQL";
- 只对聚合根提供仓储,聚合内部对象通过聚合根访问;
- 实现可以是 MyBatis、JPA 或内存 Map,领域层不关心。
仓储接口定义在领域层正是依赖倒置的体现:领域逻辑依赖抽象,MyBatis 细节在基础设施层实现。
3.5 领域服务(Domain Service)
当某个业务动作不属于任何实体/值对象、或需要协调多个聚合时,放在领域服务中。典型场景:下单要同时检查库存聚合与创建订单聚合时,跨聚合编排逻辑放领域服务,但事务边界仍各自独立(通过领域事件或应用层协调)。
3.6 领域事件(Domain Event)
聚合状态变化产生的、已发生事实的记录(过去时命名:OrderCreatedEvent、PaymentSucceededEvent)。作用:
- 解耦聚合间的副作用(订单创建后发消息、记审计、扣库存都订阅事件);
- 作为跨上下文/跨服务最终一致性的载体(配合 MQ);
- 支撑审计与事件溯源。
注意:事件的产生在领域层(聚合根方法内 registerEvent),事件的发布在应用层/基础设施层(事务提交后发 MQ),避免"事务回滚了消息却已发出"。
3.7 工厂与规格
- 工厂:聚合创建逻辑复杂(需组装多个对象、处理默认值)时,用工厂方法或独立 Factory 收拢创建逻辑,避免聚合根构造函数臃肿;
- 规格(Specification):把"是否满足某业务条件"的校验封装为可复用对象(如
OverdueOrderSpec),用于仓储查询与内存校验的统一。
3.8 CQRS(命令查询职责分离)
当读模型与写模型的形态差距过大时,把命令(写)与查询(读)分离:
- 写侧走领域模型(实体、聚合、事务);
- 读侧直接投影出查询专用模型(宽表、缓存、搜索索引、甚至独立的只读库)。
CQRS 是战术手段而非银弹:只有出现"复杂的写 + 高频多样的读"(报表、Feed、订单列表)时引入;简单 CRUD 强行 CQRS 是过度设计。可降级方案是应用层读模型 + 领域层写模型,不引入独立数据库。
3.9 事件溯源(Event Sourcing)概念
不存聚合的"当前状态",而存产生状态的全部事件序列,当前状态由事件回放得到:
订单创建 -> 订单支付 -> 订单发货 (事件流)
replay
状态 = f(事件1..n)订单创建 -> 订单支付 -> 订单发货 (事件流)
replay
状态 = f(事件1..n)- 优点:完整审计、可回溯任意历史时点、天然支撑领域事件;
- 代价:事件模型与存储快照都要管理、回放性能需快照优化、查询必须配 CQRS;
- 结论:仅当"审计追溯是硬性需求"(金融、合规)时才考虑,普通业务用"状态 + 流水日志"通常更划算。
四、贫血模型 vs 充血模型
4.1 两种模型的形态
| 维度 | 贫血模型 | 充血模型 |
|---|---|---|
| 实体类内容 | 只有 getter/setter(纯数据) | 数据 + 业务行为 |
| 业务逻辑位置 | 全部堆在 Service | 内聚到实体/值对象/领域服务 |
| 校验与规则 | Service 里 if 散落 | 对象创建/方法内强制保证 |
| 测试难度 | 测 Service 需 mock 一堆 | 领域对象可直接单测 |
| 新人上手 | 结构简单但业务真相在 Service 海洋里 | 需理解建模,门槛略高 |
| 维护风险 | 同一规则多处实现、改漏一处出 bug | 规则单一来源、改动集中 |
4.2 贫血模型的代表代码
// 贫血:Order 只是数据袋
public class Order {
private String status;
private BigDecimal totalAmount;
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
// ...getter/setter
}
// Service 承载所有规则:谁能改状态?金额怎么算?散落在方法里
@Service
public class OrderService {
public void pay(String orderId) {
Order order = orderMapper.findById(orderId);
if (!"PENDING_PAY".equals(order.getStatus())) {
throw new BizException("订单状态不允许支付");
}
order.setStatus("PAID"); // 若还有别处也改状态,规则就重复了
orderMapper.update(order);
}
}// 贫血:Order 只是数据袋
public class Order {
private String status;
private BigDecimal totalAmount;
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
// ...getter/setter
}
// Service 承载所有规则:谁能改状态?金额怎么算?散落在方法里
@Service
public class OrderService {
public void pay(String orderId) {
Order order = orderMapper.findById(orderId);
if (!"PENDING_PAY".equals(order.getStatus())) {
throw new BizException("订单状态不允许支付");
}
order.setStatus("PAID"); // 若还有别处也改状态,规则就重复了
orderMapper.update(order);
}
}4.3 充血模型的代表代码
// 充血:状态机与规则在聚合根内,任何调用方都无法绕过
public class Order {
private OrderId id;
private OrderStatus status;
private Money totalAmount;
public void pay() {
if (!status.canPay()) {
throw new BizException("当前状态(" + status + ")不允许支付");
}
this.status = OrderStatus.PAID;
// 不变量在模型内自洽
}
}// 充血:状态机与规则在聚合根内,任何调用方都无法绕过
public class Order {
private OrderId id;
private OrderStatus status;
private Money totalAmount;
public void pay() {
if (!status.canPay()) {
throw new BizException("当前状态(" + status + ")不允许支付");
}
this.status = OrderStatus.PAID;
// 不变量在模型内自洽
}
}4.4 贫血模型盛行的现实原因
- 数据库优先的开发惯性:先建表、MyBatis 反向生成实体,实体天然只是表的投影;
- 事务脚本式 Service在简单 CRUD 里确实高效;
- 团队对 DDD 建模掌握不足,强行充血容易把"伪充血"写成"把 SQL 逻辑搬进实体"。
务实结论:业务以简单 CRUD 为主时贫血模型完全够用;业务规则复杂(状态机、计价、风控)时,让核心域的模型充血,外围 CRUD 保持简单。DDD 不要求全系统充血,只要求"复杂处建模、简单处直接"。
4.5 渐进式充血路线
- 从核心域一个高频变更的聚合开始(如订单);
- 先把"状态流转规则"收进实体方法(消灭散落的 if);
- 再引入值对象收敛成对字段与校验(金额、地址);
- Service 只做编排(取仓储、调方法、发事件),消灭脚本逻辑。
五、DDD 与微服务划分的关系
5.1 限界上下文是否等于一个微服务
不等于,但高度相关。原则是"微服务边界从限界上下文推导,但粒度可以合并/再拆":
- 一个上下文不一定是一个服务:小上下文(如通知)可合并进相近服务;
- 一个服务可以包含多个内聚上下文,只要它们变化频率一致。
5.2 划分映射原则
| DDD 概念 | 对微服务的意义 |
|---|---|
| 限界上下文 | 服务的理想边界来源(数据、团队、部署的划分依据) |
| 聚合根 | 决定"哪些数据不能被别的事务直接修改",是数据库表归属的判据 |
| 防腐层 | 服务对接外部系统时的隔离层 |
| 领域事件 | 服务间最终一致性的协作协议 |
5.3 反例:错误的服务切法
- 按表切:order、order_item 各一个服务——把聚合根撕碎,跨服务事务接踵而至;
- 按技术层切:Controller 服务、Service 服务、DAO 服务——复制了单体的分层却放弃了单体的简单;
- 按代码量均分:让每个服务代码量差不多——与业务边界无关的机械拆分。
5.4 DDD 驱动拆分的流程
实操提示:拆分前先在同一代码库内按上下文分包运行一段时间(模块化单体),验证边界合理后再物理拆服务,可大幅降低返工。
六、电商订单模块 DDD 落地示例
6.1 业务与建模范围
以"用户下单(含下单地址、商品行、金额计算、库存预占)"为核心域演示。为控制篇幅,库存预占通过领域事件异步完成(见领域服务与事件发布),这正是 DDD 落地于微服务时最常见的协作方式。
6.2 战略设计结果
| 子域 | 上下文 | 归属 |
|---|---|---|
| 订单履约 | 交易上下文 | 核心域(重点建模) |
| 商品目录 | 商品上下文 | 支撑域 |
| 库存 | 库存上下文 | 支撑域 |
| 支付渠道 | 支付上下文 | 通用域(对接外部) |
| 短信通知 | 通知上下文 | 通用域(采购) |
6.3 聚合设计
- Order 聚合(聚合根:
Order):成员OrderItem(实体)、Money(值对象)、Address(值对象); - 不变量:订单金额 = 商品行金额之和;订单状态机
PENDING_PAY → PAID → SHIPPED单向流转且PAID后可发起CLOSED; - 库存扣减属于库存上下文,不进入 Order 聚合。
6.4 包结构
com.example.order
├── interfaces # 接口层:入参校验与响应组装
│ ├── OrderController.java
│ └── dto
│ ├── CreateOrderRequest.java
│ └── OrderResponse.java
├── application # 应用层:用例编排、事务、事件发布
│ ├── service
│ │ └── OrderApplicationService.java
│ └── assembler
│ └── OrderAssembler.java
├── domain # 领域层:模型与规则(不依赖任何框架)
│ ├── model
│ │ ├── order
│ │ │ ├── Order.java # 聚合根(充血)
│ │ │ ├── OrderItem.java # 实体
│ │ │ ├── OrderId.java # 值对象
│ │ │ ├── OrderStatus.java # 状态枚举(携带流转规则)
│ │ │ ├── Money.java # 值对象:金额+币种
│ │ │ └── Address.java # 值对象:收货地址
│ │ └── event
│ │ └── OrderCreatedEvent.java
│ ├── repository
│ │ └── OrderRepository.java # 仓储接口(依赖倒置)
│ └── service
│ └── OrderDomainService.java # 领域服务(预占库存协调)
├── infrastructure # 基础设施层:仓储实现、事件发布、Mapper
│ ├── repository
│ │ └── OrderRepositoryImpl.java
│ ├── mapper
│ │ ├── OrderMapper.java
│ │ └── OrderItemMapper.java
│ └── event
│ └── OrderEventPublisher.javacom.example.order
├── interfaces # 接口层:入参校验与响应组装
│ ├── OrderController.java
│ └── dto
│ ├── CreateOrderRequest.java
│ └── OrderResponse.java
├── application # 应用层:用例编排、事务、事件发布
│ ├── service
│ │ └── OrderApplicationService.java
│ └── assembler
│ └── OrderAssembler.java
├── domain # 领域层:模型与规则(不依赖任何框架)
│ ├── model
│ │ ├── order
│ │ │ ├── Order.java # 聚合根(充血)
│ │ │ ├── OrderItem.java # 实体
│ │ │ ├── OrderId.java # 值对象
│ │ │ ├── OrderStatus.java # 状态枚举(携带流转规则)
│ │ │ ├── Money.java # 值对象:金额+币种
│ │ │ └── Address.java # 值对象:收货地址
│ │ └── event
│ │ └── OrderCreatedEvent.java
│ ├── repository
│ │ └── OrderRepository.java # 仓储接口(依赖倒置)
│ └── service
│ └── OrderDomainService.java # 领域服务(预占库存协调)
├── infrastructure # 基础设施层:仓储实现、事件发布、Mapper
│ ├── repository
│ │ └── OrderRepositoryImpl.java
│ ├── mapper
│ │ ├── OrderMapper.java
│ │ └── OrderItemMapper.java
│ └── event
│ └── OrderEventPublisher.java依赖规则(单向):interfaces → application → domain ← infrastructure。领域层最内层,谁都不依赖它,它也不依赖谁。
6.5 核心代码骨架
领域层先定义值对象与实体:
// 值对象:OrderId
public record OrderId(Long value) {
public OrderId {
if (value == null || value <= 0) throw new IllegalArgumentException("订单 ID 非法");
}
}
// 值对象:收货地址(不可变、整体相等)
public record Address(String province, String city, String detail, String phone) {
}
// 实体:订单商品行(属于 Order 聚合,无独立对外仓储)
public class OrderItem {
private Long productId;
private String productName;
private Money price; // 快照单价(防商品后续改价影响历史订单)
private int quantity;
OrderItem(Long productId, String productName, Money price, int quantity) {
if (quantity <= 0) throw new IllegalArgumentException("数量必须大于 0");
this.productId = productId;
this.productName = productName;
this.price = price;
this.quantity = quantity;
}
Money subtotal() {
return price.multiply(quantity); // Money 内部实现乘数量
}
}
// 聚合根:Order(充血模型,所有状态变更收口在方法内)
public class Order {
private final OrderId id;
private OrderStatus status;
private Address address;
private Money totalAmount;
private final List<OrderItem> items = new ArrayList<>();
private final List<DomainEvent> pendingEvents = new ArrayList<>();
private Order(OrderId id, Address address, List<OrderItem> items) {
this.id = id;
this.address = address;
this.status = OrderStatus.PENDING_PAY;
this.items.addAll(items);
this.totalAmount = items.stream()
.map(OrderItem::subtotal)
.reduce(new Money(BigDecimal.ZERO, "CNY"), Money::add);
}
/** 工厂方法:集中创建逻辑并登记领域事件 */
public static Order place(OrderId id, Address address, List<OrderItem> items) {
if (items == null || items.isEmpty()) throw new IllegalArgumentException("订单不能为空");
Order order = new Order(id, address, items);
order.pendingEvents.add(new OrderCreatedEvent(id.value(), items.size(), order.totalAmount.amount()));
return order;
}
/** 状态流转收口:外部无法绕过规则直接改状态 */
public void pay() {
this.status = this.status.transitToPaid();
this.pendingEvents.add(new OrderPaidEvent(id.value()));
}
public void cancelByTimeout() {
this.status = this.status.transitToClosed();
}
/** 事务提交后由应用层取走并发布事件 */
public List<DomainEvent> collectEvents() {
List<DomainEvent> events = new ArrayList<>(pendingEvents);
pendingEvents.clear();
return events;
}
public OrderId id() { return id; }
public OrderStatus status() { return status; }
public Money totalAmount() { return totalAmount; }
}
// 状态机收口:状态流转规则只此一份
public enum OrderStatus {
PENDING_PAY {
@Override OrderStatus transitToPaid() { return PAID; }
},
PAID {
@Override OrderStatus transitToPaid() { throw new IllegalStateException("订单已支付,不可重复支付"); }
@Override OrderStatus transitToClosed() { return CLOSED; } // 支付后可关闭(退款/取消发货)
},
CLOSED, SHIPPED;
OrderStatus transitToPaid() { throw new IllegalStateException("当前状态不可支付"); }
OrderStatus transitToClosed() { throw new IllegalStateException("当前状态不可关闭"); }
}// 值对象:OrderId
public record OrderId(Long value) {
public OrderId {
if (value == null || value <= 0) throw new IllegalArgumentException("订单 ID 非法");
}
}
// 值对象:收货地址(不可变、整体相等)
public record Address(String province, String city, String detail, String phone) {
}
// 实体:订单商品行(属于 Order 聚合,无独立对外仓储)
public class OrderItem {
private Long productId;
private String productName;
private Money price; // 快照单价(防商品后续改价影响历史订单)
private int quantity;
OrderItem(Long productId, String productName, Money price, int quantity) {
if (quantity <= 0) throw new IllegalArgumentException("数量必须大于 0");
this.productId = productId;
this.productName = productName;
this.price = price;
this.quantity = quantity;
}
Money subtotal() {
return price.multiply(quantity); // Money 内部实现乘数量
}
}
// 聚合根:Order(充血模型,所有状态变更收口在方法内)
public class Order {
private final OrderId id;
private OrderStatus status;
private Address address;
private Money totalAmount;
private final List<OrderItem> items = new ArrayList<>();
private final List<DomainEvent> pendingEvents = new ArrayList<>();
private Order(OrderId id, Address address, List<OrderItem> items) {
this.id = id;
this.address = address;
this.status = OrderStatus.PENDING_PAY;
this.items.addAll(items);
this.totalAmount = items.stream()
.map(OrderItem::subtotal)
.reduce(new Money(BigDecimal.ZERO, "CNY"), Money::add);
}
/** 工厂方法:集中创建逻辑并登记领域事件 */
public static Order place(OrderId id, Address address, List<OrderItem> items) {
if (items == null || items.isEmpty()) throw new IllegalArgumentException("订单不能为空");
Order order = new Order(id, address, items);
order.pendingEvents.add(new OrderCreatedEvent(id.value(), items.size(), order.totalAmount.amount()));
return order;
}
/** 状态流转收口:外部无法绕过规则直接改状态 */
public void pay() {
this.status = this.status.transitToPaid();
this.pendingEvents.add(new OrderPaidEvent(id.value()));
}
public void cancelByTimeout() {
this.status = this.status.transitToClosed();
}
/** 事务提交后由应用层取走并发布事件 */
public List<DomainEvent> collectEvents() {
List<DomainEvent> events = new ArrayList<>(pendingEvents);
pendingEvents.clear();
return events;
}
public OrderId id() { return id; }
public OrderStatus status() { return status; }
public Money totalAmount() { return totalAmount; }
}
// 状态机收口:状态流转规则只此一份
public enum OrderStatus {
PENDING_PAY {
@Override OrderStatus transitToPaid() { return PAID; }
},
PAID {
@Override OrderStatus transitToPaid() { throw new IllegalStateException("订单已支付,不可重复支付"); }
@Override OrderStatus transitToClosed() { return CLOSED; } // 支付后可关闭(退款/取消发货)
},
CLOSED, SHIPPED;
OrderStatus transitToPaid() { throw new IllegalStateException("当前状态不可支付"); }
OrderStatus transitToClosed() { throw new IllegalStateException("当前状态不可关闭"); }
}仓储接口(定义在领域层)与实现(基础设施层):
// 领域层:仓储接口,语义是存取聚合
public interface OrderRepository {
OrderId nextId(); // 生成 ID 的策略由基础设施提供(如雪花)
Order findById(OrderId id);
void save(Order order); // 聚合整体落库(含明细)
}
// 基础设施层:MyBatis/JPA 细节实现接口
@Repository
public class OrderRepositoryImpl implements OrderRepository {
// 组装 OrderMapper / OrderItemMapper,实现聚合与表的双向转换
}// 领域层:仓储接口,语义是存取聚合
public interface OrderRepository {
OrderId nextId(); // 生成 ID 的策略由基础设施提供(如雪花)
Order findById(OrderId id);
void save(Order order); // 聚合整体落库(含明细)
}
// 基础设施层:MyBatis/JPA 细节实现接口
@Repository
public class OrderRepositoryImpl implements OrderRepository {
// 组装 OrderMapper / OrderItemMapper,实现聚合与表的双向转换
}领域服务与领域事件:
// 领域事件(领域层定义,纯数据,过去时命名)
public record OrderCreatedEvent(Long orderId, int itemCount, BigDecimal totalAmount) {}
// 支付成功事件:由 Order.pay() 登记,支付上下文/通知上下文订阅
public record OrderPaidEvent(Long orderId) {}
// 领域服务:协调"下单"跨聚合的步骤
public class OrderDomainService {
// 说明:库存预占通过发布 OrderCreatedEvent 由库存上下文订阅,
// 若强一致则在本服务内调用库存网关(防超卖见《经典系统设计案例》)
}// 领域事件(领域层定义,纯数据,过去时命名)
public record OrderCreatedEvent(Long orderId, int itemCount, BigDecimal totalAmount) {}
// 支付成功事件:由 Order.pay() 登记,支付上下文/通知上下文订阅
public record OrderPaidEvent(Long orderId) {}
// 领域服务:协调"下单"跨聚合的步骤
public class OrderDomainService {
// 说明:库存预占通过发布 OrderCreatedEvent 由库存上下文订阅,
// 若强一致则在本服务内调用库存网关(防超卖见《经典系统设计案例》)
}应用层与接口层:
// 应用层:编排用例、控制事务、提交后发事件
@Service
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final OrderEventPublisher eventPublisher; // 内部封装 MQ
@Transactional
public OrderId createOrder(CreateOrderCommand cmd) {
List<OrderItem> items = cmd.toItems(); // 请求 DTO -> 领域对象(assembler)
Order order = Order.place(orderRepository.nextId(), cmd.address(), items);
orderRepository.save(order); // 本事务内落库
// 事务提交后发布事件(Spring: TransactionSynchronization 注册 afterCommit)
afterCommit(() -> eventPublisher.publish(order.collectEvents()));
return order.id();
}
}
// 接口层:只做参数接收与响应转换,不含业务规则
@RestController
public class OrderController {
@PostMapping("/orders")
public ResponseEntity<OrderResponse> create(@RequestBody @Valid CreateOrderRequest request) {
OrderId orderId = orderAppService.createOrder(request.toCommand());
return ResponseEntity.ok(new OrderResponse(orderId.value(), "下单成功"));
}
}// 应用层:编排用例、控制事务、提交后发事件
@Service
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final OrderEventPublisher eventPublisher; // 内部封装 MQ
@Transactional
public OrderId createOrder(CreateOrderCommand cmd) {
List<OrderItem> items = cmd.toItems(); // 请求 DTO -> 领域对象(assembler)
Order order = Order.place(orderRepository.nextId(), cmd.address(), items);
orderRepository.save(order); // 本事务内落库
// 事务提交后发布事件(Spring: TransactionSynchronization 注册 afterCommit)
afterCommit(() -> eventPublisher.publish(order.collectEvents()));
return order.id();
}
}
// 接口层:只做参数接收与响应转换,不含业务规则
@RestController
public class OrderController {
@PostMapping("/orders")
public ResponseEntity<OrderResponse> create(@RequestBody @Valid CreateOrderRequest request) {
OrderId orderId = orderAppService.createOrder(request.toCommand());
return ResponseEntity.ok(new OrderResponse(orderId.value(), "下单成功"));
}
}6.6 分层协作时序
6.7 代码骨架要点回顾
- 规则(状态机、金额计算、非空校验)全部在领域层,Service 无法绕过;
Money、Address用值对象消灭"成对字段 + 散落校验";- 仓储接口在领域层、实现在基础设施层,满足依赖倒置;
- 领域事件先登记、事务提交后再发,避免"回滚了消息却已发";
- 接口层/应用层不出现业务规则,只做编排。
七、DDD 落地常见坑与适用边界
7.1 什么时候不该用 DDD
| 场景 | 为什么不该用 | 该用什么 |
|---|---|---|
| 纯 CRUD 报表类 | 没有复杂规则,建模是负资产 | 传统三层 + 贫血模型 |
| 团队不熟业务领域 | 模型会反复推翻 | 先做事件风暴补齐领域知识 |
| 无业务专家可对话 | 通用语言无从建立 | 原型先行,缓上 DDD |
| 性能压倒一切且逻辑简单 | 聚合拆分会干扰 SQL 优化 | 数据驱动开发 |
| 遗留系统推倒重来式"上 DDD" | 一次性重写风险极高 | 绞杀者模式渐进替换 |
DDD 的适用特征可以一句话概括:规则复杂、状态多、变更频繁、值得长期投入的核心业务。
7.2 常见落地坑清单
| 坑 | 现象 | 对策 |
|---|---|---|
| 伪 DDD | 只有包结构是 DDD,Service 仍是脚本海洋 | 先做"聚合根方法收口规则"的最小改进 |
| 事务范围过大 | 一个用例锁多个聚合 | 一次事务只改一个聚合,跨聚合用事件 |
| 过度拆分聚合 | 每个实体一个聚合、跨聚合调用泛滥 | 用"一致性要求 + 变更频率"界定聚合 |
| 仓储变 DAO | Repository 方法返回行数据、按字段查 | 仓储只存取聚合根,查询走专门读模型 |
| 序列化/ORM 反噬 | 实体夹带 ORM 注解、JSON 注解,领域层不再纯粹 | 领域层纯 POJO,映射放基础设施层 |
| 全面铺开 DDD | 全系统同一标准建模,成本失控 | 核心域精雕、通用域采购、中间层适度 |
| 忽略通用语言 | 代码术语与业务术语两套 | 术语表进代码评审标准 |
7.3 DDD 落地路线图
- 圈定核心域(1 个即可,如订单);
- 事件风暴产出事件流与聚合草图(1~2 周,业务专家必须参与);
- 在现有代码库内按上下文分包重构,先不拆服务;
- 核心聚合转充血,配套单元测试锁定规则;
- 边界稳定后再按需物理拆服务(参考《微服务架构与拆分》)。
7.4 落地检查清单
- [ ] 术语表与代码命名一致(通用语言无歧义);
- [ ] 核心/支撑/通用域的投入分配明确;
- [ ] 一次事务只改一个聚合根;
- [ ] 聚合状态流转只能走聚合根方法;
- [ ] 领域层不 import 任何 Spring/MyBatis/JSON 注解;
- [ ] 事件在事务提交后发布;
- [ ] 读多写杂的查询已用读模型/CQRS 方式与写模型解耦(按需)。
Cao Note