DDD 领域驱动设计深入原理
DDD(Domain-Driven Design)是一种以业务领域为核心的软件设计方法。核心思想:代码结构应该反映业务结构,不是按技术分层,而是按业务领域划分。
一、传统分层架构有什么问题
1.1 典型的"大泥球"
// 传统 MVC 分层:Controller → Service → DAO
UserController // 处理 HTTP 请求
↓
UserService // 所有业务逻辑都往里塞
↓ // 验证、调用外部接口、发通知、写缓存... 全在这
UserDao // 数据库操作
问题:当业务变复杂后,UserService 会变成几千行的"上帝类",什么都往里塞,最后改不动也看不懂。
1.2 传统分层的核心矛盾
技术视角(传统分层):
表现层 → 业务层 → 持久层 → 数据库
问题:业务逻辑散落在各层,一个需求修改要跨 4 层找代码
业务视角(DDD):
订单域 → 商品域 → 用户域 → 支付域
优势:一个需求通常只改一个域,业务逻辑集中
1.3 什么时候需要 DDD
| 场景 | 用 DDD | 用简单分层 |
|---|---|---|
| 业务逻辑复杂(多状态、多规则) | ✅ | |
| 团队 > 5 人,多人改同一模块 | ✅ | |
| CRUD 型系统(增删改查为主) | ✅ | |
| 业务规则少、只是表单提交 | ✅ | |
| 希望代码反映业务、便于沟通 | ✅ |
一句话:CRUD 项目不需要 DDD,复杂业务系统才需要。别为了 DDD 而 DDD。
二、核心概念
2.1 Entity(实体) vs Value Object(值对象)
Entity:有唯一标识,改了属性还是同一个东西
Value Object:没有唯一标识,属性相同就是同一个东西
// Entity:用户
// 即使改了名字,还是同一个用户(ID 没变)
public class User {
private Long id; // 唯一标识 — Entity 的标志
private String name; // 可以修改
private Address address; // 可以修改
// 两个 Entity 是否相同?看 ID
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
return Objects.equals(id, ((User) o).id);
}
}
// Value Object:地址
// 如果省市区都相同,就是同一个地址(不需要 ID)
public class Address {
private String province;
private String city;
private String district;
// 两个 Value Object 是否相同?看所有属性
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Address)) return false;
Address a = (Address) o;
return Objects.equals(province, a.province)
&& Objects.equals(city, a.city)
&& Objects.equals(district, a.district);
}
}
// 使用
User user = new User(1001L, "张三", new Address("浙江", "杭州", "西湖"));
user.setName("李四"); // 改了名字,还是 User 1001
2.2 Aggregate(聚合) 与 Aggregate Root(聚合根)
聚合:一组强相关的 Entity + Value Object 的集合
聚合根:聚合的"入口",外部只能通过聚合根访问聚合内部的对象
举例:订单聚合
// 聚合根:Order — 外部只能通过它操作聚合内的对象
public class Order { // ← 聚合根(Aggregate Root)
private Long id; // 订单ID
private Long userId; // 用户ID
private OrderStatus status; // 订单状态
private BigDecimal totalPrice; // 总价
private List<OrderItem> items; // ← 订单明细(聚合内部对象)
// 业务方法:不是简单的 setter,而是有业务含义的方法
public void addItem(Long productId, BigDecimal price, int quantity) {
// 业务规则验证
if (status != OrderStatus.DRAFT) {
throw new BizException("只有草稿状态才能添加商品");
}
OrderItem item = new OrderItem(productId, price, quantity);
items.add(item);
// 重新计算总价
recalculateTotalPrice();
}
public void submit() {
if (items.isEmpty()) {
throw new BizException("订单不能为空");
}
this.status = OrderStatus.SUBMITTED;
}
public void pay() {
if (status != OrderStatus.SUBMITTED) {
throw new BizException("只有已提交的订单才能支付");
}
this.status = OrderStatus.PAID;
}
private void recalculateTotalPrice() {
this.totalPrice = items.stream()
.map(OrderItem::getSubTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
// OrderItem 是聚合内部对象,不能独立于 Order 存在
// 没有 ID 作为独立 Entity,它的生命周期由 Order 控制
public class OrderItem { // ← 聚合内部对象(不是聚合根)
private Long productId;
private BigDecimal price;
private int quantity;
public BigDecimal getSubTotal() {
return price.multiply(BigDecimal.valueOf(quantity));
}
}
聚合根规则:
1. 外部只能通过聚合根访问聚合内部的实体
✅ orderRepository.findById(id) // 通过聚合根
❌ orderItemRepository.findById() // 不存在这个 Repository!
2. 聚合根内部保证业务规则的一致性
在 addItem() 里检查状态,在 pay() 里检查前置条件
3. 一次操作只修改一个聚合
不要在一个用例里同时修改 Order 和 User 两个聚合
2.3 Repository(仓储)— 聚合的"数据接口"
// Repository 只为聚合根定义,不是每个表一个
// 它把数据库操作封装起来,让业务代码不感知 MySQL/Redis
public interface OrderRepository {
// 根据 ID 查完整的订单聚合
Optional<Order> findById(Long orderId);
// 保存整个聚合(订单 + 明细一起持久化)
void save(Order order);
// 按条件查询
List<Order> findByUserIdAndStatus(Long userId, OrderStatus status);
// 注意:不存在 OrderItemRepository!
// 因为 OrderItem 不是聚合根,只能通过 Order 操作
}
// 实现层(Infrastructure)
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderItemMapper orderItemMapper;
@Override
public Optional<Order> findById(Long orderId) {
OrderDO orderDO = orderMapper.selectById(orderId);
if (orderDO == null) return Optional.empty();
List<OrderItemDO> itemDOs = orderItemMapper.selectByOrderId(orderId);
// DO → Domain 对象转换
return Optional.of(OrderConverter.toDomain(orderDO, itemDOs));
}
@Override
public void save(Order order) {
// 保存订单 + 明细(全量替换或增量更新)
OrderDO orderDO = OrderConverter.toDO(order);
orderMapper.insertOrUpdate(orderDO);
// 先删旧明细,再插新明细
orderItemMapper.deleteByOrderId(order.getId());
List<OrderItemDO> itemDOs = OrderItemConverter.toDOList(order.getItems());
orderItemMapper.batchInsert(itemDOs);
}
}
2.4 Domain Service(领域服务)
当业务逻辑不属于任何一个 Entity 时,放在 Domain Service:
// 场景:转账 — 涉及两个账户,不属于单个 Entity 的行为
// 这个逻辑放在 Account 聚合里不合适,放在 Domain Service 里
@Service
public class TransferService { // 领域服务(无状态)
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) {
// 1. 加载两个 Account 聚合
Account from = accountRepository.findById(fromAccountId)
.orElseThrow(() -> new BizException("转出账户不存在"));
Account to = accountRepository.findById(toAccountId)
.orElseThrow(() -> new BizException("转入账户不存在"));
// 2. 执行领域逻辑(在聚合上操作)
from.debit(amount); // 扣钱
to.credit(amount); // 加钱
// 3. 持久化
accountRepository.save(from);
accountRepository.save(to);
}
}
2.5 Domain Event(领域事件)
聚合内部发生了重要事情,通知其他模块:
// 1. 定义领域事件
public class OrderPaidEvent {
private final Long orderId;
private final Long userId;
private final BigDecimal amount;
private final LocalDateTime paidAt;
public OrderPaidEvent(Long orderId, Long userId, BigDecimal amount) {
this.orderId = orderId;
this.userId = userId;
this.amount = amount;
this.paidAt = LocalDateTime.now();
}
// getters...
}
// 2. 在聚合内注册事件
public class Order {
private List<Object> domainEvents = new ArrayList<>();
public void pay() {
if (status != OrderStatus.SUBMITTED) {
throw new BizException("只有已提交的订单才能支付");
}
this.status = OrderStatus.PAID;
// 注册领域事件(不直接发 MQ,而是先记录)
domainEvents.add(new OrderPaidEvent(id, userId, totalPrice));
}
// 调用方拿到 events 后发布
public List<Object> getDomainEvents() {
return domainEvents;
}
}
// 3. Service 层发布事件
@Service
public class OrderApplicationService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ApplicationEventPublisher eventPublisher;
@Transactional
public void pay(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new BizException("订单不存在"));
order.pay();
orderRepository.save(order);
// 发布领域事件 → 通知积分服务、短信服务等
for (Object event : order.getDomainEvents()) {
eventPublisher.publishEvent(event);
}
}
}
// 4. 监听事件(异步处理,不阻塞主流程)
@Component
public class OrderPaidListener {
@EventListener
@Async // 异步执行
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) // 事务提交后
public void onOrderPaid(OrderPaidEvent event) {
// 通知积分服务加积分
// 通知短信服务发短信
pointsService.addPoints(event.getUserId(), event.getAmount());
}
}
三、战略设计
3.1 Bounded Context(限界上下文)
一个问题:同一个词在不同部门有不同含义。
销售部门说"订单" → 包含客户信息、折扣、总额
仓库部门说"订单" → 包含物流信息、库存流转
财务部门说"订单" → 包含收款状态、发票信息
这三个"订单"是三个不同的上下文
限界上下文 = 一个业务能力的边界。每个上下文有自己的模型、自己的语言。
电商系统限界上下文划分:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 商品上下文 │ │ 订单上下文 │ │ 支付上下文 │
│ │ │ │ │ │
│ Product │ │ Order │ │ Payment │
│ Category │ │ OrderItem │ │ Transaction │
│ Inventory │ │ OrderStatus │ │ PaymentMethod│
└──────────────┘ └──────────────┘ └──────────────┘
┌──────────────┐ ┌──────────────┐
│ 用户上下文 │ │ 营销上下文 │
│ │ │ │
│ User │ │ Coupon │
│ Address │ │ Promotion │
│ MemberLevel │ │ Campaign │
└──────────────┘ └──────────────┘
3.2 Context Map(上下文映射)
不同限界上下文如何协作:
订单上下文 ←→ 商品上下文
│ │
│ 共享内核 │
│ (Shared Kernel)
▼ ▼
订单需要查商品名称 → 通过 API 调用(Customer-Supplier)
订单创建后 → 通过领域事件通知支付上下文(Event-Driven)
| 映射模式 | 说明 | 示例 |
|---|---|---|
| Customer-Supplier | 下游调用上游 API | 订单服务调商品服务查库存 |
| Shared Kernel | 共享一个核心模型 | 两个上下文用同一个 User 模型 |
| Conformist | 下游完全接受上游模型 | 使用第三方支付 SDK 的模型 |
| Anti-Corruption Layer | 隔离层,防止外部模型污染 | 对接老旧系统时,加一层转换 |
| Open Host Service | 对外提供统一的 REST API | 多个系统通过同一接口访问订单 |
| Published Language | 使用统一的文档格式 | JSON Schema、Protocol Buffers |
3.3 Anti-Corruption Layer(防腐层)
// 场景:对接一个老旧系统的用户服务
// 老旧系统返回的用户模型非常丑:
// {"user_id": "1001", "user_nm": "zhangsan", "phn_num": "138..."}
// 1. 老系统的模型(不要直接引用到你的业务里)
public class LegacyUserDTO {
private String user_id;
private String user_nm;
private String phn_num;
}
// 2. 防腐层:把旧模型转成你领域内的模型
@Component
public class UserAntiCorruptionLayer {
public User convert(LegacyUserDTO dto) {
return new User(
Long.parseLong(dto.getUser_id()),
dto.getUser_nm(),
new PhoneNumber(dto.getPhn_num())
);
}
}
// 3. 业务代码只用领域模型 User,完全不感知 LegacyUserDTO
// 如果哪天旧系统换了,只需要改防腐层
四、DDD 项目目录结构
com.example.order/
│
├── interfaces/ # 接口层(Controller)
│ └── OrderController.java # REST API,只负责接收请求和返回结果
│
├── application/ # 应用服务层(编排)
│ ├── OrderApplicationService.java # 编排领域逻辑,处理事务
│ └── dto/
│ ├── CreateOrderCommand.java # 入参
│ └── OrderResult.java # 出参
│
├── domain/ # 领域层(核心,不依赖外部框架)
│ ├── model/ # 聚合、实体、值对象
│ │ ├── Order.java # 聚合根
│ │ ├── OrderItem.java # 实体
│ │ ├── OrderStatus.java # 枚举
│ │ └── Address.java # 值对象
│ ├── service/ # 领域服务
│ │ └── OrderDomainService.java
│ ├── repository/ # 仓储接口(只定义接口)
│ │ └── OrderRepository.java
│ └── event/ # 领域事件
│ └── OrderPaidEvent.java
│
└── infrastructure/ # 基础设施层(实现仓储、调用外部)
├── repository/
│ └── OrderRepositoryImpl.java # 仓储接口的实现
├── converter/
│ └── OrderConverter.java # Domain ↔ DO 转换
├── external/
│ └── UserFeignClient.java # 外部服务调用
└── mq/
└── OrderEventPublisher.java # 消息发布
依赖方向:
interfaces → application → domain ← infrastructure
↑
基础设施实现领域定义的接口
domain 层不依赖任何外部框架(Spring、MyBatis、Redis)
只有纯 Java 代码
为什么? → 领域逻辑纯度高,可以独立测试,技术栈换了不影响业务
五、完整实战示例
5.1 应用服务层(编排)
// Application Service:编排领域对象,但自己不写业务逻辑
// 业务规则在 domain 层的 Order 里
// Application Service 只负责:加载聚合 → 调用方法 → 保存
@Service
public class OrderApplicationService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ProductFeignClient productFeignClient;
@Autowired
private ApplicationEventPublisher eventPublisher;
/**
* 创建订单
*/
@Transactional
public Long createOrder(CreateOrderCommand command) {
// 1. 校验(用 Feign 同步查商品信息)
ProductVO product = productFeignClient.getProduct(command.getProductId());
if (product.getStock() < command.getQuantity()) {
throw new BizException("库存不足");
}
// 2. 创建订单聚合(业务规则在 Order 内部)
Order order = Order.create(
command.getUserId(),
command.getProductId(),
product.getPrice(),
command.getQuantity()
);
// 3. 持久化
orderRepository.save(order);
// 4. 发布领域事件(事务提交后)
// 不在这里直接发 MQ!用 @TransactionalEventListener 异步发
for (Object event : order.getDomainEvents()) {
eventPublisher.publishEvent(event);
}
return order.getId();
}
/**
* 支付订单
*/
@Transactional
public void pay(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new BizException("订单不存在"));
order.pay(); // 业务规则在 pay() 里
orderRepository.save(order);
}
}
5.2 领域层(业务规则)
// Domain Object:包含业务规则,不是贫血模型
public class Order {
private Long id;
private Long userId;
private Long productId;
private BigDecimal unitPrice;
private int quantity;
private BigDecimal totalPrice;
private OrderStatus status;
private LocalDateTime createdAt;
private List<Object> domainEvents = new ArrayList<>();
// 私有不含参构造 — 通过静态工厂方法创建
private Order() {}
/**
* 静态工厂方法 — 创建订单
* 业务规则:
* 1. 数量必须 > 0
* 2. 单价必须 > 0
* 3. 自动计算总价
* 4. 初始状态为 DRAFT
*/
public static Order create(Long userId, Long productId,
BigDecimal price, int quantity) {
if (quantity <= 0) {
throw new BizException("数量必须大于 0");
}
if (price.compareTo(BigDecimal.ZERO) <= 0) {
throw new BizException("单价必须大于 0");
}
Order order = new Order();
order.userId = userId;
order.productId = productId;
order.unitPrice = price;
order.quantity = quantity;
order.totalPrice = price.multiply(BigDecimal.valueOf(quantity));
order.status = OrderStatus.DRAFT;
order.createdAt = LocalDateTime.now();
// 注册领域事件
order.domainEvents.add(new OrderCreatedEvent(order));
return order;
}
/**
* 支付订单
* 业务规则:只有已提交的订单才能支付
*/
public void pay() {
if (this.status != OrderStatus.SUBMITTED) {
throw new BizException("只有已提交的订单才能支付");
}
this.status = OrderStatus.PAID;
this.domainEvents.add(new OrderPaidEvent(this.id, this.userId, this.totalPrice));
}
/**
* 取消订单
* 业务规则:
* 1. 已支付超过 1 小时的订单不能取消
* 2. 已完成/已取消的订单不能取消
*/
public void cancel() {
if (this.status == OrderStatus.COMPLETED || this.status == OrderStatus.CANCELLED) {
throw new BizException("订单已完成或已取消,不能取消");
}
if (this.status == OrderStatus.PAID
&& Duration.between(this.createdAt, LocalDateTime.now()).toHours() > 1) {
throw new BizException("已支付超过 1 小时,不能取消");
}
this.status = OrderStatus.CANCELLED;
}
// getters...
public Long getId() { return id; }
public List<Object> getDomainEvents() { return domainEvents; }
}
六、常见误区
6.1 贫血模型 ≠ DDD
// ❌ 贫血模型:只有 getter/setter,业务逻辑全在 Service 里
// 这是传统的分层架构,不是 DDD
public class Order {
private Long id;
private String status;
// 只有 getter/setter,没有任何业务方法
public void setStatus(String status) { this.status = status; }
}
// ✅ 充血模型:业务规则和数据在一起
public class Order {
private Long id;
private OrderStatus status;
public void pay() {
if (status != SUBMITTED) throw new IllegalStateException();
this.status = PAID;
}
// 不让外部直接 setStatus,只能通过业务方法修改状态
}
6.2 每个表都要建一个聚合根?
误区:我有个 order_item 表,是不是要建一个 OrderItem 聚合根?
正确做法:
- OrderItem 的生命周期由 Order 控制
- 不存在独立的 OrderItemRepository
- OrderItem 通过 Order 聚合根访问
一个聚合根 = 一个事务边界 = 一个业务一致性单元
6.3 聚合根上不要做跨聚合的事务
// ❌ 在一个方法里修改两个聚合
@Transactional
public void transfer() {
orderRepository.save(order); // 聚合1:订单
accountRepository.save(account); // 聚合2:账户
}
// 两个聚合在同一个事务里 = 耦合
// ✅ 用领域事件 + 最终一致性
@Transactional
public void pay() {
order.pay();
orderRepository.save(order); // 只修改订单聚合
// 发领域事件,异步处理账户操作
eventPublisher.publishEvent(new OrderPaidEvent(order));
}
6.4 Repository 不要返回 DTO
// ❌ 错误:Repository 返回了展示层的东西
public interface OrderRepository {
List<OrderDTO> findByUserId(Long userId); // DTO 不是领域概念
}
// ✅ 正确:Repository 返回领域对象
public interface OrderRepository {
List<Order> findByUserId(Long userId); // 返回聚合根
}
七、DDD 的代价与取舍
7.1 DDD 的成本
| 成本 | 说明 |
|---|---|
| 学习曲线 | 概念多(聚合、限界上下文、领域事件...),团队需要培训 |
| 前期投入大 | 分析业务、划分上下文,比直接写 CRUD 慢 |
| 过度设计风险 | 简单的增删改查也用 DDD = 杀鸡用牛刀 |
| 性能开销 | 聚合整体加载、领域事件异步,可能引入额外复杂度 |
7.2 什么时候不该用 DDD
1. 纯 CRUD 系统(管理后台、表单提交)
2. 业务规则极少(只做转发、透传)
3. 团队 < 3 人,沟通成本低
4. 遗留系统改造(成本太高,不如局部优化)
5. 赶进度的小项目(以后可以重构)
7.3 务实建议
不要求完美的 DDD:
1. 先让你的代码按功能模块分目录
(比按技术分层好)
2. 把核心业务放到 domain 包里
(让业务逻辑和数据在一起)
3. 使用充血模型,而不是贫血模型
(把业务方法放到对象里,而不是 Service 里)
4. 业务不复杂就保持简单
(用 @Transactional + MQ 事务消息保证一致性就够了)
八、总结
| 概念 | 一句话 | 类比 |
|---|---|---|
| Entity | 有 ID,改了属性还是同一个东西 | 你改了名字还是你 |
| Value Object | 没有 ID,属性相同就是同一个 | 坐标 (x,y) 完全相同时就是同一个位置 |
| Aggregate | 一组强关联对象的集合 | 订单 + 订单明细是一个整体 |
| Aggregate Root | 聚合的"门",外部只能走这个门 | 订单是门,不能绕过订单直接改订单明细 |
| Repository | 聚合的"数据仓库" | 仓库管理员,存取聚合 |
| Domain Service | 不属于某个 Entity 的业务逻辑 | 转账(涉及两个账户) |
| Domain Event | 聚合内发生了重要事情 | 订单支付成功 → 通知积分服务 |
| Bounded Context | 一个业务能力的边界 | 订单上下文 vs 支付上下文 |
| Anti-Corruption Layer | 隔离外部系统的旧模型 | 翻译官,把旧系统的话翻译成你能懂的 |