大学毕业那会儿第一次读《领域驱动设计》,说实话,前两章就把我劝退了。后来在一家传统企业做核心系统重构,被逼着用 DDD 梳理了三个月业务,才慢慢摸到门道。今天想把自己折腾出来的理解整理一下,不谈晦涩的理论,就说说四层领域模型到底是什么,里面那些基础概念在实战中到底怎么用。
很多人一上来就盯着"四层"两个字,觉得不就是 Controller、Service、Repository 分个层嘛。真不是这么回事。DDD 的核心是让业务语义贯穿整个系统,而不是把代码按照技术职责切几刀。你按三层架构写出来的代码,业务逻辑散落在 Service 里,领域对象退化成只有 getter/setter 的数据类,项目一复杂就成了一锅粥。这正是 DDD 想解决的问题。
1. 四层领域模型的核心结构
DDD 的四层架构,从上到下依次是:接口层(Interface)、应用层(Application)、领域层(Domain)、基础设施层(Infrastructure)。很多文章会把四层画成同心圆,或者上下堆叠的方块,但无论怎么画,依赖方向只有一个:从上往下、从外向内依赖,领域层不依赖任何外层。
1.1 接口层:把"外界"翻译成"领域"
接口层说白了就是系统的门面。它负责接收 HTTP 请求、MQ 消息、定时任务触发、外部 RPC 调用等,然后把外部输入翻译成应用层能听懂的语言。
这里有个新手常犯的错误:直接把实体对象丢给前端当 DTO 用。去年接手过一个订单系统,前端要展示订单信息,后端直接把Order实体序列化返回,结果订单状态的枚举值、金额的精度、时间的格式全都暴露给了前端,前端还时不时发现"多了一个不该暴露的字段"。后来我把接口层迁到独立的 DTO 上,实体彻底从 HTTP 层消失,这种问题才消停。
接口层的职责就三件事:
- 参数校验(格式、必填项、基础合法性)
- 将请求参数组装为应用层需要的输入对象(Command/Query)
- 将应用层返回的结果转换为前端需要的 DTO 并响应
1.2 应用层:编排业务流程,但不承载业务规则
应用层是很多人误解最深的一层。它看起来像旧的 Service 层,但有一条铁律:业务规则不能在应用层里写。
我见过一个支付模块的代码,TransferService里写了几十行"余额是否充足、单笔限额、日累计限额、风控校验"——这些全是业务规则,应该属于领域层。应用层该做什么?它应当只做用例编排:找到对应的领域服务或聚合根,调用它们的方法,把结果组装回去,必要时协调多个领域服务完成一次完整的事务。
打个比方,应用层像餐厅里的大堂经理,知道"客人坐下→点菜→下单→上菜→结账"的流程,但"哪些菜不能搭配"由后厨(领域层)说了算,大堂经理无权更改。
一个典型的应用层方法长这样:
public class OrderAppService { private final OrderRepository orderRepository; public PlaceOrderResult placeOrder(PlaceOrderCommand command) { // 1. 从仓储取出领域对象 Order order = orderRepository.findByOrderId(command.getOrderId()); // 2. 调用聚合根或领域服务执行业务操作 order.place(command.getItems()); // 3. 保存状态变更 List<DomainEvent> events = orderRepository.save(order); // 4. 发布领域事件 eventPublisher.publish(events); return PlaceOrderResult.from(order); } }注意,方法里没有"余额是否充足""库存是否够"这种判断逻辑——这些在order.place()内部由领域模型自己做。
1.3 领域层:系统的灵魂
领域层是四层架构的核心,也是最难设计的一层。它包含实体、值对象、聚合、领域服务、领域事件、仓储接口等元素。服务和应用层可以烂一点,领域层如果设计歪了,整个项目的业务表达就完了。
领域层的核心思想是:把业务规则放进模型里,让模型自己约束自己的状态变化。比如一个订单的cancel()方法,必须自己检查"已发货的订单不允许取消",而不是在 Service 层通过 if 判断。
1.4 基础设施层:最不"业务"的一层
基础设施层是工具层,负责实现领域层声明的仓储接口、数据库访问、消息发送、缓存操作等。它依赖领域层的接口,但不被领域层反向依赖。
很多人有个误区,觉得基础设施层是"最没技术含量的一层"。实际上,这一层决定了系统能不能平稳运行。MyBatis 的 Mapper、Redis 的存取、MQ 的消息投递都在这层实现。早期项目里把 SQL 写在 Service 层的事我没少干,后来强制把数据访问收敛到基础设施层,代码整洁度提升了一个档次。
这里有张表总结四层的职责与依赖关系:
| 层级 | 核心职责 | 能依赖谁 | 典型产物 |
|---|---|---|---|
| 接口层 | 输入输出翻译 | 应用层 | Controller、DTO、MessageListener |
| 应用层 | 用例编排、事务 | 领域层 | AppService、Command/Query |
| 领域层 | 业务规则、核心逻辑 | 自己 | Entity、ValueObject、Aggregate、DomainService |
| 基础设施层 | 技术实现 | 领域层接口、外部框架 | RepositoryImpl、MQProducer、HttpClient |
2. 领域层的基础概念逐个过
2.1 实体(Entity):有唯一标识、会改变的对象
实体的特征是拥有唯一标识(ID)和生命周期。两个属性完全相同的对象,只要 ID 不同,就是两个不同的实体。比如两个人,名字一样、年龄一样,但身份证号不同,就是两个实体。
实体的关键是封装业务规则:
public class Order { private OrderId orderId; private OrderStatus status; private Money totalAmount; public void cancel() { if (this.status == OrderStatus.SHIPPED || this.status == OrderStatus.COMPLETED) { throw new IllegalStateException("已发货或已完成的订单不能取消"); } this.status = OrderStatus.CANCELED; } }这段代码把"不能取消已发货订单"这条规则放进了实体内部。外部调用方不需要知道这条规则,实体自己保证状态变化合法。
2.2 值对象(Value Object):描述性概念,无标识
值对象没有唯一标识,只描述事物的属性。比如"金额"、"地址"、"颜色"。
public class Money { private final BigDecimal amount; private final String currency; public Money add(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException("币种不一致"); } return new Money(amount.add(other.amount), currency); } }值对象最大的价值在于不变性(Immutable)。金额一旦创建,就不能修改,所有操作都返回新对象。这能避免大量setter带来的隐式 bug。
2.3 聚合(Aggregate):保证一致性边界
聚合是一组实体的组合,由聚合根统一对外暴露操作。聚合内部的对象可以互相访问,外部只能通过聚合根访问。
订单聚合通常包含Order(聚合根)、OrderItem、Address。外部代码不能直接得到OrderItem然后该它的数量,必须通过Order的某个方法操作。这保证了订单维度的一致性。
我踩过一个坑:订单和明细用了两个仓储,明细的操作散落在 Service 里,结果经常出现"主订单状态是已支付,明细里却找不到任何商品"的数据不一致。后来把明细收敛到订单聚合内部,通过订单根来管理明细,这种问题基本绝迹。
2.4 领域服务(Domain Service):处理不适合放在实体里的逻辑
有些业务逻辑涉及多个对象,塞进任何一个实体里都不合适。比如"计算两个账户之间的转账金额"涉及账户 A 和账户 B,这时候适合用领域服务:
public class TransferService { public void transfer(Account source, Account target, Money amount) { source.debit(amount); target.credit(amount); } }判断标准很简单:如果逻辑放实体里会让实体承担不该有的职责,就抽出来放领域服务。但别滥用,领域服务应该只承载"跨实体的行为协调",而不是把所有逻辑一股脑丢进去。
2.5 领域事件(Domain Event):捕获业务中"发生的事"
领域事件是 DDD 中比较靠后引入的概念,主要用于解耦聚合间的通信。订单支付成功后发布OrderPaidEvent,库存服务监听这个事件做扣减,通知服务监听它发短信。
public class OrderPaidEvent implements DomainEvent { private final OrderId orderId; private final Instant occurredAt; }事件发布让系统从"同步强耦合"走向"异步解耦",但也引入了最终一致性的问题。对数据一致性要求极高的场景,别盲目用事件。
2.6 仓储(Repository):领域层声明的数据访问接口
仓储接口属于领域层,实现属于基础设施层。领域层定义"怎么查怎么存"的意图,基础设施层负责"用 MyBatis 还是 JPA 实现"。
public interface OrderRepository { Order findById(OrderId orderId); void save(Order order); }仓储和 DAO 的区别在于:DAO 偏向表结构,仓储偏向聚合的持久化。仓储负责把一个完整聚合重新组装起来,或者把改变后的聚合完整存回去。
3. 四层之间的协作流程
用一个"下单"的例子串一下完整流程,从用户请求到数据落库,四个层各自干了什么。
第一步,用户发起POST /orders,请求到达接口层的OrderController。Controller 校验参数格式后,组装成一个PlaceOrderCommand,再调用应用层的orderAppService.placeOrder(command)。
第二步,应用层拿到 command 后,从订单仓储里根据 ID 取出Order聚合根,调用order.placeOrder(items)方法。这个方法内部完成库存校验,计算金额,设定状态,产生领域事件。
第三步,应用层将订单保存回仓储。仓储的实现(基础设施层)把订单和明细拆成多张表进行持久化,发布领域事件到消息队列。
第四步,消息队列把OrderPlacedEvent推给库存服务、通知服务等其他模块。
整个流程中,数据流向是从外到内:接口层 → 应用层 → 领域层 → 基础设施层(再转出去持久化)。
4. 四层架构和经典三层/六边形架构有什么区别
“DDD 的四层领域模型”热搜常和"六边形架构"一起出现。不少人对它们概念混淆,这里说清楚。
传统三层架构分为 Web、Service、DAO 三层。它的默认方向是横向分层,所有层都直接依赖数据访问层。DDD 四层强调领域层是核心,其他层都围绕它转。
六边形架构(端口与适配器架构)是对四层架构的一种演进。外部世界通过"适配器"(Adapter)与"端口"(Port)交互,把一个系统的输入与输出摆在等同位置,更加强调"应用边界"的隔离。四层架构解决的是"领域逻辑与技术实现分离"的问题,六边形架构解决的是"应用与外部依赖隔离"的问题。
如果你在做微服务,我建议直接参考六边形架构的思路来落地 DDD 四层:领域层在最中间,仓储等端口放在边缘,适配器(Controller、Consumer、外部 API Client)在最外层。这样既有了四层的业务分层思维,又获得了六边形架构的高可测试性和可替换性。
5. 实战中常见的坑和对应的处理方式
5.1 缓存、参数校验到底放哪一层
参数校验分两层:格式校验在接口层,语义校验在应用层或领域层。比如"订单 ID 不能为空"是格式校验,放在接口层;"该订单已超过可取消时间"是业务规则,放在领域层。
缓存处理也分两种:只读缓存(如查商品信息)放在基础设施层,因为它是技术实现细节;业务性缓存(如账户实时余额)放领域层做判断,避免从缓存中取到脏数据后做出错误决策。
5.2 事务应该开在哪一层
事务边界应该开在应用层。一次用例 = 一次事务。如果把事务放到领域层,领域方法容易过度依赖事务环境;放到接口层则事务粒度太粗,一个请求涉及多个用例时会出问题。
5.3 领域对象被层层传递的问题
很多团队把Order实体直接从 Controller 传到 AppService、传到领域服务、再传回 Controller,这种操作会让实体和 UI 高度耦合。我的习惯是:Controller 和 AppService 之间传递 Command/DTO,AppService 和领域层之间传递实体。边界分明,谁也不会越界。
5.4 过度设计
DDD 不是银弹。一个简单 CRUD 后台管理系统,硬要拆出四层、定义值对象、设计领域事件,最后只会拖慢开发速度。判断标准很简单:当业务规则复杂到用三层架构难以维护时,才值得引入 DDD。我在实际工作中发现,绝大多数团队是在"业务逻辑确实复杂"的订单、支付、库存这类系统上才真正感受到 DDD 的价值。
写到这里,想起当时啃 DDD 时的一个体会:这四层架构表面上是代码分层,骨子里是认知分层——它逼着团队先想清楚"什么是业务规则",再谈"怎么实现"。
如果你正在重构成这种架构,我建议从订单或支付这类“业务规则密集且不断变化”的模块入手练手,先别一步到位。把一个模块的实体、值对象、仓储梳理清楚,感受一下业务规则内聚带来的维护体验差异,再逐步推广到更多模块。真正消化了聚合的边界和依赖方向之后,你会回来感谢这个"逼你动脑子"的架构风格。