Skip to content

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 战略设计产出物 ​

  1. 事件风暴/领域分析结果:业务事件流(用户下单 → 订单已创建 → 已支付 → 已发货);
  2. 子域清单:核心/支撑/通用域及归属;
  3. 上下文地图:所有限界上下文与映射关系;
  4. 通用语言术语表。

战略设计阶段不出现任何表结构或技术选型,先于技术做业务切分。


三、战术设计 ​

3.1 实体(Entity) ​

有唯一标识且跨生命周期状态连续变化的对象。实体不靠属性相等来判断同一性,而靠 ID。

java
// 订单 ID 相同即同一订单,即使内容被修改
public class Order {
    private OrderId id;      // 身份标识
    private OrderStatus status; // 状态随时间变化
}
// 订单 ID 相同即同一订单,即使内容被修改
public class Order {
    private OrderId id;      // 身份标识
    private OrderStatus status; // 状态随时间变化
}

3.2 值对象(Value Object) ​

描述属性本身、无标识、靠属性整体相等判断同一性、不可变的对象。金额 + 币种、地址、时间段都是典型值对象。

java
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 聚合根统一计算金额。

聚合设计的两个常见错误:

  1. 超大聚合:把订单 + 全部商品 + 全部操作日志塞进一个聚合,导致并发写互相阻塞、事务过大——应拆小(订单主聚合 + 独立的操作日志聚合);
  2. 误设聚合根:让内部实体直接暴露给外部修改,绕过聚合根校验。

3.4 仓储(Repository) ​

仓储是"聚合的存取仓库",接口定义在领域层,实现在基础设施层:

  • 语义是"拿聚合、存聚合",不是"拼 SQL";
  • 只对聚合根提供仓储,聚合内部对象通过聚合根访问;
  • 实现可以是 MyBatis、JPA 或内存 Map,领域层不关心。

仓储接口定义在领域层正是依赖倒置的体现:领域逻辑依赖抽象,MyBatis 细节在基础设施层实现。

3.5 领域服务(Domain Service) ​

当某个业务动作不属于任何实体/值对象、或需要协调多个聚合时,放在领域服务中。典型场景:下单要同时检查库存聚合与创建订单聚合时,跨聚合编排逻辑放领域服务,但事务边界仍各自独立(通过领域事件或应用层协调)。

3.6 领域事件(Domain Event) ​

聚合状态变化产生的、已发生事实的记录(过去时命名:OrderCreatedEvent、PaymentSucceededEvent)。作用:

  1. 解耦聚合间的副作用(订单创建后发消息、记审计、扣库存都订阅事件);
  2. 作为跨上下文/跨服务最终一致性的载体(配合 MQ);
  3. 支撑审计与事件溯源。

注意:事件的产生在领域层(聚合根方法内 registerEvent),事件的发布在应用层/基础设施层(事务提交后发 MQ),避免"事务回滚了消息却已发出"。

3.7 工厂与规格 ​

  • 工厂:聚合创建逻辑复杂(需组装多个对象、处理默认值)时,用工厂方法或独立 Factory 收拢创建逻辑,避免聚合根构造函数臃肿;
  • 规格(Specification):把"是否满足某业务条件"的校验封装为可复用对象(如 OverdueOrderSpec),用于仓储查询与内存校验的统一。

3.8 CQRS(命令查询职责分离) ​

当读模型与写模型的形态差距过大时,把命令(写)与查询(读)分离:

  • 写侧走领域模型(实体、聚合、事务);
  • 读侧直接投影出查询专用模型(宽表、缓存、搜索索引、甚至独立的只读库)。

CQRS 是战术手段而非银弹:只有出现"复杂的写 + 高频多样的读"(报表、Feed、订单列表)时引入;简单 CRUD 强行 CQRS 是过度设计。可降级方案是应用层读模型 + 领域层写模型,不引入独立数据库。

3.9 事件溯源(Event Sourcing)概念 ​

不存聚合的"当前状态",而存产生状态的全部事件序列,当前状态由事件回放得到:

text
订单创建 -> 订单支付 -> 订单发货   (事件流)
          replay
状态 = f(事件1..n)
订单创建 -> 订单支付 -> 订单发货   (事件流)
          replay
状态 = f(事件1..n)
  • 优点:完整审计、可回溯任意历史时点、天然支撑领域事件;
  • 代价:事件模型与存储快照都要管理、回放性能需快照优化、查询必须配 CQRS;
  • 结论:仅当"审计追溯是硬性需求"(金融、合规)时才考虑,普通业务用"状态 + 流水日志"通常更划算。

四、贫血模型 vs 充血模型 ​

4.1 两种模型的形态 ​

维度贫血模型充血模型
实体类内容只有 getter/setter(纯数据)数据 + 业务行为
业务逻辑位置全部堆在 Service内聚到实体/值对象/领域服务
校验与规则Service 里 if 散落对象创建/方法内强制保证
测试难度测 Service 需 mock 一堆领域对象可直接单测
新人上手结构简单但业务真相在 Service 海洋里需理解建模,门槛略高
维护风险同一规则多处实现、改漏一处出 bug规则单一来源、改动集中

4.2 贫血模型的代表代码 ​

java
// 贫血: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 充血模型的代表代码 ​

java
// 充血:状态机与规则在聚合根内,任何调用方都无法绕过
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 贫血模型盛行的现实原因 ​

  1. 数据库优先的开发惯性:先建表、MyBatis 反向生成实体,实体天然只是表的投影;
  2. 事务脚本式 Service在简单 CRUD 里确实高效;
  3. 团队对 DDD 建模掌握不足,强行充血容易把"伪充血"写成"把 SQL 逻辑搬进实体"。

务实结论:业务以简单 CRUD 为主时贫血模型完全够用;业务规则复杂(状态机、计价、风控)时,让核心域的模型充血,外围 CRUD 保持简单。DDD 不要求全系统充血,只要求"复杂处建模、简单处直接"。

4.5 渐进式充血路线 ​

  1. 从核心域一个高频变更的聚合开始(如订单);
  2. 先把"状态流转规则"收进实体方法(消灭散落的 if);
  3. 再引入值对象收敛成对字段与校验(金额、地址);
  4. 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 包结构 ​

text
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.java
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.java

依赖规则(单向):interfaces → application → domain ← infrastructure。领域层最内层,谁都不依赖它,它也不依赖谁。

6.5 核心代码骨架 ​

领域层先定义值对象与实体:

java
// 值对象: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("当前状态不可关闭"); }
}

仓储接口(定义在领域层)与实现(基础设施层):

java
// 领域层:仓储接口,语义是存取聚合
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,实现聚合与表的双向转换
}

领域服务与领域事件:

java
// 领域事件(领域层定义,纯数据,过去时命名)
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 由库存上下文订阅,
    // 若强一致则在本服务内调用库存网关(防超卖见《经典系统设计案例》)
}

应用层与接口层:

java
// 应用层:编排用例、控制事务、提交后发事件
@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 代码骨架要点回顾 ​

  1. 规则(状态机、金额计算、非空校验)全部在领域层,Service 无法绕过;
  2. Money、Address 用值对象消灭"成对字段 + 散落校验";
  3. 仓储接口在领域层、实现在基础设施层,满足依赖倒置;
  4. 领域事件先登记、事务提交后再发,避免"回滚了消息却已发";
  5. 接口层/应用层不出现业务规则,只做编排。

七、DDD 落地常见坑与适用边界 ​

7.1 什么时候不该用 DDD ​

场景为什么不该用该用什么
纯 CRUD 报表类没有复杂规则,建模是负资产传统三层 + 贫血模型
团队不熟业务领域模型会反复推翻先做事件风暴补齐领域知识
无业务专家可对话通用语言无从建立原型先行,缓上 DDD
性能压倒一切且逻辑简单聚合拆分会干扰 SQL 优化数据驱动开发
遗留系统推倒重来式"上 DDD"一次性重写风险极高绞杀者模式渐进替换

DDD 的适用特征可以一句话概括:规则复杂、状态多、变更频繁、值得长期投入的核心业务。

7.2 常见落地坑清单 ​

坑现象对策
伪 DDD只有包结构是 DDD,Service 仍是脚本海洋先做"聚合根方法收口规则"的最小改进
事务范围过大一个用例锁多个聚合一次事务只改一个聚合,跨聚合用事件
过度拆分聚合每个实体一个聚合、跨聚合调用泛滥用"一致性要求 + 变更频率"界定聚合
仓储变 DAORepository 方法返回行数据、按字段查仓储只存取聚合根,查询走专门读模型
序列化/ORM 反噬实体夹带 ORM 注解、JSON 注解,领域层不再纯粹领域层纯 POJO,映射放基础设施层
全面铺开 DDD全系统同一标准建模,成本失控核心域精雕、通用域采购、中间层适度
忽略通用语言代码术语与业务术语两套术语表进代码评审标准

7.3 DDD 落地路线图 ​

  1. 圈定核心域(1 个即可,如订单);
  2. 事件风暴产出事件流与聚合草图(1~2 周,业务专家必须参与);
  3. 在现有代码库内按上下文分包重构,先不拆服务;
  4. 核心聚合转充血,配套单元测试锁定规则;
  5. 边界稳定后再按需物理拆服务(参考《微服务架构与拆分》)。

7.4 落地检查清单 ​

  • [ ] 术语表与代码命名一致(通用语言无歧义);
  • [ ] 核心/支撑/通用域的投入分配明确;
  • [ ] 一次事务只改一个聚合根;
  • [ ] 聚合状态流转只能走聚合根方法;
  • [ ] 领域层不 import 任何 Spring/MyBatis/JSON 注解;
  • [ ] 事件在事务提交后发布;
  • [ ] 读多写杂的查询已用读模型/CQRS 方式与写模型解耦(按需)。