一、为什么你需要DDD?传统架构的致命痛点
在Java后端开发中,绝大多数项目都采用经典的三层架构:Controller→Service→DAO。这种架构在简单CRUD场景下高效易用,但随着业务复杂度提升,会暴露出无法解决的致命问题:
- Service层无限膨胀,沦为“大泥球”:业务规则散落在Service的各个方法中,一个Service类动辄几千行,逻辑耦合严重,修改一处代码引发多处bug。
- 业务与技术深度绑定:以数据库表为核心设计系统,业务逻辑完全依赖数据模型,一旦表结构变更,全链路代码都要修改,可维护性极差。
- 团队协作成本极高:产品、开发、测试没有统一的业务术语,产品说的“订单”和开发写的
OrderInfo、测试说的“工单”完全不是一个概念,需求沟通偏差率极高。 - 系统迭代效率指数级下降:随着业务迭代,代码耦合度越来越高,新增一个需求需要通读全量代码,生怕影响原有逻辑,最终陷入“不敢改、改不动”的困境。
DDD(领域驱动设计)正是为解决复杂业务系统的这些痛点而生,它由Eric Evans在《领域驱动设计:软件核心复杂性应对之道》中提出,经过20多年的发展,已经成为复杂业务系统设计的行业标准实践。
二、DDD核心本质:到底什么是领域驱动设计
DDD的本质不是一套架构规范,而是一种以业务领域为核心的设计思想,它的核心逻辑是:先搞清楚业务是什么、业务规则有哪些,再基于业务去设计技术实现,而不是反过来先设计数据库表,再把业务逻辑塞进去。
DDD的核心目标有两个:
- 统一语言:让整个团队(产品、开发、测试、运维)使用同一套业务术语,消除沟通偏差。
- 边界隔离:把复杂的业务拆分成多个独立的边界,每个边界内的业务逻辑高内聚,边界之间低耦合,从根源上解决代码腐化问题。
DDD的设计分为两大核心阶段:战略设计和战术设计。战略设计解决“业务边界怎么划”的问题,战术设计解决“代码怎么写”的问题,二者缺一不可。
三、DDD战略设计:划清业务边界,从根源解决耦合
战略设计是DDD的灵魂,也是绝大多数开发者落地DDD时最容易忽略的部分。没有清晰的战略设计,再完美的战术代码都是空中楼阁。
3.1 通用语言:DDD的基石
通用语言是团队在特定业务边界内,统一使用的、无歧义的业务术语集合。它要求产品文档、代码、接口、数据库表、测试用例都使用同一套术语,彻底消除“翻译成本”。
- 反例:产品叫“订单履约”,开发代码里写
OrderSendService,数据库表叫t_delivery,测试用例里写“订单发货流程”。 - 正例:全团队统一使用“订单履约”术语,代码里的类、接口、表名全部围绕这个术语命名,确保任何人看到都能立刻理解业务含义。
3.2 限界上下文:业务的最小边界
限界上下文是通用语言的边界,也是业务的最小自治单元。每个限界上下文对应一个独立的业务领域,内部有自己的通用语言、业务规则和数据模型,和其他上下文通过明确的协议交互。
举个电商系统的例子,我们可以拆分为以下限界上下文:
- 订单限界上下文:负责订单的创建、支付、发货、完成、取消全生命周期管理
- 库存限界上下文:负责商品库存的查询、扣减、归还、盘点管理
- 支付限界上下文:负责支付渠道对接、支付流水管理、退款处理
- 用户限界上下文:负责用户信息、会员等级、收货地址管理
3.3 上下文映射:跨边界的交互规则
限界上下文不是孤立的,需要明确上下文之间的交互关系,核心有3种常用映射关系:
- 合作关系:两个上下文的团队共同制定交互协议,同步变更,比如订单和支付上下文。
- 客户-供应商关系:上游上下文(供应商)提供接口,下游上下文(客户)调用,上游需要考虑下游的需求,比如商品和库存上下文。
- 防腐层(ACL):下游上下文在调用上游接口时,增加一层适配层,把上游的模型转换为自己的领域模型,彻底隔离上游的变化对自己的影响。这是DDD中最常用的解耦手段,几乎所有跨上下文调用都应该使用防腐层。
四、DDD战术设计:落地实现的核心概念全解
战略设计划清了业务边界,战术设计就是把边界内的业务逻辑落地为可运行的代码,核心概念如下:
4.1 实体与值对象
实体
实体是具有唯一标识、可变、有生命周期的业务对象,它的相等性由唯一标识决定,和属性无关。
- 核心特征:有唯一ID,属性可以变化,有明确的生命周期,包含业务规则。
- 示例:订单、用户、商品,同一个用户修改了昵称,还是同一个用户,因为用户ID不变。
值对象
值对象是无唯一标识、不可变、无生命周期的业务对象,它的相等性由所有属性的值共同决定。
- 核心特征:不可变,无唯一ID,用来描述实体的特征,本身是一个完整的概念。
- 示例:收货地址、金额、坐标,两个地址的省份、城市、详细地址、手机号都相同,我们就认为是同一个地址,不需要单独的ID。
4.2 聚合与聚合根
聚合是DDD中最核心、最容易用错的概念。聚合是一组强关联的实体和值对象的集合,是数据修改和事务的最小单元。
聚合根是聚合里的特殊实体,是聚合的唯一外部访问入口,外部只能通过聚合根访问聚合内的所有对象,聚合内的对象生命周期完全由聚合根管理。
聚合的4条铁律(来自《实现领域驱动设计》,必须严格遵守):
- 聚合根是聚合的唯一外部入口,外部绝对不能直接访问聚合内的实体或值对象。
- 一个事务只能修改一个聚合根,跨聚合的修改必须通过最终一致性实现。
- 聚合之间只能通过聚合根的ID引用,绝对不能直接引用聚合根对象。
- 聚合根删除时,聚合内的所有对象必须一起删除。
4.3 领域服务
领域服务用来处理跨多个实体的业务规则,它本身没有状态,只包含业务逻辑。
- 核心原则:领域服务只处理业务规则,不做流程编排;如果一个业务规则可以放在聚合根里,就绝对不要放在领域服务里。
- 示例:订单创建前的库存校验,需要同时校验多个订单项的库存,跨多个订单项实体,就可以放在订单领域服务里。
4.4 仓储
仓储是用来管理聚合生命周期的接口,它的核心是面向聚合,而非面向数据。
- 仓储接口定义在领域层,实现在基础设施层,遵循依赖倒置原则,领域层不依赖任何技术实现。
- 核心区别:DAO是面向单表的,操作的是数据;仓储是面向聚合的,操作的是完整的业务对象。仓储的
save方法必须保存整个聚合,findById方法必须返回完整的聚合,包括聚合内的所有实体和值对象。
4.5 领域事件
领域事件是用来记录聚合中发生的业务事实,实现跨聚合的最终一致性。
- 核心特征:不可变,包含事件发生的时间、相关业务标识,一旦发布就不能修改。
- 示例:订单创建成功后,发布
OrderCreatedEvent,库存上下文监听事件,异步扣减库存,实现订单和库存两个聚合的解耦。
4.6 工厂
工厂是用来封装复杂聚合对象的创建逻辑,确保聚合对象创建时就满足所有业务规则。
- 核心原则:聚合对象的创建逻辑必须放在工厂里,外部不能直接new聚合对象,确保创建出来的聚合对象永远是合法的。
- 示例:订单的创建需要校验地址合法性、订单项非空、计算总金额、初始化状态,这些逻辑都放在订单的工厂方法里。
五、DDD标准分层架构:职责清晰的四层架构详解
DDD的标准分层架构严格遵循依赖倒置原则,领域层是绝对的核心,不依赖任何其他层,所有外层都依赖领域层的抽象。
5.1 用户接口层(Interfaces)
- 核心职责:负责和用户交互,接收用户请求,返回响应结果,不包含任何业务逻辑。
- 包含内容:Controller、请求/响应DTO、统一返回结果、参数校验。
- 核心原则:只做请求的接收和响应,不做任何业务处理,直接调用应用层的服务。
5.2 应用层(Application)
- 核心职责:负责业务流程的编排,是用例的入口,不包含任何业务规则。
- 包含内容:应用服务、DTO、事件发布、事务管理。
- 核心原则:应用服务只负责“做什么”,不负责“怎么做”,所有业务规则都必须委托给领域层的聚合根或领域服务。事务边界在这里定义,一个用例对应一个事务。
5.3 领域层(Domain)
- 核心职责:存放所有的业务规则,是系统的核心,绝对不依赖任何其他层。
- 包含内容:聚合根、实体、值对象、领域服务、仓储接口、领域事件、工厂。
- 核心原则:所有的业务规则都必须在这里实现,绝对不能泄露到其他层。这里的代码和技术无关,只描述业务是什么。
5.4 基础设施层(Infrastructure)
- 核心职责:负责所有技术细节的实现,为其他层提供技术支持。
- 包含内容:仓储实现、数据库操作、缓存、RPC调用、消息队列、防腐层实现。
- 核心原则:实现领域层定义的抽象接口,不包含任何业务规则,技术实现的变更不能影响领域层。
六、生产级实战:电商订单系统全链路DDD落地
我们以电商订单系统为例,基于JDK17、Spring Boot 3.2.5、MyBatis Plus 3.5.7,实现完整的DDD代码。
6.1 项目结构
com.jam.demo
├── interfaces // 用户接口层
│ ├── controller
│ ├── dto
│ └── common
├── application // 应用层
│ ├── service
│ ├── dto
│ └── event
├── domain // 领域层
│ ├── aggregate // 聚合根
│ ├── entity // 实体
│ ├── valueobject // 值对象
│ ├── service // 领域服务
│ ├── repository // 仓储接口
│ ├── event // 领域事件
│ └── factory // 工厂
└── infrastructure // 基础设施层
├── mapper
├── po
├── repository
├── acl
├── config
└── converter
6.2 核心依赖pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.jam</groupId>
<artifactId>ddd-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ddd-demo</name>
<description>DDD领域驱动设计实战demo</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<springdoc.version>2.5.0</springdoc.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<guava.version>32.1.3-jre</guava.version>
<fastjson2.version>2.0.52</fastjson2.version>
<lombok.version>1.18.32</lombok.version>
<openfeign.version>4.1.1</openfeign.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>${openfeign.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
6.3 MySQL8.0建表SQL
CREATE TABLE IF NOT EXISTS `t_order` (
`order_id` BIGINT NOT NULL COMMENT '订单ID',
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`order_status` VARCHAR(32) NOT NULL COMMENT '订单状态',
`total_amount` DECIMAL(12,2) NOT NULL COMMENT '订单总金额',
`currency` VARCHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种',
`province` VARCHAR(32) NOT NULL COMMENT '省份',
`city` VARCHAR(32) NOT NULL COMMENT '城市',
`district` VARCHAR(32) NOT NULL COMMENT '区县',
`detail_address` VARCHAR(255) NOT NULL COMMENT '详细地址',
`phone` VARCHAR(11) NOT NULL COMMENT '联系电话',
`receiver` VARCHAR(32) NOT NULL COMMENT '收件人',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`version` INT NOT NULL DEFAULT 1 COMMENT '版本号,乐观锁',
PRIMARY KEY (`order_id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_order_status` (`order_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表';
CREATE TABLE IF NOT EXISTS `t_order_item` (
`order_item_id` BIGINT NOT NULL COMMENT '订单项ID',
`order_id` BIGINT NOT NULL COMMENT '订单ID',
`sku_id` BIGINT NOT NULL COMMENT '商品SKU ID',
`sku_name` VARCHAR(255) NOT NULL COMMENT '商品名称',
`quantity` INT NOT NULL COMMENT '购买数量',
`unit_price` DECIMAL(12,2) NOT NULL COMMENT '商品单价',
`currency` VARCHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种',
`subtotal` DECIMAL(12,2) NOT NULL COMMENT '小计金额',
PRIMARY KEY (`order_item_id`),
INDEX `idx_order_id` (`order_id`),
INDEX `idx_sku_id` (`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单项表';
6.4 领域层核心代码
6.4.1 值对象
package com.jam.demo.domain.valueobject;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Objects;
/**
* 金额值对象
* @author ken
* @param amount 金额数值
* @param currency 币种
*/
public record Money(BigDecimal amount, String currency) {
public Money {
if (Objects.isNull(amount)) {
throw new IllegalArgumentException("金额不能为空");
}
if (!StringUtils.hasText(currency)) {
throw new IllegalArgumentException("币种不能为空");
}
amount = amount.setScale(2, RoundingMode.HALF_UP);
}
}
package com.jam.demo.domain.valueobject;
import org.springframework.util.StringUtils;
/**
* 收货地址值对象
* @author ken
* @param province 省份
* @param city 城市
* @param district 区县
* @param detailAddress 详细地址
* @param phone 联系电话
* @param receiver 收件人
*/
public record Address(String province, String city, String district, String detailAddress, String phone, String receiver) {
public boolean isValid() {
return StringUtils.hasText(province)
&& StringUtils.hasText(city)
&& StringUtils.hasText(district)
&& StringUtils.hasText(detailAddress)
&& StringUtils.hasText(phone)
&& StringUtils.hasText(receiver)
&& phone.matches("^1[3-9]\\d{9}$");
}
}
package com.jam.demo.domain.valueobject;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* 订单状态枚举,值对象
* @author ken
*/
@Schema(description = "订单状态枚举")
public enum OrderStatus {
CREATED("已创建"),
PAID("已支付"),
DELIVERED("已发货"),
COMPLETED("已完成"),
CANCELLED("已取消");
private final String description;
OrderStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
6.4.2 实体
package com.jam.demo.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.jam.demo.domain.valueobject.Money;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.util.Objects;
/**
* 订单项实体,订单聚合内的实体
* @author ken
* @since 2026-03-05
*/
@TableName("t_order_item")
@Schema(description = "订单项实体")
@Getter
@Setter
@NoArgsConstructor
public class OrderItem {
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "订单项ID")
private Long orderItemId;
@Schema(description = "订单ID")
private Long orderId;
@Schema(description = "商品SKU ID")
private Long skuId;
@Schema(description = "商品名称")
private String skuName;
@Schema(description = "购买数量")
private Integer quantity;
@Schema(description = "商品单价")
private Money unitPrice;
@Schema(description = "订单项小计金额")
private Money subtotal;
public static OrderItem create(Long skuId, String skuName, Integer quantity, Money unitPrice) {
if (ObjectUtils.isEmpty(skuId)) {
throw new IllegalArgumentException("商品SKU ID不能为空");
}
if (!StringUtils.hasText(skuName)) {
throw new IllegalArgumentException("商品名称不能为空");
}
if (ObjectUtils.isEmpty(quantity) || quantity <= 0) {
throw new IllegalArgumentException("购买数量必须大于0");
}
if (ObjectUtils.isEmpty(unitPrice) || unitPrice.amount().compareTo(java.math.BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("商品单价必须大于0");
}
OrderItem orderItem = new OrderItem();
orderItem.skuId = skuId;
orderItem.skuName = skuName;
orderItem.quantity = quantity;
orderItem.unitPrice = unitPrice;
orderItem.subtotal = new Money(unitPrice.amount().multiply(new java.math.BigDecimal(quantity)), unitPrice.currency());
return orderItem;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrderItem orderItem = (OrderItem) o;
return Objects.equals(orderItemId, orderItem.orderItemId);
}
@Override
public int hashCode() {
return Objects.hash(orderItemId);
}
}
6.4.3 聚合根
package com.jam.demo.domain.aggregate;
import com.baomidou.mybatisplus.annotation.*;
import com.jam.demo.domain.entity.OrderItem;
import com.jam.demo.domain.valueobject.Address;
import com.jam.demo.domain.valueobject.Money;
import com.jam.demo.domain.valueobject.OrderStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
/**
* 订单聚合根
* @author ken
* @since 2026-03-05
*/
@TableName("t_order")
@Schema(description = "订单聚合根")
@Getter
@Setter
@NoArgsConstructor
@Slf4j
public class Order {
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "订单ID")
private Long orderId;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "订单状态")
private OrderStatus orderStatus;
@Schema(description = "订单总金额")
private Money totalAmount;
@Schema(description = "收货地址")
private Address deliveryAddress;
@TableField(exist = false)
@Schema(description = "订单项列表")
private List<OrderItem> orderItemList;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Version
@Schema(description = "版本号")
private Integer version;
public static Order create(Long userId, Address deliveryAddress, List<OrderItem> orderItemList) {
if (ObjectUtils.isEmpty(userId)) {
throw new IllegalArgumentException("用户ID不能为空");
}
if (!deliveryAddress.isValid()) {
throw new IllegalArgumentException("收货地址不合法");
}
if (CollectionUtils.isEmpty(orderItemList)) {
throw new IllegalArgumentException("订单项不能为空");
}
Order order = new Order();
order.userId = userId;
order.deliveryAddress = deliveryAddress;
order.orderItemList = orderItemList;
order.totalAmount = order.calculateTotalAmount();
order.orderStatus = OrderStatus.CREATED;
order.createTime = LocalDateTime.now();
order.updateTime = LocalDateTime.now();
order.version = 1;
log.info("订单创建成功,用户ID:{}, 订单总金额:{}", userId, order.totalAmount);
return order;
}
private Money calculateTotalAmount() {
BigDecimal total = orderItemList.stream()
.map(item -> item.getSubtotal().amount())
.reduce(BigDecimal.ZERO, BigDecimal::add);
return new Money(total, "CNY");
}
public void paySuccess(Money payAmount) {
if (!OrderStatus.CREATED.equals(this.orderStatus)) {
throw new IllegalStateException("订单状态异常,无法支付");
}
if (!this.totalAmount.equals(payAmount)) {
throw new IllegalArgumentException("支付金额与订单总金额不符");
}
this.orderStatus = OrderStatus.PAID;
this.updateTime = LocalDateTime.now();
log.info("订单支付成功,订单ID:{}, 支付金额:{}", this.orderId, payAmount);
}
public void deliver() {
if (!OrderStatus.PAID.equals(this.orderStatus)) {
throw new IllegalStateException("订单状态异常,无法发货");
}
this.orderStatus = OrderStatus.DELIVERED;
this.updateTime = LocalDateTime.now();
log.info("订单发货成功,订单ID:{}", this.orderId);
}
public void complete() {
if (!OrderStatus.DELIVERED.equals(this.orderStatus)) {
throw new IllegalStateException("订单状态异常,无法完成");
}
this.orderStatus = OrderStatus.COMPLETED;
this.updateTime = LocalDateTime.now();
log.info("订单完成,订单ID:{}", this.orderId);
}
public void cancel() {
if (!OrderStatus.CREATED.equals(this.orderStatus)) {
throw new IllegalStateException("订单状态异常,无法取消");
}
this.orderStatus = OrderStatus.CANCELLED;
this.updateTime = LocalDateTime.now();
log.info("订单取消成功,订单ID:{}", this.orderId);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Order order = (Order) o;
return Objects.equals(orderId, order.orderId);
}
@Override
public int hashCode() {
return Objects.hash(orderId);
}
}
6.4.4 领域服务
package com.jam.demo.domain.service;
import com.jam.demo.domain.entity.OrderItem;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.util.List;
import java.util.Map;
/**
* 订单领域服务
* @author ken
* @since 2026-03-05
*/
@Slf4j
@Component
public class OrderDomainService {
public void checkStock(List<OrderItem> orderItemList, Map<Long, Integer> stockMap) {
if (CollectionUtils.isEmpty(orderItemList)) {
throw new IllegalArgumentException("订单项不能为空");
}
if (CollectionUtils.isEmpty(stockMap)) {
throw new IllegalArgumentException("库存信息不能为空");
}
for (OrderItem item : orderItemList) {
Integer stock = stockMap.get(item.getSkuId());
if (ObjectUtils.isEmpty(stock) || stock < item.getQuantity()) {
throw new IllegalArgumentException("商品SKU:" + item.getSkuId() + "库存不足");
}
}
log.info("订单商品库存校验通过");
}
}
6.4.5 仓储接口
package com.jam.demo.domain.repository;
import com.jam.demo.domain.aggregate.Order;
/**
* 订单仓储接口
* @author ken
* @since 2026-03-05
*/
public interface OrderRepository {
Order save(Order order);
Order findById(Long orderId);
void deleteById(Long orderId);
}
6.4.6 领域事件
package com.jam.demo.domain.event;
import com.jam.demo.domain.entity.OrderItem;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
import java.time.LocalDateTime;
import java.util.List;
/**
* 订单创建成功领域事件
* @author ken
* @since 2026-03-05
*/
@Getter
public class OrderCreatedEvent extends ApplicationEvent {
private final Long orderId;
private final Long userId;
private final List<OrderItem> orderItemList;
private final LocalDateTime eventTime;
public OrderCreatedEvent(Object source, Long orderId, Long userId, List<OrderItem> orderItemList) {
super(source);
this.orderId = orderId;
this.userId = userId;
this.orderItemList = orderItemList;
this.eventTime = LocalDateTime.now();
}
}
6.5 应用层核心代码
package com.jam.demo.application.service;
import com.jam.demo.application.dto.OrderCreateDTO;
import com.jam.demo.domain.aggregate.Order;
import com.jam.demo.domain.entity.OrderItem;
import com.jam.demo.domain.event.OrderCreatedEvent;
import com.jam.demo.domain.repository.OrderRepository;
import com.jam.demo.domain.service.OrderDomainService;
import com.jam.demo.domain.valueobject.Address;
import com.jam.demo.domain.valueobject.Money;
import com.jam.demo.infrastructure.acl.StockFeignClient;
import com.jam.demo.interfaces.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 订单应用服务
* @author ken
* @since 2026-03-05
*/
@Slf4j
@Service
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final OrderDomainService orderDomainService;
private final ApplicationEventPublisher eventPublisher;
private final TransactionTemplate transactionTemplate;
private final StockFeignClient stockFeignClient;
public OrderApplicationService(OrderRepository orderRepository,
OrderDomainService orderDomainService,
ApplicationEventPublisher eventPublisher,
TransactionTemplate transactionTemplate,
StockFeignClient stockFeignClient) {
this.orderRepository = orderRepository;
this.orderDomainService = orderDomainService;
this.eventPublisher = eventPublisher;
this.transactionTemplate = transactionTemplate;
this.stockFeignClient = stockFeignClient;
}
@Transactional(rollbackFor = Exception.class)
public Long createOrder(OrderCreateDTO orderCreateDTO) {
log.info("开始创建订单,用户ID:{}", orderCreateDTO.getUserId());
List<OrderItem> orderItemList = orderCreateDTO.getOrderItemDTOList().stream()
.map(item -> OrderItem.create(item.getSkuId(), item.getSkuName(), item.getQuantity(),
new Money(item.getUnitPrice(), "CNY")))
.toList();
List<Long> skuIdList = orderItemList.stream().map(OrderItem::getSkuId).toList();
Result<Map<Long, Integer>> stockResult = stockFeignClient.batchQueryStock(skuIdList);
if (!stockResult.isSuccess()) {
throw new RuntimeException("查询库存失败:" + stockResult.getMessage());
}
orderDomainService.checkStock(orderItemList, stockResult.getData());
Address address = new Address(
orderCreateDTO.getProvince(),
orderCreateDTO.getCity(),
orderCreateDTO.getDistrict(),
orderCreateDTO.getDetailAddress(),
orderCreateDTO.getPhone(),
orderCreateDTO.getReceiver()
);
Order order = Order.create(orderCreateDTO.getUserId(), address, orderItemList);
Order savedOrder = transactionTemplate.execute(status -> {
try {
return orderRepository.save(order);
} catch (Exception e) {
status.setRollbackOnly();
log.error("保存订单失败", e);
throw new RuntimeException("保存订单失败", e);
}
});
if (savedOrder == null) {
throw new RuntimeException("订单保存失败");
}
eventPublisher.publishEvent(new OrderCreatedEvent(this, savedOrder.getOrderId(),
savedOrder.getUserId(), savedOrder.getOrderItemList()));
log.info("订单创建完成,订单ID:{}", savedOrder.getOrderId());
return savedOrder.getOrderId();
}
public void paySuccess(Long orderId, BigDecimal payAmount) {
log.info("开始处理订单支付成功,订单ID:{}, 支付金额:{}", orderId, payAmount);
Order order = orderRepository.findById(orderId);
if (order == null) {
throw new IllegalArgumentException("订单不存在");
}
order.paySuccess(new Money(payAmount, "CNY"));
transactionTemplate.execute(status -> {
try {
return orderRepository.save(order);
} catch (Exception e) {
status.setRollbackOnly();
log.error("更新订单状态失败", e);
throw new RuntimeException("更新订单状态失败", e);
}
});
log.info("订单支付成功处理完成,订单ID:{}", orderId);
}
public void deliverOrder(Long orderId) {
log.info("开始处理订单发货,订单ID:{}", orderId);
Order order = orderRepository.findById(orderId);
if (order == null) {
throw new IllegalArgumentException("订单不存在");
}
order.deliver();
transactionTemplate.execute(status -> {
try {
return orderRepository.save(order);
} catch (Exception e) {
status.setRollbackOnly();
log.error("更新订单发货状态失败", e);
throw new RuntimeException("更新订单发货状态失败", e);
}
});
log.info("订单发货处理完成,订单ID:{}", orderId);
}
public void cancelOrder(Long orderId) {
log.info("开始处理订单取消,订单ID:{}", orderId);
Order order = orderRepository.findById(orderId);
if (order == null) {
throw new IllegalArgumentException("订单不存在");
}
order.cancel();
transactionTemplate.execute(status -> {
try {
return orderRepository.save(order);
} catch (Exception e) {
status.setRollbackOnly();
log.error("取消订单失败", e);
throw new RuntimeException("取消订单失败", e);
}
});
log.info("订单取消处理完成,订单ID:{}", orderId);
}
}
6.6 用户接口层核心代码
package com.jam.demo.interfaces.controller;
import com.jam.demo.application.dto.OrderCreateDTO;
import com.jam.demo.application.service.OrderApplicationService;
import com.jam.demo.interfaces.common.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
/**
* 订单接口
* @author ken
* @since 2026-03-05
*/
@RestController
@RequestMapping("/api/v1/order")
@Tag(name = "订单管理接口")
@Slf4j
public class OrderController {
private final OrderApplicationService orderApplicationService;
public OrderController(OrderApplicationService orderApplicationService) {
this.orderApplicationService = orderApplicationService;
}
@PostMapping("/create")
@Operation(summary = "创建订单")
public Result<Long> createOrder(@RequestBody @Validated OrderCreateDTO orderCreateDTO) {
try {
Long orderId = orderApplicationService.createOrder(orderCreateDTO);
return Result.success("订单创建成功", orderId);
} catch (Exception e) {
log.error("创建订单失败", e);
return Result.fail(e.getMessage());
}
}
@PostMapping("/pay/success")
@Operation(summary = "订单支付成功回调")
public Result<Void> paySuccess(@RequestParam Long orderId, @RequestParam BigDecimal payAmount) {
try {
orderApplicationService.paySuccess(orderId, payAmount);
return Result.success("支付成功处理完成", null);
} catch (Exception e) {
log.error("支付成功处理失败", e);
return Result.fail(e.getMessage());
}
}
@PostMapping("/deliver")
@Operation(summary = "订单发货")
public Result<Void> deliverOrder(@RequestParam Long orderId) {
try {
orderApplicationService.deliverOrder(orderId);
return Result.success("订单发货处理完成", null);
} catch (Exception e) {
log.error("订单发货处理失败", e);
return Result.fail(e.getMessage());
}
}
@PostMapping("/cancel")
@Operation(summary = "取消订单")
public Result<Void> cancelOrder(@RequestParam Long orderId) {
try {
orderApplicationService.cancelOrder(orderId);
return Result.success("订单取消处理完成", null);
} catch (Exception e) {
log.error("订单取消处理失败", e);
return Result.fail(e.getMessage());
}
}
}
6.7 基础设施层核心代码
package com.jam.demo.infrastructure.repository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.jam.demo.domain.aggregate.Order;
import com.jam.demo.domain.repository.OrderRepository;
import com.jam.demo.infrastructure.converter.OrderConverter;
import com.jam.demo.infrastructure.mapper.OrderItemMapper;
import com.jam.demo.infrastructure.mapper.OrderMapper;
import com.jam.demo.infrastructure.po.OrderItemPO;
import com.jam.demo.infrastructure.po.OrderPO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.util.List;
/**
* 订单仓储实现类
* @author ken
* @since 2026-03-05
*/
@Repository
@Slf4j
public class OrderRepositoryImpl implements OrderRepository {
private final OrderMapper orderMapper;
private final OrderItemMapper orderItemMapper;
private final OrderConverter orderConverter;
public OrderRepositoryImpl(OrderMapper orderMapper, OrderItemMapper orderItemMapper, OrderConverter orderConverter) {
this.orderMapper = orderMapper;
this.orderItemMapper = orderItemMapper;
this.orderConverter = orderConverter;
}
@Override
public Order save(Order order) {
OrderPO orderPO = orderConverter.toPO(order);
if (ObjectUtils.isEmpty(order.getOrderId())) {
orderMapper.insert(orderPO);
order.setOrderId(orderPO.getOrderId());
} else {
orderMapper.updateById(orderPO);
}
Long orderId = order.getOrderId();
if (!ObjectUtils.isEmpty(orderId)) {
LambdaQueryWrapper<OrderItemPO> wrapper = Wrappers.lambdaQuery();
wrapper.eq(OrderItemPO::getOrderId, orderId);
orderItemMapper.delete(wrapper);
}
if (!CollectionUtils.isEmpty(order.getOrderItemList())) {
List<OrderItemPO> orderItemPOList = order.getOrderItemList().stream()
.peek(item -> item.setOrderId(orderId))
.map(orderConverter::toPO)
.toList();
orderItemMapper.batchInsert(orderItemPOList);
}
log.info("订单聚合保存成功,订单ID:{}", orderId);
return findById(orderId);
}
@Override
public Order findById(Long orderId) {
if (ObjectUtils.isEmpty(orderId)) {
throw new IllegalArgumentException("订单ID不能为空");
}
OrderPO orderPO = orderMapper.selectById(orderId);
if (ObjectUtils.isEmpty(orderPO)) {
return null;
}
LambdaQueryWrapper<OrderItemPO> wrapper = Wrappers.lambdaQuery();
wrapper.eq(OrderItemPO::getOrderId, orderId);
List<OrderItemPO> orderItemPOList = orderItemMapper.selectList(wrapper);
Order order = orderConverter.toDomain(orderPO);
order.setOrderItemList(orderItemPOList.stream().map(orderConverter::toDomain).toList());
return order;
}
@Override
public void deleteById(Long orderId) {
if (ObjectUtils.isEmpty(orderId)) {
throw new IllegalArgumentException("订单ID不能为空");
}
orderMapper.deleteById(orderId);
LambdaQueryWrapper<OrderItemPO> wrapper = Wrappers.lambdaQuery();
wrapper.eq(OrderItemPO::getOrderId, orderId);
orderItemMapper.delete(wrapper);
log.info("订单聚合删除成功,订单ID:{}", orderId);
}
}
七、易混淆核心概念明确区分
| 概念 | 核心职责 | 核心区别 | 错误用法 |
| 应用服务 | 业务流程编排,用例入口 | 只关心“做什么”,不包含业务规则,无状态 | 把业务规则写在应用服务里,Service层膨胀 |
| 领域服务 | 处理跨实体的业务规则 | 只关心“怎么做”,只包含业务规则,无状态 | 把单实体的业务规则放在领域服务里,聚合根贫血 |
| 仓储 | 面向聚合的生命周期管理 | 操作完整聚合,接口定义在领域层,实现和技术解耦 | 把仓储当DAO用,操作单表,接口定义在基础设施层 |
| DAO | 面向数据库的单表操作 | 操作数据,和数据库表一一对应,和技术强绑定 | 用DAO直接操作聚合内的实体,破坏聚合边界 |
| 实体 | 有唯一标识的可变业务对象 | 相等性靠唯一ID,有生命周期,包含业务规则 | 实体只有getter/setter,无业务规则,沦为数据载体 |
| 值对象 | 无唯一标识的不可变业务对象 | 相等性靠所有属性,不可变,描述实体特征 | 给值对象加唯一ID,设计成可变的,破坏不可变性 |
| 限界上下文 | 业务的边界 | 业务自治单元,基于通用语言划分 | 和微服务划等号,一个微服务放多个上下文,边界混乱 |
| 聚合根 | 聚合的唯一入口,事务边界 | 管理聚合内所有对象的生命周期,一个事务只修改一个聚合根 | 聚合根划分过大,事务过大,外部直接访问聚合内的实体 |
八、DDD落地的8大常见坑与避坑指南
- 过度设计,简单业务硬套DDD
- 坑:简单的CRUD业务,强行拆分限界上下文、聚合,导致代码复杂度飙升,开发效率下降。
- 避坑:DDD只适合复杂业务场景,简单CRUD场景用三层架构更高效。判断标准:业务规则多、需求迭代频繁、团队协作成本高,就用DDD;否则不要硬套。
- 只做战术设计,忽略战略设计
- 坑:上来就写代码,不做通用语言梳理和限界上下文划分,导致边界混乱,最终还是回到三层架构的老路上。
- 避坑:DDD的核心是战略设计,必须先和团队一起梳理通用语言、划分限界上下文,再开始写代码。战略设计占70%的工作量,战术设计只占30%。
- 贫血模型,聚合根沦为数据载体
- 坑:聚合根只有getter/setter,所有业务规则都写在应用服务里,聚合根没有任何业务逻辑,沦为纯粹的数据载体。
- 避坑:遵循“充血模型”原则,所有和聚合相关的业务规则,都必须放在聚合根里,应用服务只做编排,不处理任何业务规则。
- 聚合根划分错误,事务边界混乱
- 坑:把聚合根划得太大,一个聚合包含十几个实体,导致事务过大,性能极差;或者划得太小,把强关联的实体拆成多个聚合,导致强一致性无法保证。
- 避坑:聚合的划分标准是“事务边界”,需要强一致性的业务对象放在一个聚合里,最终一致性的拆成多个聚合。一个聚合的实体数量尽量控制在3个以内,最多不超过5个。
- 业务规则泄露,散落在各个层
- 坑:业务规则散落在Controller、应用服务、DAO里,甚至在SQL里,修改一个业务规则需要改多处代码。
- 避坑:严格遵循分层架构的职责,所有业务规则必须放在领域层,其他层绝对不能包含任何业务规则。
- 跨聚合直接调用,破坏边界
- 坑:在一个聚合里直接注入另一个聚合的仓储,直接修改另一个聚合的对象,破坏了聚合的边界,导致耦合严重。
- 避坑:聚合之间绝对不能直接调用,跨聚合的交互有两种方式:应用层编排多个聚合的操作,或者用领域事件实现最终一致性。
- 忽略防腐层,系统强耦合
- 坑:直接调用其他系统的接口,把其他系统的模型直接用在自己的领域里,导致其他系统的变更直接影响自己的系统。
- 避坑:所有跨上下文、跨系统的调用,必须加防腐层,把外部的模型转换为自己的领域模型,彻底隔离外部变化。
- 用数据库表设计驱动领域设计
- 坑:先设计数据库表,再根据表结构设计聚合和实体,完全颠倒了DDD的设计逻辑,最终还是数据驱动。
- 避坑:先基于业务设计领域模型,再根据领域模型设计数据库表,数据库表只是领域模型的持久化载体,不能反过来驱动领域设计。
九、总结:DDD的正确打开方式
DDD不是一套银弹,也不是一套固定的代码模板,它的核心是“领域优先,边界隔离”的设计思想。它的本质是让我们先搞清楚业务,再用代码去实现业务,而不是反过来被技术和数据库表牵着鼻子走。
落地DDD的正确步骤永远是:
- 和团队一起梳理业务,统一通用语言;
- 基于通用语言划分限界上下文,明确业务边界;
- 在上下文内划分聚合,明确事务边界;
- 设计聚合根、实体、值对象,把业务规则内聚到领域对象里;
- 基于分层架构,实现代码落地。
对于Java开发者来说,DDD最大的价值,是让我们从“CRUD工程师”升级为“业务架构师”,从只会写代码实现需求,变成能主导业务设计、从根源解决系统复杂度的核心开发者。