DDD 分层架构:六层分层架构

DDD 分层架构:六层分层架构

目录

上一篇我们聊了 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 读写分离。

相关推荐
无忧智库1 小时前
集团股权激励方案设计:从“发股份”到“绑定长期价值”的10D实战指南(交付版)(PPT)
架构
Ulyanov1 小时前
Python雷达电子对抗仿真引擎(一):打破单体瓶颈,构建微服务与ECS架构的顶层设计
开发语言·python·微服务·云原生·架构·雷达电子对抗
qq_150841992 小时前
SQL的insert和update二合一指令
java·数据库·sql
我不是码农哦2 小时前
AI Agent 工程化实践:从 ReAct 循环到多智能体编排
架构·rails
2601_964702892 小时前
Claude Opus 5 API 开发实战:对话、文本生成与结构化输出
java·服务器·前端
Sayuanni%32 小时前
Spring IOC
java·spring·rpc
风景的人生2 小时前
流式输出与springboot中的响应式编程
java·spring boot·ai编程
咖啡星人k2 小时前
MonkeyCode 架构深度解析:SDD 规范驱动开发的工程哲学
驱动开发·架构·monkeycode
长不胖的路人甲2 小时前
斐波那契查找Java 实现 + 完整思路
java·开发语言