DDD 分层架构:六层分层架构
目录
- 为什么需要分层
- 六层架构概览
- 依赖方向
- 每一层都放什么
- [trigger 和 api 为什么要分开](#trigger 和 api 为什么要分开)
- [api 为什么需要独立 Jar](#api 为什么需要独立 Jar)
- [实战:Spring Boot 项目结构](#实战:Spring Boot 项目结构)
- 常见错误
- 小结
上一篇我们聊了 Repository。接口定义在领域层,实现在基础设施层,应用服务注入接口不关心具体实现。到这里,实体、聚合、领域服务、领域事件、Repository 都有了各自的归属。但有一个问题一直没正面回答:这些对象散落在项目里,该按什么规则组织?
一个 Order 实体、一个 DiscountService、一个 OrderRepository 接口、一个 MybatisOrderRepository 实现、一个 OrderController,它们之间怎么排列?Controller 能直接调 Repository 吗?领域服务能依赖基础设施层的类吗?没有分层规则,项目写到后面还是会乱。
六层架构概览
DDD 的分层方式有多种流派。这里采用六层划分,这套分层在国内 Java 微服务项目中比较常见:
trigger(触发层) → 入口适配,谁来触发这个操作
api(接口契约层) → 服务契约,对外提供什么能力
app(应用层) → 流程编排,业务流程怎么组织
domain(领域层) → 业务核心,规则是什么
infrastructure(基础设施层) → 技术实现,能力怎么落地
types(类型定义层) → 通用类型,跨层共享的基础定义
每层的职责:
| 层 | 职责 | 典型类 |
|---|---|---|
| trigger | 入口适配,把外部请求转换成系统可以理解的输入 | Controller、MQ 消费者、定时任务 |
| api | 定义跨模块或跨服务的能力契约 | RPC 接口、Command、DTO |
| app | 应用能力编排,同时承担启动和配置组装 | ApplicationService、Configuration |
| domain | 承载核心业务规则 | 实体、值对象、聚合、领域服务、Repository 接口 |
| infrastructure | 提供技术能力的外围实现 | Repository 实现、MQ 生产者、外部 API 调用 |
| types | 存放跨领域共享的基础类型 | Response、PageResult、ErrorCode、Constants |
一个 HTTP 请求的处理流程长这样:
HTTP 请求
│
▼
trigger(Controller) ← 入口适配
│ 参数解析、协议转换
▼
api(OrderService 接口) ← 服务契约
│
▼
app(ApplicationService) ← 流程编排
│ 查数据 → 调领域服务 → 保存 → 发事件
▼
domain(领域层) ← 业务核心
│
├── infrastructure(Repository 实现)← 持久化
└── infrastructure(外部 API 调用) ← 调第三方服务
注意看 infrastructure 的位置。它不是"底层",而是外围技术实现。domain 是业务核心,infrastructure 围绕 domain 提供技术能力。这和传统架构"Controller → Service → DAO"自上而下的依赖方向完全不同。
依赖方向
分层的核心不是画线,而是定方向。

几个关键约束:
domain 层不依赖任何其他层。 它不知道请求从哪来(HTTP 还是 MQ),不知道数据怎么存(MySQL 还是 MongoDB),不知道调用方是谁。领域层只关心业务规则本身。
infrastructure 是外围,不是底层。 Repository 接口在 domain 层定义,MybatisOrderRepository 在 infrastructure 层实现。infrastructure 依赖 domain(实现接口),但 domain 不依赖 infrastructure。想换存储方案?写一个新的实现类,domain 层完全不用动。
types 被各层引用,但只提供基础类型。 Response、PageResult、ErrorCode 这些跨领域共享的类型放在 types 里。业务枚举(OrderStatus、UserStatus)不放这里,它们属于各自的领域。
用代码来看这个依赖倒置:
java
// domain 层:定义接口,不知道具体实现
public interface OrderRepository {
Order findById(String orderId);
void save(Order order);
}
// infrastructure 层:实现接口,依赖 domain 层的 Order 和 OrderRepository
@Repository
public class MybatisOrderRepository implements OrderRepository {
@Autowired
private OrderMapper orderMapper;
@Override
public Order findById(String orderId) {
OrderDO orderDO = orderMapper.selectById(orderId);
return toDomain(orderDO);
}
@Override
public void save(Order order) {
orderMapper.insertOrUpdate(toDO(order));
}
}
传统架构里,Service 依赖 DAO,DAO 依赖数据库,依赖方向是"上层 → 下层"。DDD 里,domain 定义接口,infrastructure 实现接口,依赖方向反转了:infrastructure → domain。 业务规则在 domain 层,技术实现在 infrastructure 层,两者通过接口连接,domain 层永远是被依赖的那一方。
这就是依赖倒置原则(DIP)的体现。传统架构里数据库在最底层,DDD 里业务规则才是核心,技术实现反而变成了"插件"。
每一层都放什么
我们举一些例子来说明相关代码应该放在哪一层:
| 代码 | 放在哪层 | 为什么 |
|---|---|---|
| Controller | trigger | 入口适配,接收 HTTP 请求 |
| MQ 消费者 | trigger | 入口适配,接收消息 |
| 定时任务 | trigger | 入口适配,按时间触发 |
| RPC 接口定义 | api | 跨服务的能力契约 |
| Command / DTO | api | 接口层的传输对象 |
| ApplicationService | app | 流程编排,不承载业务规则 |
| @Configuration / AOP | app | 启动配置和切面组装 |
| @Transactional | app | 事务边界由应用层控制 |
| Entity / ValueObject | domain | 核心业务模型 |
| 领域服务 | domain | 承载业务规则 |
| Repository 接口 | domain | 领域层定义持久化需求 |
| 业务枚举(OrderStatus 等) | domain | 业务概念,属于对应领域 |
| Repository 实现 | infrastructure | 提供具体的持久化能力 |
| MQ 生产者 | infrastructure | 技术实现,不是业务规则 |
| 外部 API 调用 | infrastructure | 调第三方服务的技术实现 |
| Response / PageResult | types | 跨领域共享的基础类型 |
| ErrorCode / Constants | types | 跨领域共享的基础定义 |
几个容易放错的地方:
业务枚举不放 types,放 domain。 OrderStatus 定义了订单的生命周期状态,它是订单领域的核心概念。如果把 OrderStatus、UserStatus、PaymentStatus 全扔进 types/enums,最后这个包会变成垃圾桶,和 DDD 按领域组织代码的初衷背道而驰。types 只放 Response、PageResult、ErrorCode 这类真正跨领域共享的基础类型。
@Transactional 放在 app 层,不放在 domain 层。 事务的边界是"一个业务流程包含哪些操作",这是应用层的职责。domain 层的实体方法不应该知道自己是否在事务中。
DTO 放 api 层,不放 domain 层。 DTO 是传输对象,用来在层之间传递数据。domain 层的对象是领域模型,两者关注点不同。
Controller 不直接调 Repository。 Controller 属于 trigger 层,Repository 接口在 domain 层,中间隔了 app 层。如果 Controller 直接调 Repository,就跳过了应用层的流程编排,业务逻辑会散落到入口层。
trigger 和 api 为什么要分开
六层架构和传统四层架构最大的区别之一,是把 trigger 和 api 拆成了两层。很多人觉得这多此一举:Controller 不就是 API 吗?为什么要拆?
因为它们解决的问题不一样。
trigger 是入口适配层。它关心的是"请求怎么进来":HTTP 协议的参数解析、MQ 消息的反序列化、定时任务的触发条件。trigger 不包含业务逻辑,只负责把外部请求转换成系统可以理解的输入。同一个业务操作,入口可能有多个:下单可以通过 HTTP 接口触发,也可以通过 MQ 消息触发(比如后台补单),还可以通过定时任务触发(比如自动取消超时订单)。trigger 层处理的是入口的差异。
api 是服务契约层。它定义的是"这个服务对外提供什么能力",和入口无关。接口签名和参数定义在 api 层,不管是 HTTP 调用还是 MQ 调用,最终都走同一个业务流程。
HTTP 请求 ──→ OrderController(trigger)
│
▼
OrderService(api:同一个契约)
│
▼
OrderApplicationService(app)
MQ 消息 ──→ OrderMessageConsumer(trigger)
│
▼
OrderService(api:同一个契约)
│
▼
OrderApplicationService(app:同一个流程)
拆开之后,新增一个入口只需要加一个 trigger 层的类,api 和 app 层完全不用动。如果不拆,Controller 里既包含参数解析的逻辑,又包含 DTO 定义和 API 契约,换一种入口方式就得改 Controller,改 DTO,改流程编排,牵一发动全身。
api 为什么需要独立 Jar
api 层在很多项目里会单独打成一个 Jar 包(比如 order-api.jar),调用方依赖这个 Jar 来发起 RPC 调用。这不是多此一举,而是微服务架构的刚需。
比如订单服务定义了一个接口:
java
// order-api 模块
public interface OrderService {
OrderDTO createOrder(CreateOrderCommand command);
OrderDTO queryOrder(String orderId);
}
商品服务下单时需要调用订单服务,它不需要知道订单服务的内部实现,只需要引入 order-api.jar,通过 RPC 框架(Dubbo、Feign 等)生成代理对象就能调用:
java
// 商品服务
@Service
public class ProductApplicationService {
@DubboReference
private OrderService orderService; // 引入 order-api.jar 后直接使用
public void createBundleOrder(BundleOrderCommand cmd) {
// 调用订单服务创建订单
OrderDTO order = orderService.createOrder(cmd.toOrderCommand());
}
}
如果 api 没有独立成 Jar,商品服务就得自己定义一份 OrderService 接口和 OrderDTO,两边的定义迟早会不一致。独立 Jar 保证了一份契约,多方引用。
这就是为什么 api 层的接口和 DTO 要尽量保持稳定。一旦发布,改签名就是 Breaking Change,所有依赖方都得跟着改。
实战:Spring Boot 项目结构
把六层架构落地到一个 Spring Boot 项目里,包结构长这样:
shop/
├── shop-trigger/ ← 触发层
│ └── src/main/java/.../trigger/
│ ├── OrderController.java HTTP 入口
│ ├── OrderMessageConsumer.java MQ 入口
│ └── OrderScheduleTask.java 定时任务入口
│
├── shop-api/ ← 接口契约层(独立 Jar)
│ └── src/main/java/.../api/
│ ├── OrderService.java RPC 接口定义
│ └── dto/
│ ├── CreateOrderCommand.java
│ └── OrderDTO.java
│
├── shop-app/ ← 应用层
│ └── src/main/java/.../app/
│ ├── OrderApplicationService.java 流程编排
│ └── config/
│ └── EventConfig.java 配置组装
│
├── shop-domain/ ← 领域层
│ └── src/main/java/.../domain/
│ ├── order/
│ │ ├── Order.java 聚合根
│ │ ├── OrderItem.java 实体
│ │ ├── Money.java 值对象
│ │ ├── DeliveryInfo.java 值对象
│ │ ├── OrderStatus.java 业务枚举
│ │ ├── OrderRepository.java Repository 接口
│ │ └── DiscountService.java 领域服务
│ └── event/
│ └── OrderPaidEvent.java 领域事件
│
├── shop-infrastructure/ ← 基础设施层
│ └── src/main/java/.../infrastructure/
│ ├── persistence/
│ │ ├── MybatisOrderRepository.java Repository 实现
│ │ ├── OrderMapper.java MyBatis Mapper
│ │ ├── OrderDO.java 数据库对象
│ │ └── OrderItemDO.java
│ ├── mq/
│ │ └── OrderEventPublisher.java MQ 生产者
│ └── remote/
│ └── UserFeignClient.java 外部服务调用
│
└── shop-types/ ← 类型定义层
└── src/main/java/.../types/
├── Response.java 通用响应
├── PageResult.java 分页对象
└── ErrorCode.java 错误码定义
来看一个完整的下单流程,六层怎么配合:
java
// trigger 层:入口适配,只做参数解析
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
public Response<OrderDTO> placeOrder(@RequestBody CreateOrderCommand cmd) {
// trigger 层只做参数接收和协议转换,不处理业务逻辑
return Response.success(orderService.createOrder(cmd));
}
}
java
// api 层:定义服务契约
public interface OrderService {
OrderDTO createOrder(CreateOrderCommand command);
OrderDTO queryOrder(String orderId);
}
java
// app 层:流程编排
@Service
public class OrderApplicationService implements OrderService {
private OrderRepository orderRepo;
private DiscountService discountService;
private UserQueryService userQuery;
private OrderEventPublisher eventPublisher;
@Override
@Transactional
public OrderDTO createOrder(CreateOrderCommand cmd) {
// 1. 创建订单
Order order = Order.create(cmd.getUserId(), cmd.getDeliveryInfo());
for (OrderItemCommand item : cmd.getItems()) {
order.addItem(item.getProductId(), item.getProductName(),
item.getPrice(), item.getQuantity());
}
// 2. 查询用户折扣上下文(不直接操作 User 聚合)
DiscountContext context = userQuery.getDiscountContext(cmd.getUserId());
// 3. 计算折扣(领域服务)
Money discount = discountService.calculate(order, context);
order.applyDiscount(discount);
// 4. 保存
orderRepo.save(order);
// 5. 发布事件
eventPublisher.publish(order.getDomainEvents());
order.clearDomainEvents();
return OrderDTO.from(order);
}
}
java
// domain 层:核心业务规则
public class Order {
private final String orderId;
private final String userId;
private OrderStatus status;
private List<OrderItem> items;
private Money totalAmount;
private List<DomainEvent> domainEvents = new ArrayList<>();
public void addItem(String productId, String productName,
Money price, int quantity) {
OrderItem item = new OrderItem(productId, productName, price, quantity);
this.items.add(item);
this.totalAmount = this.totalAmount.add(item.getSubtotal());
}
public void applyDiscount(Money discount) {
this.totalAmount = this.totalAmount.subtract(discount);
}
}
java
// domain 层:领域服务,只依赖领域对象
public class DiscountService {
public Money calculate(Order order, DiscountContext context) {
Money price = order.getTotalAmount();
if (context.isVip()) {
price = price.multiply(context.getDiscountRate());
}
if (context.hasValidCoupon()) {
price = price.subtract(context.getCouponValue());
}
if (price.isNegative()) {
return Money.ZERO;
}
return price;
}
}
java
// infrastructure 层:持久化实现
@Repository
public class MybatisOrderRepository implements OrderRepository {
@Autowired
private OrderMapper orderMapper;
@Override
public Order findById(String orderId) {
OrderDO orderDO = orderMapper.selectById(orderId);
if (orderDO == null) {
throw new BusinessException("订单不存在");
}
return toDomain(orderDO);
}
@Override
public void save(Order order) {
orderMapper.insertOrUpdate(toDO(order));
}
}
整个调用链:
HTTP POST /api/orders
│
▼
OrderController(trigger) ← 入口适配
│ 参数解析
▼
OrderService 接口(api) ← 服务契约
│
▼
OrderApplicationService(app) ← 流程编排
│
├── Order.create() ← domain:创建聚合
├── order.addItem() ← domain:业务行为
├── DiscountService.calculate() ← domain:领域服务
├── orderRepo.save() ← infrastructure:持久化
└── eventPublisher.publish() ← infrastructure:发消息
注意 app 层的 userQuery.getDiscountContext()。它不直接注入 UserRepository 去查 User 聚合,而是通过一个专门的查询服务获取折扣上下文。这样 Order 领域不需要知道 User 聚合的内部结构,只需要一个 DiscountContext 值对象就够了。不同聚合之间不应该直接访问内部模型,通过上下文对象传递必要的信息。
小结
分层的本质是让每一层只关心自己该关心的事。domain 是业务核心,infrastructure 围绕它提供技术能力,app 编排流程,api 定义契约,trigger 处理入口,types 提供基础类型。每一层各司其职,代码才能在业务增长时保持清晰。
到这里,DDD 的战术设计元素和代码组织方式都讲清楚了。但有一个问题被反复提到却没有展开:应用服务负责流程编排,查询和写入是否走的是同一个模型,列表查询是否要加载完整的聚合?下一篇聊 CQRS 读写分离。