写在前面
说实话,我刚接触DDD的时候,整个人是懵的。什么聚合根、限界上下文、领域事件,听起来像玄学。直到我接手了一个维护了三年的电商项目,Service层代码超过5000行,一个需求改下来要动十几个文件,我才真正理解DDD的价值。这篇文章把我踩过的坑和学到的经验分享给你,希望能帮你少走点弯路。

文章目录
-
- 一、为什么需要DDD?
-
- [1.1 传统三层架构的困境](#1.1 传统三层架构的困境)
- [1.2 生活类比](#1.2 生活类比)
- [1.3 DDD的核心价值](#1.3 DDD的核心价值)
- 二、DDD核心概念
-
- [2.1 领域与子域](#2.1 领域与子域)
- [2.2 限界上下文(Bounded Context)](#2.2 限界上下文(Bounded Context))
- [2.3 实体与值对象](#2.3 实体与值对象)
- [2.4 聚合与聚合根](#2.4 聚合与聚合根)
- [2.5 领域事件](#2.5 领域事件)
- [2.6 仓储与领域服务](#2.6 仓储与领域服务)
- 三、DDD四层架构详解
-
- [3.1 架构分层](#3.1 架构分层)
- [3.2 依赖关系](#3.2 依赖关系)
- [3.3 各层职责](#3.3 各层职责)
- [3.4 代码分层示例](#3.4 代码分层示例)
- [四、DDD vs 传统三层架构对比](#四、DDD vs 传统三层架构对比)
-
- [4.1 对比表格](#4.1 对比表格)
- [4.2 什么时候用DDD?](#4.2 什么时候用DDD?)
- [4.3 什么时候不用DDD?](#4.3 什么时候不用DDD?)
- 五、DDD实战:电商订单系统
-
- [5.1 领域建模](#5.1 领域建模)
- [5.2 领域事件与仓储](#5.2 领域事件与仓储)
- [5.3 应用层编排](#5.3 应用层编排)
- 六、踩坑指南
- 七、问题与解答
- 八、面试高频考点汇总
- 九、模拟面试官提问和参考答案
- 十、互动话题
- 参考资料
一、为什么需要DDD?
1.1 传统三层架构的困境
我见过太多项目,一开始用三层架构(Controller → Service → DAO)跑得飞快,半年后就成了"代码坟场"。
java
// 传统三层架构的典型代码
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private OrderItemDao orderItemDao;
@Autowired
private InventoryDao inventoryDao;
@Autowired
private PaymentDao paymentDao;
// ... 还有十几个Dao
public void createOrder(OrderDTO dto) {
// 1. 保存订单
// 2. 保存订单项
// 3. 扣减库存
// 4. 创建支付记录
// 5. 发送消息
// ... 几百行代码挤在一个方法里
}
}
业务逻辑散落在Service层,订单逻辑、库存逻辑、支付逻辑全搅在一起。一个需求改动,要改十几个文件,改完还不敢保证没bug。
1.2 生活类比
传统三层架构就像杂货铺,所有东西堆在一起,找个螺丝钉要翻半天。
DDD就像超市,按品类分区,生鲜区、日用品区、电器区,找东西清晰,补货也方便。
1.3 DDD的核心价值
| 价值点 | 说明 |
|---|---|
| 业务与技术解耦 | 业务逻辑集中在领域层,不依赖框架 |
| 代码可维护性 | 修改只影响一个聚合,不会牵一发而动全身 |
| 便于单元测试 | 领域对象不依赖Spring,纯Java代码就能测 |
| 团队沟通 | 用业务语言写代码,产品和开发能聊到一起 |
二、DDD核心概念
2.1 领域与子域
领域(Domain):你要解决的业务问题范围。比如电商系统,领域就是"电商"。
子域(Subdomain):把大领域拆成小块。
| 子域类型 | 说明 | 例子 |
|---|---|---|
| 核心域 | 公司的核心竞争力,投入最多资源 | 电商里的下单、支付 |
| 支撑域 | 支撑核心域运转,但非核心 | 物流跟踪、库存管理 |
| 通用域 | 通用能力,可以外包或买现成的 | 用户认证、消息通知 |
2.2 限界上下文(Bounded Context)
限界上下文就是业务的边界。同一个词在不同上下文里意思可能完全不同。
举个例子:"商品"在商品上下文 里指SKU信息(名称、价格、图片),在库存上下文 里指库存数量,在物流上下文里指包裹里的物品。三个"商品"不是同一个东西,不要硬塞到一个类里。
通常一个限界上下文对应一个微服务。
2.3 实体与值对象
实体(Entity):有唯一标识,即使属性全变,只要ID不变还是同一个对象。
java
// 订单是实体,orderId是唯一标识
public class Order {
private OrderId orderId; // 唯一标识
private List<OrderItem> items;
private Address address;
private OrderStatus status;
// 业务方法
public void pay() {
if (status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("订单状态异常");
}
this.status = OrderStatus.PAID;
}
public void cancel() {
if (status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("只能取消待支付订单");
}
this.status = OrderStatus.CANCELLED;
}
}
值对象(Value Object) :没有唯一标识,通过属性判断相等,不可变。
java
// 地址是值对象
public class Address {
private final String province;
private final String city;
private final String detail;
public Address(String province, String city, String detail) {
this.province = province;
this.city = city;
this.detail = detail;
}
// 值对象不可变,修改时返回新对象
public Address withCity(String newCity) {
return new Address(this.province, newCity, this.detail);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return Objects.equals(province, address.province) &&
Objects.equals(city, address.city) &&
Objects.equals(detail, address.detail);
}
}
2.4 聚合与聚合根
聚合(Aggregate):一组相关对象的集合,对外是一个整体。
聚合根(Aggregate Root):聚合的入口,外部只能通过聚合根访问内部对象。
java
// Order是聚合根,OrderItem在Order聚合内部
public class Order {
private OrderId orderId;
private List<OrderItem> items; // 只能通过Order操作OrderItem
private Address address;
// 外部不能直接new OrderItem,只能通过Order添加
public void addItem(Product product, int quantity, Money price) {
OrderItem item = new OrderItem(product.getId(), quantity, price);
this.items.add(item);
// 维护聚合内的一致性规则
if (getTotalAmount().compareTo(Money.ZERO) <= 0) {
throw new IllegalArgumentException("订单金额必须大于0");
}
}
public Money getTotalAmount() {
return items.stream()
.map(OrderItem::getSubTotal)
.reduce(Money.ZERO, Money::add);
}
}
2.5 领域事件
领域事件 = 领域内发生的有意义的事情。
java
// 订单创建事件
public class OrderCreatedEvent {
private final OrderId orderId;
private final List<OrderItem> items;
private final LocalDateTime occurredOn;
public OrderCreatedEvent(OrderId orderId, List<OrderItem> items) {
this.orderId = orderId;
this.items = items;
this.occurredOn = LocalDateTime.now();
}
// getters...
}
2.6 仓储与领域服务
仓储(Repository) :聚合的持久化接口,只操作聚合根。
java
public interface OrderRepository {
Order findById(OrderId id);
void save(Order order);
List<Order> findByUserId(UserId userId);
}
领域服务(Domain Service):跨实体的业务逻辑,不属于任何一个实体。
java
// 促销计算涉及订单、优惠券、会员多个实体,放在领域服务里
public class PromotionService {
public Money calculateDiscount(Order order, Coupon coupon, Member member) {
// 复杂的促销计算逻辑
Money discount = coupon.apply(order);
if (member.isVip()) {
discount = discount.add(order.getTotalAmount().multiply(0.05));
}
return discount;
}
}
三、DDD四层架构详解
3.1 架构分层
┌─────────────────────────────────────┐
│ 用户接口层 (User Interface) │ ← Controller、DTO转换、参数校验
│ Adapter / Web层 │
├─────────────────────────────────────┤
│ 应用层 (Application) │ ← 用例编排、事务控制、事件发布
├─────────────────────────────────────┤
│ 领域层 (Domain) │ ← 实体、值对象、领域服务、领域事件
│ │ 核心业务逻辑,不依赖任何框架
├─────────────────────────────────────┤
│ 基础设施层 (Infrastructure) │ ← 数据库访问、消息队列、外部API
└─────────────────────────────────────┘
3.2 依赖关系
核心原则:领域层不依赖任何其他层。
- 用户接口层 → 依赖应用层、领域层
- 应用层 → 依赖领域层
- 领域层 → 不依赖任何层
- 基础设施层 → 依赖领域层(实现领域层的接口)
这就是依赖倒置原则的体现。领域层定义接口(如Repository),基础设施层实现接口。
3.3 各层职责
| 层级 | 职责 | 包含内容 |
|---|---|---|
| 用户接口层 | 接收请求、返回响应、参数校验 | Controller、DTO、Validator |
| 应用层 | 编排用例、控制事务、转换DTO | ApplicationService、事务注解 |
| 领域层 | 核心业务逻辑 | Entity、ValueObject、DomainService、DomainEvent |
| 基础设施层 | 技术实现 | RepositoryImpl、MQSender、HttpClient |
3.4 代码分层示例
com.example.order
├── application // 应用层
│ ├── OrderApplicationService.java
│ └── dto
│ ├── OrderCreateRequest.java
│ └── OrderResponse.java
├── domain // 领域层(最核心,不依赖Spring)
│ ├── model
│ │ ├── Order.java // 聚合根
│ │ ├── OrderItem.java // 实体
│ │ ├── Address.java // 值对象
│ │ └── OrderStatus.java // 枚举
│ ├── service
│ │ └── OrderDomainService.java
│ ├── event
│ │ └── OrderCreatedEvent.java
│ └── repository
│ └── OrderRepository.java // 接口
├── infrastructure // 基础设施层
│ ├── repository
│ │ └── OrderRepositoryImpl.java // MyBatis/JPA实现
│ └── messaging
│ └── OrderEventPublisher.java
└── interfaces // 用户接口层
└── OrderController.java
四、DDD vs 传统三层架构对比
4.1 对比表格
| 对比维度 | 传统三层架构 | DDD分层架构 |
|---|---|---|
| 关注点 | 以技术为中心(数据表驱动) | 以业务为中心(领域模型驱动) |
| 代码组织 | 按技术分层(Service、Dao) | 按业务领域(Order、Payment) |
| 可维护性 | 业务复杂后难以维护 | 业务逻辑清晰,修改影响范围小 |
| 学习成本 | 低,大家熟悉 | 高,需要理解领域建模 |
| 团队沟通 | 开发说"表",产品说"业务" | 统一语言,代码即文档 |
| 适用场景 | 简单CRUD、快速原型 | 复杂业务、长期维护项目 |
| 单元测试 | 依赖数据库,难测 | 领域层纯Java,好测 |
4.2 什么时候用DDD?
- 业务逻辑复杂,规则经常变化
- 项目需要长期维护(一年以上)
- 团队规模较大,需要清晰的边界
- 微服务架构,需要明确服务边界
4.3 什么时候不用DDD?
这个坑我踩过。给一个内部管理系统硬上DDD,结果领域层就几个getter/setter,完全是过度设计。
- 简单CRUD,没什么业务逻辑
- 快速原型,验证想法
- 一次性脚本、数据迁移工具
五、DDD实战:电商订单系统
5.1 领域建模
java
// ==================== 值对象 ====================
public class Money {
private final BigDecimal amount;
private final String currency;
public static final Money ZERO = new Money(BigDecimal.ZERO, "CNY");
public Money(BigDecimal amount, String currency) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负数");
}
this.amount = amount;
this.currency = currency;
}
public Money add(Money other) {
checkCurrency(other);
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(double factor) {
return new Money(this.amount.multiply(BigDecimal.valueOf(factor)), this.currency);
}
private void checkCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("币种不一致");
}
}
// getters...
}
public class Address {
private final String province;
private final String city;
private final String district;
private final String detail;
public Address(String province, String city, String district, String detail) {
this.province = Objects.requireNonNull(province);
this.city = Objects.requireNonNull(city);
this.district = district;
this.detail = Objects.requireNonNull(detail);
}
// getters...
}
// ==================== 实体 ====================
public class OrderItem {
private ProductId productId;
private int quantity;
private Money unitPrice;
public OrderItem(ProductId productId, int quantity, Money unitPrice) {
if (quantity <= 0) {
throw new IllegalArgumentException("数量必须大于0");
}
this.productId = productId;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
public Money getSubTotal() {
return unitPrice.multiply(quantity);
}
// getters...
}
// ==================== 聚合根 ====================
public class Order {
private OrderId orderId;
private UserId userId;
private List<OrderItem> items = new ArrayList<>();
private Address address;
private OrderStatus status;
private Money totalAmount;
private LocalDateTime createTime;
// 私有构造器,只能通过工厂方法创建
private Order() {}
public static Order create(UserId userId, Address address) {
Order order = new Order();
order.orderId = OrderId.generate();
order.userId = userId;
order.address = address;
order.status = OrderStatus.PENDING_PAYMENT;
order.createTime = LocalDateTime.now();
return order;
}
// 业务方法:添加商品项
public void addItem(Product product, int quantity, Money price) {
ensureCanModify();
OrderItem item = new OrderItem(product.getId(), quantity, price);
items.add(item);
recalculateTotal();
}
// 业务方法:支付
public void pay(Money amount) {
if (status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("订单状态异常,当前状态:" + status);
}
if (!amount.equals(totalAmount)) {
throw new IllegalArgumentException("支付金额不匹配");
}
this.status = OrderStatus.PAID;
// 发布领域事件
registerEvent(new OrderPaidEvent(this.orderId, this.totalAmount));
}
// 业务方法:取消订单
public void cancel() {
if (status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("只能取消待支付订单");
}
this.status = OrderStatus.CANCELLED;
registerEvent(new OrderCancelledEvent(this.orderId, this.items));
}
// 业务方法:发货
public void ship(String trackingNumber) {
if (status != OrderStatus.PAID) {
throw new IllegalStateException("未支付订单不能发货");
}
this.status = OrderStatus.SHIPPED;
registerEvent(new OrderShippedEvent(this.orderId, trackingNumber));
}
private void ensureCanModify() {
if (status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("订单已不可修改");
}
}
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::getSubTotal)
.reduce(Money.ZERO, Money::add);
}
private void registerEvent(DomainEvent event) {
// 事件暂存,事务提交后统一发布
// 具体实现由基础设施层完成
}
// getters...
}
5.2 领域事件与仓储
java
// ==================== 领域事件 ====================
public interface DomainEvent {
LocalDateTime getOccurredOn();
}
public class OrderCreatedEvent implements DomainEvent {
private final OrderId orderId;
private final List<OrderItem> items;
private final LocalDateTime occurredOn;
public OrderCreatedEvent(OrderId orderId, List<OrderItem> items) {
this.orderId = orderId;
this.items = new ArrayList<>(items);
this.occurredOn = LocalDateTime.now();
}
@Override
public LocalDateTime getOccurredOn() {
return occurredOn;
}
// getters...
}
// ==================== 仓储接口(领域层定义) ====================
public interface OrderRepository {
Order findById(OrderId id);
void save(Order order);
List<Order> findByUserId(UserId userId);
}
// ==================== 仓储实现(基础设施层) ====================
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Autowired
private OrderMapper orderMapper; // MyBatis Mapper
@Autowired
private OrderItemMapper orderItemMapper;
@Override
public Order findById(OrderId id) {
// 从数据库加载并组装聚合
OrderPO orderPO = orderMapper.selectById(id.getValue());
List<OrderItemPO> itemPOs = orderItemMapper.selectByOrderId(id.getValue());
return convertToDomain(orderPO, itemPOs);
}
@Override
public void save(Order order) {
// 保存聚合根和内部实体
OrderPO orderPO = convertToPO(order);
orderMapper.insert(orderPO);
for (OrderItem item : order.getItems()) {
orderItemMapper.insert(convertToPO(item));
}
}
// 转换方法...
}
5.3 应用层编排
java
@Service
@Transactional
public class OrderApplicationService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private DomainEventPublisher eventPublisher;
public OrderId createOrder(CreateOrderCommand command) {
// 1. 创建聚合根
Order order = Order.create(
new UserId(command.getUserId()),
new Address(command.getProvince(), command.getCity(),
command.getDistrict(), command.getDetail())
);
// 2. 添加商品项
for (ItemCommand item : command.getItems()) {
order.addItem(
new Product(new ProductId(item.getProductId())),
item.getQuantity(),
new Money(item.getPrice(), "CNY")
);
}
// 3. 保存聚合
orderRepository.save(order);
// 4. 发布领域事件(事务提交后)
eventPublisher.publish(order.getDomainEvents());
return order.getOrderId();
}
public void payOrder(OrderId orderId, Money amount) {
Order order = orderRepository.findById(orderId);
if (order == null) {
throw new OrderNotFoundException(orderId);
}
order.pay(amount);
orderRepository.save(order);
eventPublisher.publish(order.getDomainEvents());
}
}
六、踩坑指南
贫血领域模型
我见过太多"伪DDD"项目,领域层里全是getter/setter,业务逻辑还在Service里。这叫贫血领域模型,是DDD的大忌。判断标准很简单:如果你的Entity里只有getter/setter,没有业务方法,那就是贫血模型。
聚合设计过大这个坑我踩过。一开始我把订单、订单项、物流、支付全塞到一个聚合里,结果加载一个订单要join七八张表。聚合应该尽量小,只包含真正需要强一致性的对象。
过度设计一个只有增删改查的后台管理,硬上DDD就是给自己找麻烦。DDD是有成本的,简单项目用传统三层就够了。
领域事件丢失领域事件必须在事务提交后 发送。如果在事务内发送,消息出去了但事务回滚了,就会导致数据不一致。可以用Spring的
@TransactionalEventListener或者事务同步器来解决。
七、问题与解答
Q1:DDD和微服务是什么关系?
A:DDD是微服务划分的理论基础。限界上下文(Bounded Context)通常对应一个微服务。但DDD不等于微服务,单体应用也可以用DDD。反过来,微服务也不一定用DDD,很多微服务就是简单的CRUD服务。
Q2:聚合根和实体有什么区别?
A:所有的聚合根都是实体,但不是所有实体都是聚合根。聚合根是聚合的入口,外部只能通过聚合根访问聚合内部的对象。比如Order是聚合根,OrderItem是实体,你不能直接操作OrderItem,必须通过Order.addItem()。
Q3:值对象一定要不可变吗?
A:理论上值对象应该是不可变的,这样能保证线程安全和数据一致性。实践中如果性能敏感,也可以可变,但要确保不会共享引用导致意外修改。我的建议是:先按不可变写,真有性能瓶颈再优化。
八、面试高频考点汇总
考点1:什么是贫血领域模型和充血领域模型?
答案:
- 贫血模型:实体只有getter/setter,业务逻辑全部在Service层。优点是简单,缺点是业务逻辑分散,难以复用。
- 充血模型:实体包含业务方法和状态变更逻辑,Service层只做编排。DDD推荐充血模型,业务内聚在领域对象中。
考点2:DDD中的聚合设计原则是什么?
答案:
- 聚合内强一致性:聚合内的事务必须原子完成
- 聚合间最终一致性:通过领域事件异步同步
- 聚合尽量小:只包含真正需要一起修改的对象
- 聚合根是唯一入口:外部只能通过聚合根访问内部对象
考点3:限界上下文和微服务的关系?
答案 :
限界上下文是业务边界,微服务是技术边界。通常一个限界上下文对应一个微服务,但不是绝对的。如果两个上下文数据耦合很强、调用频繁,也可以放在同一个服务里。划分微服务时要考虑业务边界,也要考虑团队规模、网络开销等实际因素。
考点4:领域事件和普通事件的区别?
答案:
- 领域事件:由领域层触发,表达业务领域发生的有意义的事情(如OrderCreatedEvent)。是DDD的核心概念,不依赖具体框架。
- 普通事件:可以是技术事件(如应用启动事件)或其他层的事件。领域事件强调的是"业务含义"。
考点5:DDD四层架构中,领域层为什么不能依赖其他层?
答案 :
这是依赖倒置原则的体现。领域层包含最核心的业务逻辑,应该独立于框架和技术细节。如果领域层依赖Spring或MyBatis,换框架时就要改领域层代码。通过接口(如Repository)定义契约,由基础设施层实现,领域层只依赖接口,实现技术细节的隔离。
九、模拟面试官提问和参考答案
场景题1:你接手一个遗留项目,Service层有5000行代码,怎么用DDD重构?
参考答案:
- 先识别领域:和业务方沟通,搞清楚核心业务概念(订单、商品、库存等)
- 提取实体:从Service里找出有状态、有标识的对象,封装成Entity
- 提取值对象:把地址、金额等无标识的对象抽成ValueObject
- 识别聚合:看哪些对象总是一起修改,组成聚合
- 拆分应用层:把事务编排、DTO转换移到ApplicationService
- 渐进式重构:不要一次性全改,按模块逐个迁移,保证测试覆盖
场景题2:订单和库存应该是同一个聚合还是不同聚合?
参考答案 :
应该是不同聚合。订单和库存虽然有关联(下单要扣库存),但它们有各自独立的生命周期。库存可能被线下门店、其他渠道修改,不需要和订单强一致。通过领域事件(OrderCreatedEvent → 扣减库存)实现最终一致性,这样两个聚合可以独立扩展。
场景题3:如何防止领域事件丢失?
参考答案:
- 事务内持久化事件:把事件和领域数据存在同一个事务里(如事件表)
- 事务提交后发送 :用
@TransactionalEventListener或事务同步器 - 消息发送确认:MQ发送失败要重试,消费端要幂等
- 事件日志:记录所有事件,便于排查和补偿
场景题4:DDD项目中,一个需求改了,通常要改哪些文件?
参考答案 :
理想情况下只改领域层的相关实体或领域服务。比如修改订单支付规则,只需要改Order.pay()方法。用户接口层、应用层、基础设施层都不需要动。这就是DDD的价值------业务逻辑内聚在领域层,修改影响范围小。
场景题5:如何向团队推广DDD?
参考答案:
- 先小范围试点:找一个核心模块实践,做出效果再推广
- 统一语言:让产品和开发用同一套术语沟通
- 代码评审:重点检查贫血模型、跨聚合直接引用等问题
- 不要追求完美:DDD是渐进的过程,允许有过渡期的"不纯粹"
- 培训分享:定期组织DDD案例分享,让大家看到好处
十、互动话题
你在项目中用过DDD吗?遇到过最大的困难是什么?是团队不理解、业务方不配合,还是不知道怎么划分聚合?欢迎在评论区聊聊你的经历。
参考资料
领域驱动设计:软件核心复杂性应对之道(Eric Evans著)
如果这篇文章对你有帮助,欢迎点赞收藏。有问题也可以在评论区留言,我会尽力回复。