从传统三层到六边形架构:以 order-service 为例拆解模块职责与调用边界
在 Java 后端项目中,很多人一开始都会使用传统三层架构:
Controller -> Service -> Repository
这种结构简单直接,适合小型项目。但随着业务越来越复杂,尤其是涉及订单、支付、消息、状态流转、领域规则、外部系统集成时,传统三层架构很容易出现几个问题:
Controller 越来越重
Service 越来越臃肿
Repository 被到处调用
业务规则散落在各个地方
领域对象只剩 getter/setter
这时,引入六边形架构,也就是 Ports and Adapters Architecture,可以更好地控制模块边界,让业务核心保持稳定,让技术细节可替换、可维护、可测试。
本文以如下 order-service 模块结构为例,说明六边形架构中各模块的职责、依赖关系和调用链。
order-service
├── order-application
├── order-container
├── order-dataaccess
├── order-domain
│ ├── order-application-service
│ └── order-domain-core
└── order-messaging
一、这个结构不是传统三层架构
很多人看到这些模块,容易把它理解成:
Controller -> Service -> Repository
但这个结构本质上不是传统三层,而是六边形架构。
六边形架构的核心思想是:
外部世界
↓
输入适配器 Adapter In
↓
输入端口 Port In
↓
应用服务 Application Service
↓
领域模型 Domain Model
↓
输出端口 Port Out
↓
输出适配器 Adapter Out
↓
外部系统
也就是说,业务核心不应该直接依赖数据库、消息队列、HTTP、第三方接口这些技术细节。
真正的依赖方向应该是:
外层依赖内层
内层不依赖外层
二、整体模块职责
1. order-container:启动装配层
order-container 是整个服务的启动模块,也可以理解为 Composition Root。
它负责:
Spring Boot 启动类
Bean 扫描
模块装配
配置文件加载
启动整个 order-service
典型内容包括:
OrderServiceApplication.java
BeanConfiguration.java
application.properties
它可以依赖其他所有模块,因为它的职责就是把系统组装起来。
但是要注意:
order-container 不应该写业务代码
它只负责启动和装配,不负责订单规则、支付规则、消息处理逻辑。
2. order-application:HTTP 输入适配器
order-application 是外部 HTTP 请求进入系统的入口。
它负责:
Controller
REST Request DTO
参数校验
把 HTTP 请求转换成 Command
调用 input port
返回 Response
例如:
@RestController
@RequiredArgsConstructor
public class OrderController {
private final OrderApplicationService orderApplicationService;
@PostMapping("/orders")
public CreateOrderResponse createOrder(@RequestBody CreateOrderRequest request) {
return orderApplicationService.createOrder(
CreateOrderCommand.from(request)
);
}
}
这里最关键的一点是:
Controller 只调用 Application Service 的 input port
它不应该直接调用:
JPA Repository
RabbitTemplate
KafkaTemplate
数据库 Mapper
消息 Publisher
领域对象复杂逻辑
否则 HTTP 层就会侵入业务层,后期维护会非常困难。
3. order-domain-core:纯领域核心
order-domain-core 是整个系统最核心、最稳定的部分。
它负责表达真正的业务规则。
这里应该放:
Aggregate Root
Entity
Value Object
Domain Service
Domain Event
Domain Exception
领域枚举
业务规则
例如:
Order
OrderItem
Product
Money
OrderStatus
OrderDomainService
OrderCreatedEvent
OrderPaidEvent
OrderCancelledEvent
OrderDomainException
这里不应该出现:
@RestController
@Service
@Component
@Entity
JpaRepository
KafkaTemplate
RabbitTemplate
RedisTemplate
HTTP Client
Spring Security
严格来说,order-domain-core 最好是一个纯 Java 模块。
它只关心业务,不关心技术实现。
比如订单支付这件事,不应该写成:
order.setOrderStatus(OrderStatus.PAID);
而应该写成:
order.pay();
领域对象自己保护自己的状态:
public class Order extends AggregateRoot<OrderId> {
private OrderStatus orderStatus;
public void pay() {
if (this.orderStatus != OrderStatus.PENDING) {
throw new OrderDomainException("Order is not in correct state for pay operation");
}
this.orderStatus = OrderStatus.PAID;
}
public void approve() {
if (this.orderStatus != OrderStatus.PAID) {
throw new OrderDomainException("Order is not paid for approve operation");
}
this.orderStatus = OrderStatus.APPROVED;
}
public void cancel() {
if (this.orderStatus == OrderStatus.APPROVED) {
throw new OrderDomainException("Approved order cannot be cancelled");
}
this.orderStatus = OrderStatus.CANCELLED;
}
}
这样做的好处是:
订单状态规则集中在 Order 聚合内部
不会散落在 Controller、Service、MQ Listener 中
不会变成贫血模型
4. order-application-service:用例编排层
order-application-service 是六边形架构里的应用服务层。
它负责:
用例编排
事务边界
调用领域对象
调用输出端口
发布领域事件
处理 Command
返回 Response
这里通常会放:
ports.input.service
ports.input.message.listener
ports.output.repository
ports.output.message.publisher
dto.command
dto.response
mapper
handler
saga
例如:
@Service
@RequiredArgsConstructor
public class OrderApplicationServiceImpl implements OrderApplicationService {
private final OrderCreateCommandHandler orderCreateCommandHandler;
@Override
@Transactional
public CreateOrderResponse createOrder(CreateOrderCommand command) {
return orderCreateCommandHandler.createOrder(command);
}
}
真正的业务流程编排可以放在 Handler 中:
@Component
@RequiredArgsConstructor
public class OrderCreateCommandHandler {
private final OrderDomainService orderDomainService;
private final OrderRepository orderRepository;
private final CustomerRepository customerRepository;
private final RestaurantRepository restaurantRepository;
private final OrderCreatedPaymentRequestMessagePublisher paymentRequestMessagePublisher;
private final OrderDataMapper orderDataMapper;
public CreateOrderResponse createOrder(CreateOrderCommand command) {
Customer customer = customerRepository.findCustomer(command.customerId());
Restaurant restaurant = restaurantRepository.findRestaurantInformation(command.restaurantId());
Order order = orderDataMapper.createOrderCommandToOrder(command);
OrderCreatedEvent orderCreatedEvent =
orderDomainService.validateAndInitiateOrder(order, restaurant);
Order savedOrder = orderRepository.save(order);
paymentRequestMessagePublisher.publish(orderCreatedEvent);
return orderDataMapper.orderToCreateOrderResponse(savedOrder);
}
}
需要注意的是,这里的:
private final OrderRepository orderRepository;
不是 Spring Data JPA 的 Repository。
它应该是应用层定义的输出端口:
ports.output.repository.OrderRepository
真正的 JPA 实现应该放在 order-dataaccess 里。
5. order-dataaccess:数据库输出适配器
order-dataaccess 是数据库适配器模块。
它负责:
JPA Entity
Spring Data JpaRepository
Repository Adapter
数据库 Mapper
持久化实现
典型结构如下:
order-dataaccess
├── order
│ ├── entity
│ │ ├── OrderEntity
│ │ └── OrderItemEntity
│ ├── mapper
│ │ └── OrderDataAccessMapper
│ ├── repository
│ │ └── OrderJpaRepository
│ └── adapter
│ └── OrderRepositoryImpl
它实现 application-service 中定义的 output port:
@Component
@RequiredArgsConstructor
public class OrderRepositoryImpl implements OrderRepository {
private final OrderJpaRepository orderJpaRepository;
private final OrderDataAccessMapper orderDataAccessMapper;
@Override
public Order save(Order order) {
OrderEntity orderEntity = orderDataAccessMapper.orderToOrderEntity(order);
OrderEntity savedEntity = orderJpaRepository.save(orderEntity);
return orderDataAccessMapper.orderEntityToOrder(savedEntity);
}
}
这就是依赖倒置。
不是应用层依赖数据库,而是数据库适配器依赖应用层定义的接口。
6. order-messaging:消息输入/输出适配器
order-messaging 负责消息相关的技术实现。
它有两类职责。
第一类是消费消息,也就是输入适配器:
PaymentResponseMessageListener
RestaurantApprovalResponseMessageListener
例如支付服务发回支付成功消息,MQ Listener 收到后,不应该直接改数据库,而应该调用 application-service 的 input port。
错误方式:
public void paymentCompleted(PaymentResponse response) {
OrderEntity order = orderJpaRepository.findById(...);
order.setStatus(PAID);
orderJpaRepository.save(order);
}
正确方式:
public void paymentCompleted(PaymentResponse response) {
paymentResponseMessageListener.paymentCompleted(response);
}
然后由应用层编排:
order.pay();
orderRepository.save(order);
第二类是发布消息,也就是输出适配器:
OrderCreatedPaymentRequestMessagePublisher
OrderPaidRestaurantRequestMessagePublisher
它们实现 application-service 中定义的 publisher port。
例如:
@Component
@RequiredArgsConstructor
public class OrderCreatedPaymentRequestKafkaMessagePublisher
implements OrderCreatedPaymentRequestMessagePublisher {
private final KafkaTemplate<String, PaymentRequestAvroModel> kafkaTemplate;
private final OrderMessagingDataMapper orderMessagingDataMapper;
@Override
public void publish(OrderCreatedEvent domainEvent) {
PaymentRequestAvroModel message =
orderMessagingDataMapper.orderCreatedEventToPaymentRequestAvroModel(domainEvent);
kafkaTemplate.send("payment-request-topic", message.getId(), message);
}
}
所以 order-messaging 既可能是:
Adapter In
也可能是:
Adapter Out
关键看它当前是在消费消息,还是在发送消息。
三、模块依赖关系应该怎么设计
推荐依赖方向如下:
order-domain-core
无业务模块依赖,最纯净
order-application-service
depends on order-domain-core
order-application
depends on order-application-service
order-dataaccess
depends on order-application-service
depends on order-domain-core
order-messaging
depends on order-application-service
depends on order-domain-core
order-container
depends on all
画成图大概是:
order-container
│
┌──────────────────────┼──────────────────────┐
│ │ │
order-application order-dataaccess order-messaging
│ │ │
└──────────────┬───────┴──────────────┬───────┘
↓ ↓
order-application-service
↓
order-domain-core
最重要的原则是:
外层模块依赖内层模块
内层模块不能依赖外层模块
也就是说:
Controller 可以依赖 Application Service
Application Service 可以依赖 Domain Core
DataAccess 可以依赖 Application Service 的 Port 接口
Messaging 可以依赖 Application Service 的 Port 接口
Domain Core 不知道任何外部技术
四、一次创建订单的完整调用链
假设用户调用:
POST /orders
完整调用链应该是:
1. order-application
OrderController.createOrder(request)
2. order-application-service
OrderApplicationService.createOrder(command)
3. order-application-service
OrderCreateCommandHandler.createOrder(command)
4. order-application-service 调 output port
CustomerRepository.findCustomer(...)
RestaurantRepository.findRestaurantInformation(...)
5. order-dataaccess 实现 output port
CustomerRepositoryImpl
RestaurantRepositoryImpl
6. order-domain-core
OrderDomainService.validateAndInitiateOrder(order, restaurant)
Order.initializeOrder()
Order.validateOrder()
Order.validatePrice()
7. order-application-service 调 output port
OrderRepository.save(order)
8. order-dataaccess
OrderRepositoryImpl.save(order)
OrderEntityMapper
OrderJpaRepository.save(entity)
9. order-application-service 发布领域事件
OrderCreatedPaymentRequestMessagePublisher.publish(orderCreatedEvent)
10. order-messaging
Kafka/RabbitMQ Publisher 发送 PaymentRequest
11. order-application
返回 CreateOrderResponse
这条链路中有几个非常重要的边界:
Controller 不知道 JPA
Application Service 不知道 KafkaTemplate
Domain Core 不知道 Spring
DataAccess 不知道 Controller
Messaging 不直接操作数据库状态
这就是六边形架构真正想解决的问题。
五、支付成功消息的完整调用链
假设 payment-service 发回支付成功消息:
PaymentResponse
调用链应该是:
1. order-messaging
PaymentResponseKafkaListener 接收消息
2. order-messaging
把 Avro / JSON 消息转成 application-service 的 message dto
3. order-application-service
PaymentResponseMessageListener.paymentCompleted(response)
4. order-application-service
OrderPaymentSaga.process(response)
5. order-application-service
OrderRepository.findById(...)
6. order-dataaccess
OrderRepositoryImpl.findById(...)
7. order-domain-core
order.pay()
8. order-application-service
OrderRepository.save(order)
9. order-application-service
OrderPaidRestaurantRequestMessagePublisher.publish(orderPaidEvent)
10. order-messaging
发送 RestaurantApprovalRequest
这里最重要的是:
MQ Listener 不应该直接 order.setStatus(PAID)
而应该让领域对象自己完成状态变化:
order.pay();
这样订单状态规则就不会散落到消息层。
六、Port 和 Adapter 在项目里的映射
1. 输入端口 Port In
输入端口放在:
order-application-service/ports/input
例如:
public interface OrderApplicationService {
CreateOrderResponse createOrder(CreateOrderCommand command);
TrackOrderResponse trackOrder(TrackOrderQuery query);
}
也可以有消息输入端口:
public interface PaymentResponseMessageListener {
void paymentCompleted(PaymentResponse response);
void paymentCancelled(PaymentResponse response);
}
它们表示:
外部世界可以通过哪些方式驱动订单领域
2. 输入适配器 Adapter In
输入适配器通常放在:
order-application
order-messaging
HTTP Controller 是输入适配器:
@RestController
public class OrderController {
private final OrderApplicationService orderApplicationService;
}
MQ Consumer 也是输入适配器:
@Component
public class PaymentResponseKafkaListener {
private final PaymentResponseMessageListener paymentResponseMessageListener;
}
它们都负责把外部请求转换成应用层可以理解的输入。
3. 输出端口 Port Out
输出端口放在:
order-application-service/ports/output
例如:
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(OrderId orderId);
}
以及:
public interface OrderCreatedPaymentRequestMessagePublisher {
void publish(OrderCreatedEvent event);
}
它们表示:
应用层需要外部世界帮它做什么
比如:
保存订单
查询客户
查询餐厅
发送支付请求
发送餐厅审核请求
4. 输出适配器 Adapter Out
输出适配器放在:
order-dataaccess
order-messaging
数据库适配器:
@Component
public class OrderRepositoryImpl implements OrderRepository {
}
消息发布适配器:
@Component
public class OrderCreatedPaymentRequestKafkaPublisher
implements OrderCreatedPaymentRequestMessagePublisher {
}
它们负责把应用层定义的接口变成真实技术实现。
七、最容易写错的几个地方
错误一:Controller 直接调用 Repository
错误写法:
@RestController
public class OrderController {
private final OrderJpaRepository orderJpaRepository;
}
正确写法:
@RestController
public class OrderController {
private final OrderApplicationService orderApplicationService;
}
Controller 不应该知道数据库怎么查,也不应该知道订单怎么保存。
错误二:Application Service 直接依赖 JpaRepository
错误写法:
@Service
public class OrderApplicationServiceImpl {
private final OrderJpaRepository orderJpaRepository;
}
正确写法:
@Service
public class OrderApplicationServiceImpl {
private final OrderRepository orderRepository;
}
其中 OrderRepository 是 application-service 定义的 output port。
错误三:Domain Core 里出现 JPA 注解
如果走严格 DDD / 六边形架构,不建议这样写:
@Entity
@Table(name = "orders")
public class Order {
}
更推荐分开:
domain-core:
Order
dataaccess:
OrderEntity
然后通过 Mapper 转换:
Order <-> OrderEntity
这样领域模型不会被数据库结构污染。
错误四:MQ Listener 直接改状态
错误写法:
public void paymentCompleted(PaymentResponse response) {
OrderEntity order = orderJpaRepository.findById(...);
order.setStatus(PAID);
orderJpaRepository.save(order);
}
正确写法:
public void paymentCompleted(PaymentResponse response) {
paymentResponseMessageListener.paymentCompleted(response);
}
然后在应用层:
order.pay();
orderRepository.save(order);
状态规则应该在领域对象里,而不是消息适配器里。
错误五:领域对象只有 getter/setter
错误写法:
@Getter
@Setter
public class Order {
private OrderStatus orderStatus;
}
这就是典型贫血模型。
更好的写法是:
public class Order {
private OrderStatus orderStatus;
public void pay() {
validateOrderCanBePaid();
this.orderStatus = OrderStatus.PAID;
}
private void validateOrderCanBePaid() {
if (this.orderStatus != OrderStatus.PENDING) {
throw new OrderDomainException("Order cannot be paid");
}
}
}
也就是说,领域对象不只是数据容器,它应该拥有行为和规则。
八、如何判断代码应该放在哪一层?
写代码时,不要先问:
这个代码应该放在哪个 Service?
而应该问:
这个规则是谁的职责?
比如:
订单只能从 PENDING 变成 PAID
这是订单自身的规则,应该放在:
Order 聚合
所以应该写:
order.pay();
而不是:
if (order.getStatus() == PENDING) {
order.setStatus(PAID);
}
再比如:
支付完成后,需要推动餐厅审核
这不是 Order 自己的职责,因为它涉及跨系统流程编排。
它应该放在:
OrderPaymentSaga
OrderPaymentResponseHandler
Application Service
再比如:
怎么把 Order 保存到 PostgreSQL
这不是领域层职责。
它应该放在:
order-dataaccess
再比如:
怎么把 OrderPaidEvent 发到 RabbitMQ
这也不是领域层职责。
它应该放在:
order-messaging
九、一句话总结每个模块
order-application
负责接 HTTP 请求,不负责业务规则。
order-container
负责启动和装配,不负责业务规则。
order-dataaccess
负责数据库技术实现,不负责业务规则。
order-domain-core
负责真正的业务对象和业务规则。
order-application-service
负责用例编排、事务、调用领域对象、定义端口。
order-messaging
负责消息收发,不负责业务规则。
十、最终心法
六边形架构不是为了把项目拆得更复杂,而是为了让复杂业务有清晰边界。
最核心的一句话是:
domain-core 负责“业务是什么”;
application-service 负责“这个用例怎么编排”;
application、dataaccess、messaging 负责“外部怎么进来、怎么出去”;
container 负责“把它们装起来”。
当你坚持这个边界之后,系统会有几个明显好处:
业务规则更集中
领域模型更干净
技术细节更容易替换
测试更容易写
模块之间不容易乱依赖
Service 不容易变成上帝类
对于复杂订单系统、支付系统、任务编排系统、消息驱动系统来说,这种结构会比传统三层架构更适合长期演进。