
Ddd Architecture Hexagonal
- 15 installs
- 1 repo stars
- Updated July 29, 2026
- full-statck-skills/ddd-skills
Implements Alistair Cockburn's Hexagonal (Ports & Adapters) architecture isolating domain logic behind ports with driving/driven adapters in Spring Boot.
About
Guides Hexagonal Architecture implementation with primary/secondary adapters, use-case and repository ports, and dependency injection. A developer uses it to build a DDD system with clean separation between domain and infrastructure.
- Six-step primary-to-secondary adapter flow
- Dependency and port-granularity rules
Ddd Architecture Hexagonal by the numbers
- 15 all-time installs (skills.sh)
- Ranked #3,491 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/full-statck-skills/ddd-skills --skill ddd-architecture-hexagonalAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 29, 2026 |
| Repository | full-statck-skills/ddd-skills ↗ |
What it does
Implements Alistair Cockburn's Hexagonal (Ports & Adapters) architecture isolating domain logic behind ports with driving/driven adapters in Spring Boot.
Files
DDD Architecture — Hexagonal (Ports & Adapters)
Alistair Cockburn (2005) — 业务逻辑通过端口(Ports)隔离,外部系统通过适配器(Adapters)接入。适用受众: 后端架构师/技术负责人(团队 > 5 人)、DDD 实践者。定制机制: 提供技术栈/入口类型/团队规模三个配置维度。加载本技能时,建议先阅读 Workflow 和 When to Use 确认匹配度。
"允许应用被用户、程序、自动化测试或批处理脚本平等驱动,并在脱离最终运行设备和数据库的情况下开发和测试。" — Alistair Cockburn
Workflow
Step 1: 主适配器协议转换 — REST Controller/CLI 接收请求,完成参数校验与协议转换
Step 2: 入站端口调用 — 适配器调用 UseCase 接口,触发应用层用例编排
Step 3: 应用服务编排 — Application Service 编排用例,调用领域模型执行业务逻辑
Step 4: 领域模型执行 — 聚合根/实体执行业务逻辑,输出结果或领域事件
Step 5: 出站端口持久化 — 应用服务通过 Repository 接口保存领域状态
Step 6: 次适配器技术实现 — Adapter 执行 DB/MQ/外部 API 的技术实现
定义验证法: 脱离数据库和 HTTP 即可跑通全部领域层单元测试 → 边界正确。
Rules
1. 依赖规则: 依赖方向必须从外到内——Adapter → Application → Domain。Domain 层零外部依赖。 2. 端口粒度规则: 每个端口职责单一,包含 10+ 方法的端口需立即拆分。 3. 异常转换规则: 技术异常(SQLException、TimeoutException)必须在 Adapter 内部捕获并转换为领域异常,不得向上泄漏。
When to Use
✅ 适合场景
1. 多入口系统(REST + CLI + MQ + gRPC):多个外部系统同时驱动同一业务逻辑 2. 基础设施频繁变更(换 DB/MQ/缓存):只需换 Adapter 实现,Domain 零修改 3. 极致可测试性:Mock 端口即可测试全部领域逻辑,不依赖数据库 4. 微服务架构标准化:团队 > 5 人,需要各服务一致的架构约定 5. 团队熟悉抽象设计:能合理设计端口粒度,避免过度抽象
Boundary
⚠️ 需要条件
1. 团队理解抽象设计:端口粒度需要领域知识和抽象能力,否则可能定义不当 2. 项目规模适中:小项目用 Layered 更简单,六边形增加间接层成本 3. 需要 DDD 领域模型配合:Port 接口定义需要领域建模前置知识 4. 良好的 DI 容器支持:Spring/Guice 等框架可简化适配器装配
❌ 不适用(附替代方案)
1. 单一 REST API + 简单 CRUD → 用 ddd-architecture-layered 2. 中文企业 MyBatis 生态 → 用 ddd-architecture-cola 3. 需 UseCase + Entity 严格分离 → 用 ddd-architecture-clean 4. 快速原型 / MVP 阶段 → 直接用传统三层(不引入六边形架构的开销)
核心原理
三大抽象
Driving Side (REST/CLI/gRPC/Test) → Domain Core ← Driven Side (PostgreSQL/RabbitMQ/Redis/Stripe)| 概念 | 说明 | 代码体现 |
|---|---|---|
| Port(端口) | 领域层定义的接口 | interface OrderRepository |
| Primary Adapter(主适配器) | 外部如何驱动系统 | REST Controller、CLI Command、gRPC Service |
| Secondary Adapter(次适配器) | 系统如何驱动外部 | JPA RepositoryImpl、Kafka Producer、StripeClient |
Strong Port vs Weak Port
// ❌ Weak — 泄漏 SQL 概念
interface OrderRepository { List<Order> findByQuery(String sql, Map<String, Object> params); }
// ✅ Strong — 纯领域概念
interface OrderRepository {
Optional<Order> findById(OrderId id);
List<Order> findByCustomerId(CustomerId customerId);
void save(Order order);
}目录结构
{project}/
├── {project}-domain/ # 领域核心 + 端口定义(零框架依赖)
│ └── port/{inbound, outbound}/ # ★ 端口接口
├── {project}-application/ # 应用层(UseCase 实现)
├── {project}-adapter/ # 适配器层
│ ├── inbound/ # ★ 主适配器(REST/CLI/gRPC/MQ)
│ └── outbound/ # ★ 次适配器(Persistence/Messaging/External)
└── {project}-configuration/ # 配置层(DI 装配)开发规范
层职责
| 层 | 依赖 | 允许做的事 | 禁止做的事 |
|---|---|---|---|
| Domain | 无 | 实体行为、值对象、领域事件、端口定义 | import 框架注解、SQL、HTTP |
| Application | Domain | 用例编排、事务管理、端口调用 | if/else 业务判断、直接操作 DB |
| Adapter | Application | 协议转换、参数校验、异常映射 | 包含业务逻辑、直接操作 Domain 内部状态 |
| Configuration | 全部 | DI 装配、Profile 配置 | 包含业务代码 |
代码规范
// 入站端口(Domain)
public interface CreateOrderUseCase { OrderCreatedResult execute(CreateOrderCommand command); }
// 出站端口(Domain)
public interface OrderRepository { void save(Order order); Optional<Order> findById(OrderId id); }
// 应用服务(实现入站端口,注入出站端口)
@ApplicationService
public class CreateOrderService implements CreateOrderUseCase {
private final OrderRepository orderRepository;
@Override @Transactional
public OrderCreatedResult execute(CreateOrderCommand command) {
Order order = Order.create(command.getCustomerId());
orderRepository.save(order);
return OrderCreatedResult.from(order);
}
}
// 主适配器(仅协议转换)
@RestController
public class OrderController {
@PostMapping("/orders")
public ResponseEntity<CreateOrderResponse> create(@RequestBody @Valid CreateOrderRequest req) {
var result = createOrderUseCase.execute(req.toCommand());
return ResponseEntity.status(201).body(CreateOrderResponse.from(result));
}
}
// 次适配器(技术实现)
@Repository
public class PostgresOrderRepository implements OrderRepository {
@Override public void save(Order order) { jpaRepo.save(mapper.toPO(order)); }
}落地步骤
Phase 1: 定义端口(1-2 天)→ 入站端口(UseCase) → 出站端口(Repository/Gateway)
Phase 2: 领域模型(2-3 天)→ 聚合根/实体/值对象 → 领域服务 → 领域事件
Phase 3: 应用服务(1-2 天)→ UseCase 实现(注入端口,编排调用)
Phase 4: 适配器(2-4 天) → 主适配器(REST/gRPC/CLI) → 次适配器(DB/MQ/External)
Phase 5: DI 装配 + 测试(1-2 天)→ DI 配置 → 端口 Mock 测试 → 适配器集成测试Security & Stability
- 本技能为纯文档型架构指南,不收集、不处理、不上传用户数据。
- 所有代码模板均为教学参考。替换外部服务 URL、密钥和凭证为环境变量配置。
- Port/Adapter 隔离确保领域逻辑不直接依赖 HTTP/DB/MQ 库——Adapter 层处理所有 I/O。
- 实现 Secondary Adapter 时,始终设置连接/读取超时,对关键路径实现断路器模式。
Gotchas(8 条常见陷阱)
1. 端口定义过宽: 每个端口只做一件事。包含 10+ 方法的端口视为"上帝端口",立即拆分。 2. 主适配器包含业务逻辑: Controller 只能做参数转换和调用 UseCase,不能包含 if/else 业务判断。 3. 次适配器忘记异常转换: 技术异常(SQLException、TimeoutException)必须在适配器内部转换为领域异常。 4. 领域层框架依赖: Domain 模块不能 import Spring/JPA/MyBatis 注解。 5. 端口命名不一致: 入站端口用动作命名(CreateOrderUseCase),出站端口用资源命名(OrderRepository)。 6. 事务放在适配器层: 事务由应用层控制,适配器不负责事务管理。 7. 过度抽象: 不为"未来可能会换"提前创建端口。等真正需要替换时再抽取端口。 8. 用户需求不清晰时先给假设: 用户没说技术栈/入口类型/团队规模时,先给假设版本再补充提问。
FAQ(8 条常见问题)
Q1: 六边形和整洁架构有什么区别? A: 六边形由 Cockburn 提出(2005),核心是端口/适配器;整洁由 Uncle Bob 提出(2012),强调 UseCase 和 Entity 分离。六边形更关注"对称性"(主/次适配器),整洁更关注"依赖规则"。 Q2: 端口应该放在 Domain 层还是 Application 层? A: 推荐放在 Domain 层。Domain 是业务核心,端口是业务对外部依赖的抽象。 Q3: 如何避免端口过度抽象? A: YAGNI 原则:只为当前确定的变更需求定义端口。 Q4: 一个 UseCase 接口一个方法还是多个方法? A: 推荐一个接口一个方法(接口隔离原则)。但高度相关的方法(如 OrderRepository 的 save/findById)可放同一接口。 Q5: Controller 层的 DTO 和 Domain 层的 Entity 能否共用? A: 不能。DTO 是适配器层的数据载体,Entity 是领域层的业务模型,需要 Mapper 转换。 Q6: 六边形架构如何处理查询? A: 查询也通过端口进行。使用 QueryOrderUseCase 返回只读 DTO,复杂查询可用 CQRS。 Q7: 如何验证六边形边界是否正确? A: 能否不启动数据库和 HTTP 就跑通 Domain 层全部单元测试?能 → 正确。不能 → 有泄漏。 Q8: 团队不熟悉六边形架构怎么办? A: 逐步引入。先做好 Domain 层纯净度和端口定义,再逐步抽取适配器。
References
| 文件 | 目的 |
|---|---|
| references/01-port-definitions.md | 端口定义规范 — 入站/出站端口、命名、粒度、Strong/Weak |
| references/02-primary-adapters.md | 主适配器详解 — REST/CLI/gRPC/MQ 四种适配器实现 |
| references/03-secondary-adapters.md | 次适配器详解 — JPA/MyBatis/Stripe/RabbitMQ/InMemory |
| references/04-domain-model.md | 领域模型设计 — 聚合根、实体、值对象、领域服务、领域事件 |
| references/05-application-services.md | 应用服务层 — UseCase 实现、编排规范、命令对象 |
| references/06-di-configuration.md | DI 配置 — Spring Config、Profile 切换、多环境适配器 |
| references/07-testing.md | 测试策略 — 领域层/应用层/适配器/架构 四级测试 |
| references/08-migration.md | 渐进迁移 — 从传统三层到六边形的迁移指南 |
| examples/01-order-hexagonal-complete.md | 完整订单六边形示例 — Domain/Port/Service/Adapter 全流程 |
| examples/02-user-registration-example.md | 用户注册示例 — 值对象、验证码、领域事件 |
| examples/03-multi-entry-points-example.md | 多入口系统示例 — 同一 UseCase 供 REST/CLI/Kafka/gRPC 调用 |
| examples/04-adapter-swapping-example.md | 适配器可替换性示例 — Postgres→MongoDB、Stripe→PayPal 零代码修改 |
| examples/05-port-swapping-test.md | 主适配器可替换性示例 — REST/gRPC/CLI 三种入口 + 端口级测试 |
| examples/06-monolith-simple.md | 单体简单六边形 — 单模块四包 + ports/adapters 子包结构 |
| examples/07-monolith-complex.md | 单体复杂六边形 — 多端口 + 多适配器 + 多聚合根 |
| examples/08-monolith-multi-module.md | 单体多模块六边形 — Maven 多模块编译期边界约束 |
| examples/09-microservice-simple.md | 微服务简单六边形 — 单微服务内最小六边形结构 |
| examples/10-microservice-complex.md | 微服务复杂六边形 — 多聚合 + Saga + 跨服务调用 |
| examples/11-microservice-multi-module.md | 微服务多模块六边形 — 每服务内部 Maven 多模块 |
| examples/12-microservice-complex-multi.md | 微服务复杂多模块 — CQRS + 读写分离 + CDC + 分布式 Saga |
Primary Sources
- Hexagonal Architecture — Alistair Cockburn (2005)
- Hexagonal Architecture Explained — Cockburn & Garrido de Paz (2024)
- Domain-Driven Design: The Blue Book — Eric Evans (2003)
- AWS: Hexagonal Architecture Pattern
导航
- → Next: domain-designer — 为六边形架构设计领域模型
- 🔗 Related: testing-strategist — 端口 Mock 测试 | api-designer — 六边形 API 设计
💡 六边形 = 端口 + 适配器。验证法:不启动数据库和 HTTP,只跑 CLI/单元测试就能执行业务逻辑 → 边界正确。
Example: 完整六边形订单示例
本示例展示一个订单创建和支付流程的完整六边形架构实现,涵盖 Domain、Application、Adapter 三层。
领域模型
// ─── 值对象 ───
// domain/model/order/OrderId.java
public final class OrderId {
private final String value;
private OrderId(String value) { this.value = value; }
public static OrderId generate() {
return new OrderId("ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());
}
public static OrderId of(String value) { return new OrderId(value); }
public String getValue() { return value; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
}
// domain/model/order/OrderStatus.java
public enum OrderStatus {
DRAFT, PAID, SHIPPED, DELIVERED, CANCELLED;
public boolean canTransitionTo(OrderStatus target) {
return switch (this) {
case DRAFT -> target == PAID || target == CANCELLED;
case PAID -> target == SHIPPED || target == CANCELLED;
case SHIPPED -> target == DELIVERED;
default -> false;
};
}
public boolean canPay() { return this == DRAFT; }
}
// ─── 聚合根 ───
// domain/model/order/Order.java
public class Order extends AggregateRoot<OrderId> {
private OrderId id;
private CustomerId customerId;
private Money totalAmount;
private OrderStatus status;
private List<OrderItem> items;
private LocalDateTime createdAt;
protected Order() {}
public static Order create(CustomerId customerId) {
Order order = new Order();
order.id = OrderId.generate();
order.customerId = customerId;
order.status = OrderStatus.DRAFT;
order.items = new ArrayList<>();
order.totalAmount = Money.ZERO;
order.createdAt = LocalDateTime.now();
order.addDomainEvent(new OrderCreatedEvent(order.id, customerId));
return order;
}
public void addItem(ProductId productId, int quantity, Money unitPrice) {
if (status != OrderStatus.DRAFT) throw new OrderException("只能向草稿添加商品");
items.add(new OrderItem(productId, quantity, unitPrice));
recalculateTotal();
}
public void pay(PaymentId paymentId) {
if (!status.canPay()) throw new OrderException("当前状态不可支付");
this.status = OrderStatus.PAID;
addDomainEvent(new OrderPaidEvent(this.id, this.totalAmount, paymentId));
}
public void cancel(String reason) {
if (status == OrderStatus.DELIVERED || status == OrderStatus.CANCELLED) {
throw new OrderException("已发货或已取消的订单不可取消");
}
this.status = OrderStatus.CANCELLED;
addDomainEvent(new OrderCancelledEvent(this.id, reason));
}
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
public OrderId getId() { return id; }
public OrderStatus getStatus() { return status; }
public Money getTotalAmount() { return totalAmount; }
public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
}端口定义
// domain/port/inbound/CreateOrderUseCase.java
public interface CreateOrderUseCase {
OrderCreatedResult execute(CreateOrderCommand command);
}
// domain/port/inbound/PayOrderUseCase.java
public interface PayOrderUseCase {
PaymentResult execute(PayOrderCommand command);
}
// domain/port/inbound/GetOrderUseCase.java
public interface GetOrderUseCase {
OrderDTO execute(GetOrderQuery query);
}
// domain/port/outbound/OrderRepository.java
public interface OrderRepository {
Optional<Order> findById(OrderId id);
void save(Order order);
void delete(Order order);
}
// domain/port/outbound/PaymentGateway.java
public interface PaymentGateway {
PaymentResult charge(Money amount, PaymentMethod method);
RefundResult refund(PaymentId paymentId, Money amount);
}
// domain/port/outbound/EventPublisher.java
public interface EventPublisher {
void publish(DomainEvent event);
void publishAll(List<DomainEvent> events);
}应用服务
// application/service/CreateOrderService.java
@ApplicationService
public class CreateOrderService implements CreateOrderUseCase {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final EventPublisher eventPublisher;
public CreateOrderService(OrderRepository orderRepository,
ProductRepository productRepository,
EventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
this.eventPublisher = eventPublisher;
}
@Override
@Transactional
public OrderCreatedResult execute(CreateOrderCommand command) {
Order order = Order.create(command.getCustomerId());
for (var item : command.getItems()) {
Product product = productRepository.findById(item.getProductId())
.orElseThrow(() -> new ProductNotFoundException(item.getProductId()));
order.addItem(product.getId(), item.getQuantity(), product.getPrice());
}
orderRepository.save(order);
eventPublisher.publishAll(order.getDomainEvents());
return OrderCreatedResult.from(order);
}
}
// application/service/PayOrderService.java
@ApplicationService
public class PayOrderService implements PayOrderUseCase {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final EventPublisher eventPublisher;
// ... 注入 + execute 实现
}主适配器(REST Controller)
// adapter/inbound/web/OrderController.java
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final CreateOrderUseCase createOrder;
private final PayOrderUseCase payOrder;
private final GetOrderUseCase getOrder;
public OrderController(CreateOrderUseCase createOrder,
PayOrderUseCase payOrder,
GetOrderUseCase getOrder) {
this.createOrder = createOrder;
this.payOrder = payOrder;
this.getOrder = getOrder;
}
@PostMapping
public ResponseEntity<CreateOrderResponse> create(@RequestBody @Valid CreateOrderRequest req) {
var result = createOrder.execute(req.toCommand());
return ResponseEntity.status(201).body(CreateOrderResponse.from(result));
}
@GetMapping("/{id}")
public ResponseEntity<OrderDTO> get(@PathVariable String id) {
var order = getOrder.execute(new GetOrderQuery(id));
if (order == null) return ResponseEntity.notFound().build();
return ResponseEntity.ok(order);
}
@PostMapping("/{id}/pay")
public ResponseEntity<PaymentResponse> pay(@PathVariable String id,
@RequestBody @Valid PayOrderRequest req) {
var result = payOrder.execute(new PayOrderCommand(id, req.getPaymentMethod()));
return ResponseEntity.ok(PaymentResponse.from(result));
}
}次适配器(JPA 实现)
// adapter/outbound/persistence/PostgresOrderRepository.java
@Repository
public class PostgresOrderRepository implements OrderRepository {
private final JpaOrderRepository jpaRepo;
private final OrderMapper mapper;
public PostgresOrderRepository(JpaOrderRepository jpaRepo, OrderMapper mapper) {
this.jpaRepo = jpaRepo;
this.mapper = mapper;
}
@Override
public Optional<Order> findById(OrderId id) {
return jpaRepo.findById(id.getValue()).map(mapper::toDomain);
}
@Override
public void save(Order order) {
var po = mapper.toPO(order);
jpaRepo.save(po);
}
@Override
public void delete(Order order) {
jpaRepo.deleteById(order.getId().getValue());
}
}
// adapter/outbound/persistence/mapper/OrderMapper.java
@Component
public class OrderMapper {
public OrderPO toPO(Order domain) {
// Domain → PO 转换
OrderPO po = new OrderPO();
po.setId(domain.getId().getValue());
po.setCustomerId(domain.getCustomerId().getValue());
po.setStatus(domain.getStatus().name());
po.setTotalAmount(domain.getTotalAmount().getAmount());
po.setCurrency(domain.getTotalAmount().getCurrencyCode());
po.setCreatedAt(domain.getCreatedAt());
return po;
}
public Order toDomain(OrderPO po) {
// PO → Domain 转换(通过工厂方法重建)
Order order = Order.create(CustomerId.of(po.getCustomerId()));
// ... 恢复状态
return order;
}
}DI 配置
// configuration/config/BeanConfig.java
@Configuration
public class BeanConfig {
@Bean
public OrderRepository orderRepository(JpaOrderRepository jpaRepo, OrderMapper mapper) {
return new PostgresOrderRepository(jpaRepo, mapper);
}
@Bean
public PaymentGateway paymentGateway(StripeClient stripeClient) {
return new StripePaymentGateway(stripeClient);
}
@Bean
public CreateOrderUseCase createOrderUseCase(
OrderRepository orderRepository,
ProductRepository productRepository,
EventPublisher eventPublisher) {
return new CreateOrderService(orderRepository, productRepository, eventPublisher);
}
@Bean
public PayOrderUseCase payOrderUseCase(
OrderRepository orderRepository,
PaymentGateway paymentGateway,
EventPublisher eventPublisher) {
return new PayOrderService(orderRepository, paymentGateway, eventPublisher);
}
}单元测试
// 领域层测试 — 零 Mock
@Test
void should_create_order_and_transition_status() {
var order = Order.create(CustomerId.of("cust-001"));
assertEquals(OrderStatus.DRAFT, order.getStatus());
order.pay(PaymentId.of("PAY-001"));
assertEquals(OrderStatus.PAID, order.getStatus());
assertTrue(order.getDomainEvents().stream()
.anyMatch(e -> e instanceof OrderPaidEvent));
}
// 应用层测试 — Mock 端口
@Test
void should_create_order_via_usecase() {
var orderRepo = mock(OrderRepository.class);
var productRepo = mock(ProductRepository.class);
var eventPub = mock(EventPublisher.class);
var service = new CreateOrderService(orderRepo, productRepo, eventPub);
when(productRepo.findById(any())).thenReturn(Optional.of(mock(Product.class)));
var result = service.execute(validCommand());
assertNotNull(result.getOrderId());
verify(orderRepo).save(any(Order.class));
}数据流转
HTTP POST /api/v1/orders
│
▼
OrderController.create() ← 主适配器
│ 请求 → DTO → Command
▼
CreateOrderService.execute() ← 应用服务(实现入站端口)
│ Order.create() ← 领域模型
│ productRepository.findById() ← 出站端口调用
│ order.addItem() ← 领域模型行为
│ orderRepository.save() ← 出站端口调用
▼
PostgresOrderRepository.save() ← 次适配器实现
│ mapper.toPO() → jpaRepo.save()
▼
DatabaseExample: 用户注册六边形示例
本示例展示用户注册流程,强调值对象设计、领域事件和端口隔离。
值对象
// domain/model/user/Email.java
public final class Email {
private final String value;
private Email(String value) {
if (value == null || !value.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
throw new IllegalArgumentException("无效邮箱: " + value);
}
this.value = value.toLowerCase();
}
public static Email of(String value) { return new Email(value); }
public String getValue() { return value; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Email email = (Email) o;
return value.equals(email.value);
}
@Override
public int hashCode() { return value.hashCode(); }
}
// domain/model/user/PhoneNumber.java
public final class PhoneNumber {
private final String countryCode;
private final String number;
// ... 工厂方法、equals/hashCode
}
// domain/model/user/UserId.java
public final class UserId {
private final String value;
// ... 工厂方法
}聚合根
// domain/model/user/User.java
public class User extends AggregateRoot<UserId> {
private UserId id;
private Email email;
private PhoneNumber phone;
private UserStatus status;
private LocalDateTime registeredAt;
protected User() {}
public static User register(Email email, PhoneNumber phone) {
User user = new User();
user.id = UserId.generate();
user.email = email;
user.phone = phone;
user.status = UserStatus.ACTIVE;
user.registeredAt = LocalDateTime.now();
user.addDomainEvent(new UserRegisteredEvent(user.id, user.email));
return user;
}
public void deactivate(String reason) {
if (status != UserStatus.ACTIVE) throw new UserException("用户已非激活状态");
this.status = UserStatus.INACTIVE;
addDomainEvent(new UserDeactivatedEvent(this.id, reason));
}
public void changeEmail(Email newEmail) {
if (newEmail.equals(this.email)) return;
this.email = newEmail;
addDomainEvent(new UserEmailChangedEvent(this.id, newEmail));
}
public UserId getId() { return id; }
public Email getEmail() { return email; }
public UserStatus getStatus() { return status; }
}端口定义
// domain/port/inbound/RegisterUserUseCase.java
public interface RegisterUserUseCase {
UserRegisteredResult execute(RegisterUserCommand command);
}
// domain/port/inbound/GetUserUseCase.java
public interface GetUserUseCase {
UserDTO execute(GetUserQuery query);
}
// domain/port/outbound/UserRepository.java
public interface UserRepository {
Optional<User> findById(UserId id);
Optional<User> findByEmail(Email email);
void save(User user);
boolean existsByEmail(Email email);
}
// domain/port/outbound/VerificationCodePort.java
public interface VerificationCodePort {
void sendCode(Email email, String code);
boolean verify(Email email, String code);
}
// domain/port/outbound/NotificationPort.java
public interface NotificationPort {
void sendWelcomeEmail(Email email, String userName);
}应用服务
// application/service/RegisterUserService.java
@ApplicationService
public class RegisterUserService implements RegisterUserUseCase {
private final UserRepository userRepository;
private final VerificationCodePort verificationCode;
private final NotificationPort notification;
private final EventPublisher eventPublisher;
public RegisterUserService(UserRepository userRepository,
VerificationCodePort verificationCode,
NotificationPort notification,
EventPublisher eventPublisher) {
this.userRepository = userRepository;
this.verificationCode = verificationCode;
this.notification = notification;
this.eventPublisher = eventPublisher;
}
@Override
@Transactional
public UserRegisteredResult execute(RegisterUserCommand command) {
Email email = Email.of(command.getEmail());
// 1. 校验邮箱唯一性
if (userRepository.existsByEmail(email)) {
throw new EmailAlreadyExistsException(email);
}
// 2. 校验验证码
if (!verificationCode.verify(email, command.getCode())) {
throw new VerificationCodeException("验证码错误或已过期");
}
// 3. 创建用户聚合
User user = User.register(email, PhoneNumber.of(command.getPhone()));
// 4. 持久化
userRepository.save(user);
// 5. 发送欢迎通知
notification.sendWelcomeEmail(user.getEmail(), email.getValue());
// 6. 发布事件
eventPublisher.publishAll(user.getDomainEvents());
return UserRegisteredResult.from(user);
}
}主适配器
// adapter/inbound/web/UserController.java
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final RegisterUserUseCase registerUser;
private final GetUserUseCase getUser;
@PostMapping("/register")
public ResponseEntity<UserRegisteredResponse> register(
@RequestBody @Valid RegisterUserRequest request) {
var result = registerUser.execute(request.toCommand());
return ResponseEntity.status(201).body(UserRegisteredResponse.from(result));
}
@PostMapping("/send-code")
public ResponseEntity<Void> sendVerificationCode(@RequestBody @Valid SendCodeRequest request) {
verificationCodePort.sendCode(Email.of(request.getEmail()), generateCode());
return ResponseEntity.ok().build();
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> get(@PathVariable String id) {
var user = getUser.execute(new GetUserQuery(id));
if (user == null) return ResponseEntity.notFound().build();
return ResponseEntity.ok(user);
}
}次适配器
// adapter/outbound/persistence/JpaUserRepository.java
@Repository
public class JpaUserRepository implements UserRepository {
private final SpringDataUserRepository jpaRepo;
private final UserMapper mapper;
@Override
public Optional<User> findById(UserId id) {
return jpaRepo.findById(id.getValue()).map(mapper::toDomain);
}
@Override
public Optional<User> findByEmail(Email email) {
return jpaRepo.findByEmail(email.getValue()).map(mapper::toDomain);
}
@Override
public boolean existsByEmail(Email email) {
return jpaRepo.existsByEmail(email.getValue());
}
@Override
public void save(User user) {
jpaRepo.save(mapper.toPO(user));
}
}
// adapter/outbound/external/AliyunSmsVerificationCode.java
@Component
public class AliyunSmsVerificationCode implements VerificationCodePort {
private final RedisTemplate<String, String> redis;
@Override
public void sendCode(Email email, String code) {
redis.opsForValue().set("vc:" + email.getValue(), code, Duration.ofMinutes(5));
// 实际发送短信...
}
@Override
public boolean verify(Email email, String code) {
String stored = redis.opsForValue().get("vc:" + email.getValue());
return code.equals(stored);
}
}测试
// 应用层测试
@Test
void should_register_user_successfully() {
var userRepo = mock(UserRepository.class);
var vcPort = mock(VerificationCodePort.class);
var notif = mock(NotificationPort.class);
var eventPub = mock(EventPublisher.class);
var service = new RegisterUserService(userRepo, vcPort, notif, eventPub);
when(userRepo.existsByEmail(any())).thenReturn(false);
when(vcPort.verify(any(), any())).thenReturn(true);
var cmd = new RegisterUserCommand("test@example.com", "13800138000", "123456");
var result = service.execute(cmd);
assertNotNull(result.getUserId());
verify(userRepo).save(any(User.class));
verify(notif).sendWelcomeEmail(any(), any());
verify(eventPub).publishAll(anyList());
}数据流转
POST /api/v1/users/register
│ JSON → RegisterUserCommand
▼
RegisterUserService.execute() ← 应用服务
│ userRepository.existsByEmail() ← 出站端口
│ verificationCode.verify() ← 出站端口
│ User.register() ← 领域模型
│ userRepository.save() ← 出站端口
│ notification.sendWelcomeEmail()← 出站端口
▼
JpaUserRepository.save() ← 次适配器
AliyunSmsVerificationCode.verify() ← 次适配器
SendGridNotification.send() ← 次适配器Example: 多入口系统示例
本示例展示六边形架构的核心优势之一:同一业务逻辑通过多种入口(REST + CLI + MQ + gRPC)访问,Domain 层代码零修改。
场景:订单查询系统
系统提供订单查询功能,支持:
- REST API(前端调用)
- CLI 命令(运维人员)
- Kafka Consumer(内部服务)
- gRPC(微服务间调用)
领域模型(零修改 — 所有入口共享)
// domain/model/order/Order.java
public class Order extends AggregateRoot<OrderId> {
// ... 与示例 01 完全相同的 Order 聚合
// 不依赖任何 HTTP/CLI/MQ/gRPC 框架
}入站端口(零修改 — 所有入口共享)
// domain/port/inbound/GetOrderUseCase.java
public interface GetOrderUseCase {
OrderDTO execute(GetOrderQuery query);
}
// domain/port/inbound/CreateOrderUseCase.java
public interface CreateOrderUseCase {
OrderCreatedResult execute(CreateOrderCommand command);
}应用服务(零修改 — 所有入口共享)
// application/service/GetOrderService.java
@ApplicationService
public class GetOrderService implements GetOrderUseCase {
private final OrderRepository orderRepository;
private final OrderDTOAssembler assembler;
public GetOrderService(OrderRepository orderRepository, OrderDTOAssembler assembler) {
this.orderRepository = orderRepository;
this.assembler = assembler;
}
@Override
public OrderDTO execute(GetOrderQuery query) {
return orderRepository.findById(new OrderId(query.getOrderId()))
.map(assembler::toDTO)
.orElse(null);
}
}入口 1: REST Controller
// adapter/inbound/web/OrderController.java
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final GetOrderUseCase getOrder;
private final CreateOrderUseCase createOrder;
@GetMapping("/{id}")
public ResponseEntity<OrderDTO> get(@PathVariable String id) {
var order = getOrder.execute(new GetOrderQuery(id));
return order != null ? ResponseEntity.ok(order) : ResponseEntity.notFound().build();
}
@PostMapping
public ResponseEntity<CreateOrderResponse> create(@RequestBody @Valid CreateOrderRequest req) {
var result = createOrder.execute(req.toCommand());
return ResponseEntity.status(201).body(CreateOrderResponse.from(result));
}
}入口 2: CLI Command
// adapter/inbound/cli/OrderCliCommand.java
@Component
public class OrderCliCommand implements CommandLineRunner {
private final CreateOrderUseCase createOrder;
private final GetOrderUseCase getOrder;
public OrderCliCommand(CreateOrderUseCase createOrder, GetOrderUseCase getOrder) {
this.createOrder = createOrder;
this.getOrder = getOrder;
}
@Override
public void run(String... args) {
if (args.length == 0) return;
switch (args[0]) {
case "create-order" -> handleCreate(args);
case "get-order" -> handleGet(args);
}
}
private void handleCreate(String[] args) {
// Usage: java -jar app.jar create-order cust-001 prod-001 2
var cmd = new CreateOrderCommand(
CustomerId.of(args[1]),
List.of(new ItemCmd(ProductId.of(args[2]), Quantity.of(Integer.parseInt(args[3])))));
var result = createOrder.execute(cmd);
System.out.println("Order created: " + result.getOrderId());
}
private void handleGet(String[] args) {
// Usage: java -jar app.jar get-order ORD-001
var order = getOrder.execute(new GetOrderQuery(args[1]));
if (order != null) {
System.out.printf("Order %s: status=%s, amount=%s%n",
order.getOrderId(), order.getStatus(), order.getTotalAmount());
} else {
System.out.println("Order not found");
}
}
}入口 3: Kafka Consumer
// adapter/inbound/messaging/OrderCommandConsumer.java
@Component
public class OrderCommandConsumer {
private final CreateOrderUseCase createOrder;
private final ObjectMapper objectMapper;
public OrderCommandConsumer(CreateOrderUseCase createOrder, ObjectMapper objectMapper) {
this.createOrder = createOrder;
this.objectMapper = objectMapper;
}
@KafkaListener(topics = "order-commands", groupId = "order-service")
public void onMessage(ConsumerRecord<String, String> record) {
try {
var message = objectMapper.readValue(record.value(), OrderCommandMessage.class);
var cmd = message.toCommand();
createOrder.execute(cmd);
} catch (Exception e) {
throw new MessagingException("订单创建消息处理失败", e);
}
}
}入口 4: gRPC Service
// adapter/inbound/grpc/OrderGrpcService.java
import io.grpc.stub.StreamObserver;
public class OrderGrpcService extends OrderServiceGrpc.OrderServiceImplBase {
private final GetOrderUseCase getOrder;
private final CreateOrderUseCase createOrder;
public OrderGrpcService(GetOrderUseCase getOrder, CreateOrderUseCase createOrder) {
this.getOrder = getOrder;
this.createOrder = createOrder;
}
@Override
public void getOrder(GetOrderProtoRequest request,
StreamObserver<GetOrderProtoResponse> responseObserver) {
var order = getOrder.execute(new GetOrderQuery(request.getOrderId()));
if (order == null) {
responseObserver.onError(Status.NOT_FOUND.asRuntimeException());
return;
}
var response = GetOrderProtoResponse.newBuilder()
.setOrderId(order.getOrderId())
.setStatus(order.getStatus())
.setTotalAmount(order.getTotalAmount())
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
@Override
public void createOrder(CreateOrderProtoRequest request,
StreamObserver<CreateOrderProtoResponse> responseObserver) {
var cmd = CreateOrderCommand.fromProto(request);
var result = createOrder.execute(cmd);
var response = CreateOrderProtoResponse.newBuilder()
.setOrderId(result.getOrderId())
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}DI 配置 — 装配所有入口
// configuration/config/BeanConfig.java
@Configuration
public class BeanConfig {
// ---- 端口实现 ----
@Bean
public OrderRepository orderRepository(/* ... */) { return new PostgresOrderRepository(/* ... */); }
@Bean
public PaymentGateway paymentGateway(/* ... */) { return new StripePaymentGateway(/* ... */); }
@Bean
public EventPublisher eventPublisher(/* ... */) { return new RabbitMQEventPublisher(/* ... */); }
// ---- 应用服务 ----
@Bean
public GetOrderUseCase getOrderUseCase(OrderRepository repo) {
return new GetOrderService(repo, new OrderDTOAssembler());
}
@Bean
public CreateOrderUseCase createOrderUseCase(OrderRepository repo, ProductRepository productRepo,
EventPublisher eventPub) {
return new CreateOrderService(repo, productRepo, eventPub);
}
// ---- 所有入口共享同一 UseCase 实例 ----
// REST Controller: 注入 createOrderUseCase
// CLI Command: 注入 createOrderUseCase
// Kafka Consumer: 注入 createOrderUseCase
// gRPC Service: 注入 createOrderUseCase
}多入口测试
// 应用服务测试(一次编写,覆盖所有入口)
@Test
void get_order_should_return_dto() {
var orderRepo = mock(OrderRepository.class);
var assembler = new OrderDTOAssembler();
var service = new GetOrderService(orderRepo, assembler);
var orderId = OrderId.generate();
when(orderRepo.findById(orderId)).thenReturn(Optional.of(createTestOrder(orderId)));
var result = service.execute(new GetOrderQuery(orderId.getValue()));
assertNotNull(result);
assertEquals(orderId.getValue(), result.getOrderId());
}架构图
┌─────────────────────────────────────────────────┐
│ 入站适配器 (共享同一 UseCase) │
│ │
HTTP ──────────▶ │ OrderController (Spring MVC) │
CLI ──────────▶ │ OrderCliCommand (CommandLineRunner) │
Kafka ──────────▶ │ OrderCommandConsumer (KafkaListener) │──▶ CreateOrderUseCase (共享)
gRPC ──────────▶ │ OrderGrpcService (gRPC) │
└─────────────────────────────────────────────────┘
│
┌──────────────────▼──────────────────┐
│ 应用服务(仅一套) │
│ CreateOrderService │
│ GetOrderService (共享) │
└──────────────────┬──────────────────┘
│
┌─────────────────────────────────────────────┼──────────────────────────────┐
│ │ │
┌────▼─────────┐ ┌────────────▼──────────┐ ┌─────────────▼────┐
│ Postgres │ │ StripePayment │ │ RabbitMQ │
│ OrderRepo │ │ Gateway │ │ EventPublisher │
└──────────────┘ └───────────────────────┘ └───────────────────┘核心验证
验证方法: 停止数据库、关闭 Kafka、不启动 HTTP 服务,通过 CLI 入口测试领域逻辑:
```
java -jar app.jar create-order cust-001 prod-001 2
Order created: ORD-A1B2C3D4
```
如果不用数据库和 HTTP 就能跑通核心流程,六边形边界就对了。
Example: 适配器可替换性示例
本示例展示六边形架构的适配器可替换性。通过更换 DI 配置,可以在不修改 Domain/Application 代码的情况下切换数据库、支付网关和消息队列实现。
场景:数据库切换(Postgres → MongoDB)
Step 1: 定义端口(Domain 层 — 不需要修改)
// domain/port/outbound/OrderRepository.java
public interface OrderRepository {
Optional<Order> findById(OrderId id);
void save(Order order);
void delete(Order order);
}Step 2: 实现 Postgres 适配器
// adapter/outbound/persistence/PostgresOrderRepository.java
@Repository
public class PostgresOrderRepository implements OrderRepository {
private final JpaOrderRepository jpaRepo;
private final OrderMapper mapper;
@Override
public Optional<Order> findById(OrderId id) {
return jpaRepo.findById(id.getValue()).map(mapper::toDomain);
}
@Override
public void save(Order order) {
jpaRepo.save(mapper.toPO(order));
}
@Override
public void delete(Order order) {
jpaRepo.deleteById(order.getId().getValue());
}
}Step 3: 实现 MongoDB 适配器(新增 — 不需要修改现有关代码)
// adapter/outbound/persistence/MongoOrderRepository.java
@Repository
public class MongoOrderRepository implements OrderRepository {
private final MongoTemplate mongoTemplate;
private final MongoOrderConverter converter;
public MongoOrderRepository(MongoTemplate mongoTemplate, MongoOrderConverter converter) {
this.mongoTemplate = mongoTemplate;
this.converter = converter;
}
@Override
public Optional<Order> findById(OrderId id) {
var document = mongoTemplate.findById(id.getValue(), Document.class, "orders");
return Optional.ofNullable(document).map(converter::toDomain);
}
@Override
public void save(Order order) {
var document = converter.toDocument(order);
mongoTemplate.save(document, "orders");
}
@Override
public void delete(Order order) {
var query = new Query(Criteria.where("_id").is(order.getId().getValue()));
mongoTemplate.remove(query, "orders");
}
}Step 4: 切换 DI 配置(唯一需要修改的地方)
// 切换前: Postgres
@Configuration
public class DatabaseConfig {
@Bean
public OrderRepository orderRepository(JpaOrderRepository jpaRepo, OrderMapper mapper) {
return new PostgresOrderRepository(jpaRepo, mapper);
}
}
// 切换后: MongoDB
@Configuration
public class DatabaseConfig {
@Bean
public OrderRepository orderRepository(MongoTemplate mongoTemplate) {
return new MongoOrderRepository(mongoTemplate, new MongoOrderConverter());
}
}Step 5: 验证 — Domain 和 Application 代码零修改
// 无需修改!以下代码完全不受影响
@ApplicationService
public class CreateOrderService implements CreateOrderUseCase {
// 只依赖 OrderRepository 接口,不管实现是 Postgres 还是 MongoDB
private final OrderRepository orderRepository;
// ...
}场景:支付网关切换(Stripe → PayPal)
端口定义(Domain — 不需要修改)
// domain/port/outbound/PaymentGateway.java
public interface PaymentGateway {
PaymentResult charge(Money amount, PaymentMethod method);
RefundResult refund(PaymentId paymentId, Money amount);
}Stripe 实现
// adapter/outbound/external/StripePaymentGateway.java
@Component
public class StripePaymentGateway implements PaymentGateway {
private final StripeClient stripeClient;
@Override
public PaymentResult charge(Money amount, PaymentMethod method) {
try {
var intent = stripeClient.paymentIntents().create(/* ... */);
return PaymentResult.success(PaymentId.from(intent.getId()));
} catch (StripeException e) {
throw new InfrastructureException("Stripe payment failed", e);
}
}
}PayPal 实现(新增)
// adapter/outbound/external/PayPalPaymentGateway.java
@Component
public class PayPalPaymentGateway implements PaymentGateway {
private final PayPalHttpClient payPalClient;
@Override
public PaymentResult charge(Money amount, PaymentMethod method) {
try {
var order = new OrderRequest();
order.checkoutPaymentIntent("CAPTURE");
order.purchaseUnits(List.of(new PurchaseUnitRequest(
new MoneyRequest(amount.getCurrencyCode(), amount.toCents()))));
var response = payPalClient.execute(new OrdersCreateRequest().requestBody(order));
return PaymentResult.success(PaymentId.from(response.result().id()));
} catch (IOException e) {
throw new InfrastructureException("PayPal payment failed", e);
}
}
}场景:消息队列切换(RabbitMQ → Kafka)
端口定义(Domain — 不需要修改)
// domain/port/outbound/EventPublisher.java
public interface EventPublisher {
void publish(DomainEvent event);
void publishAll(List<DomainEvent> events);
}Kafka 实现
// adapter/outbound/messaging/KafkaEventPublisher.java
@Component
public class KafkaEventPublisher implements EventPublisher {
private final KafkaTemplate<String, String> kafkaTemplate;
private final ObjectMapper objectMapper;
@Override
public void publish(DomainEvent event) {
try {
var payload = objectMapper.writeValueAsString(event.toPayload());
kafkaTemplate.send("domain-events", event.getEventType(), payload);
} catch (JsonProcessingException e) {
throw new InfrastructureException("Event serialization failed", e);
}
}
@Override
public void publishAll(List<DomainEvent> events) {
events.forEach(this::publish);
}
}环境配置(Profile 切换)
# application-dev.yml — 开发环境用内存实现
adapter:
persistence: inmemory
payment: fake
messaging: inmemory
# application-staging.yml — 预发布环境用真实但独立服务
adapter:
persistence: postgres
payment: stripe-test
messaging: rabbitmq
# application-prod.yml — 生产环境
adapter:
persistence: postgres
payment: stripe
messaging: kafka// 通过 Profile 切换配置
@Configuration
@Profile("dev")
public class DevAdapterConfig {
@Bean
public OrderRepository orderRepository() {
return new InMemoryOrderRepository();
}
@Bean
public PaymentGateway paymentGateway() {
return new FakePaymentGateway();
}
}适配器可替换性清单
| 适配器类型 | 可替换实现 | 切换方式 |
|---|---|---|
| OrderRepository | Postgres / MySQL / MongoDB / InMemory | DI Config |
| PaymentGateway | Stripe / PayPal / WeChatPay / Fake | DI Config |
| EventPublisher | RabbitMQ / Kafka / AWS SQS / InMemory | DI Config |
| NotificationPort | SendGrid / AliyunSMS / AWS SES / LogOnly | DI Config |
| VerificationCode | Redis / DB / Mock | DI Config |
验证:更改适配器后零回归
// 这些 Domain 层测试不受适配器变更影响
@Test
void order_state_transitions() {
var order = Order.create(CustomerId.of("cust-001"));
order.pay(PaymentId.of("PAY-001"));
assertEquals(OrderStatus.PAID, order.getStatus());
}
// 这些应用层测试不受适配器变更影响(Mock 端口)
@Test
void create_order_via_usecase() {
var orderRepo = mock(OrderRepository.class);
var service = new CreateOrderService(orderRepo, /* ... */);
// ...
}
// 只有适配器层的集成测试需要修改
// @Test
// void postgres_repository_should_save() { ... }
// → 新增:
// @Test
// void mongo_repository_should_save() { ... }Example: 主适配器端口可替换性与测试
本示例展示六边形架构中主适配器(Primary Adapter)的端口可替换性。同一个 UseCase 接口可以被 REST、gRPC、CLI 三种入口调用,无需修改 Domain 和 Application 层代码。
场景:同一 UseCase 三种入口
端口定义(Domain 层 — 唯一不变的核心)
// domain/port/inbound/CreateOrderUseCase.java
public interface CreateOrderUseCase {
OrderCreatedResult execute(CreateOrderCommand command);
}
// domain/port/inbound/CreateOrderCommand.java
public record CreateOrderCommand(
CustomerId customerId,
List<OrderLineItem> items,
Address shippingAddress
) {}
// domain/port/inbound/OrderCreatedResult.java
public record OrderCreatedResult(
OrderId orderId,
OrderStatus status,
Money totalAmount,
Instant createdAt
) {}应用服务实现(Application 层 — 不依赖具体入口类型)
// application/service/CreateOrderServiceImpl.java
@ApplicationService
public class CreateOrderServiceImpl implements CreateOrderUseCase {
private final OrderRepository orderRepository;
private final EventPublisher eventPublisher;
public CreateOrderServiceImpl(OrderRepository orderRepository, EventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.eventPublisher = eventPublisher;
}
@Override
public OrderCreatedResult execute(CreateOrderCommand command) {
var order = Order.create(command.customerId());
command.items().forEach(order::addItem);
order.assignShippingAddress(command.shippingAddress());
orderRepository.save(order);
eventPublisher.publish(new OrderCreatedEvent(order));
return OrderCreatedResult.from(order);
}
}入口 1:REST Controller
// adapter/inbound/rest/OrderController.java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final CreateOrderUseCase createOrderUseCase;
public OrderController(CreateOrderUseCase createOrderUseCase) {
this.createOrderUseCase = createOrderUseCase;
}
@PostMapping
public ResponseEntity<CreateOrderResponse> create(@RequestBody @Valid CreateOrderRequest request) {
var command = request.toCommand();
var result = createOrderUseCase.execute(command);
return ResponseEntity.status(201).body(CreateOrderResponse.from(result));
}
}入口 2:gRPC Service
// adapter/inbound/grpc/OrderGrpcService.java
@GrpcService
public class OrderGrpcService extends OrderServiceGrpc.OrderServiceImplBase {
private final CreateOrderUseCase createOrderUseCase;
private final GrpcMapper grpcMapper;
public OrderGrpcService(CreateOrderUseCase createOrderUseCase, GrpcMapper grpcMapper) {
this.createOrderUseCase = createOrderUseCase;
this.grpcMapper = grpcMapper;
}
@Override
public void createOrder(CreateOrderProto request, StreamObserver<CreateOrderResponseProto> observer) {
var command = grpcMapper.toCommand(request);
var result = createOrderUseCase.execute(command);
observer.onNext(grpcMapper.toProto(result));
observer.onCompleted();
}
}入口 3:CLI Command
// adapter/inbound/cli/CreateOrderCommand.java
@Component
public class CreateOrderCliCommand implements CommandLineRunner {
private final CreateOrderUseCase createOrderUseCase;
public CreateOrderCliCommand(CreateOrderUseCase createOrderUseCase) {
this.createOrderUseCase = createOrderUseCase;
}
@Override
public void run(String... args) {
if (args.length < 1 || !"--create-order".equals(args[0])) return;
var command = parseArgs(args);
var result = createOrderUseCase.execute(command);
System.out.printf("订单已创建: %s (金额: %s)%n", result.orderId(), result.totalAmount());
}
}测试:端口级别隔离验证
核心验证目标:Domain 和 Application 层不需要为每种入口编写测试,因为入口切换不影响业务逻辑。
1. 领域层测试(零入口依赖)
// domain/src/test/java/.../OrderTest.java
class OrderTest {
@Test
void should_create_order_with_initial_status() {
var order = Order.create(CustomerId.of("CUST-001"));
assertEquals(OrderStatus.CREATED, order.getStatus());
assertTrue(order.getItems().isEmpty());
}
@Test
void should_add_item_to_order() {
var order = Order.create(CustomerId.of("CUST-001"));
order.addItem(new OrderLineItem(ProductId.of("PROD-001"), 2, Money.of(99.99)));
assertEquals(1, order.getItems().size());
}
}2. 应用层测试(Mock 端口 — 不依赖入口类型)
// application/src/test/java/.../CreateOrderServiceImplTest.java
class CreateOrderServiceImplTest {
@Mock OrderRepository orderRepository;
@Mock EventPublisher eventPublisher;
private CreateOrderServiceImpl service;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
service = new CreateOrderServiceImpl(orderRepository, eventPublisher);
}
@Test
void should_execute_order_creation() {
var command = new CreateOrderCommand(
CustomerId.of("CUST-001"),
List.of(new OrderLineItem(ProductId.of("PROD-001"), 1, Money.of(99.99))),
new Address("北京", "朝阳区", "100000")
);
var result = service.execute(command);
assertNotNull(result.orderId());
verify(orderRepository).save(any(Order.class));
verify(eventPublisher).publish(any(OrderCreatedEvent.class));
}
}3. 主适配器测试(仅测协议转换 — 用 Mock 隔离业务逻辑)
// adapter/src/test/java/.../rest/OrderControllerTest.java
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@MockBean CreateOrderUseCase createOrderUseCase;
@Autowired MockMvc mockMvc;
@Test
void should_convert_http_request_to_command() throws Exception {
var result = new OrderCreatedResult(
OrderId.of("ORD-001"), OrderStatus.CREATED, Money.of(99.99), Instant.now());
when(createOrderUseCase.execute(any())).thenReturn(result);
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"customerId":"CUST-001","items":[{"productId":"PROD-001","quantity":1,"price":99.99}]}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.orderId").value("ORD-001"));
}
}// adapter/src/test/java/.../grpc/OrderGrpcServiceTest.java
@ExtendWith(MockitoExtension.class)
class OrderGrpcServiceTest {
@Mock CreateOrderUseCase createOrderUseCase;
@Mock GrpcMapper grpcMapper;
private OrderGrpcService service;
@Test
void should_convert_grpc_request_to_command() {
service = new OrderGrpcService(createOrderUseCase, grpcMapper);
var protoRequest = CreateOrderProto.newBuilder()
.setCustomerId("CUST-001").build();
var command = new CreateOrderCommand(
CustomerId.of("CUST-001"), List.of(), null);
when(grpcMapper.toCommand(protoRequest)).thenReturn(command);
when(createOrderUseCase.execute(command)).thenReturn(mockResult());
var observer = new TestStreamObserver<CreateOrderResponseProto>();
service.createOrder(protoRequest, observer);
assertNotNull(observer.getResponse());
}
}4. 适配器可替换性测试(同一测试用例验证所有入口)
// adapter/src/test/java/.../PrimaryAdapterSwapTest.java
class PrimaryAdapterSwapTest {
@Mock CreateOrderUseCase useCase;
private OrderController restAdapter;
private OrderGrpcService grpcAdapter;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
restAdapter = new OrderController(useCase);
var grpcMapper = new GrpcMapper();
grpcAdapter = new OrderGrpcService(useCase, grpcMapper);
}
@ParameterizedTest
@MethodSource("allPrimaryAdapters")
void all_adapters_produce_same_order_result(String label, CreateOrderCommand command) {
var expectedResult = new OrderCreatedResult(
OrderId.of("ORD-001"), OrderStatus.CREATED, Money.of(99.99), Instant.now());
when(useCase.execute(command)).thenReturn(expectedResult);
// 验证所有入口都返回一致的 UseCase 执行结果
// REST、gRPC、CLI 只是协议转换层,业务结果完全一致
var restResponse = restAdapter.create(new CreateOrderRequest(command));
assertEquals(201, restResponse.getStatusCodeValue());
// 验证 UseCase 被每个入口正确调用
verify(useCase, times(1)).execute(command);
}
}验证清单:端口可替换性确认
| 检查项 | REST | gRPC | CLI | 说明 |
|---|---|---|---|---|
| UseCase 接口零修改 | ✅ | ✅ | ✅ | 所有入口调用同一个 CreateOrderUseCase |
| 业务逻辑零修改 | ✅ | ✅ | ✅ | CreateOrderServiceImpl 不变 |
| 新增入口无需改现有代码 | ✅ | ✅ | ✅ | 各自实现协议转换即可 |
| 领域层测试零修改 | ✅ | ✅ | ✅ | OrderTest 不受入口影响 |
| 应用层测试零修改 | ✅ | ✅ | ✅ | CreateOrderServiceImplTest 用 Mock |
| 适配器测试各自独立 | ✅ | ✅ | ✅ | 只测协议映射不测业务逻辑 |
核心结论
主适配器可替换性证明了六边形边界正确: 所有入口只做协议转换,业务逻辑完全隔离在 Domain 和 Application 层。新增 gRPC 或 CLI 入口时,Domain 和 Application 代码零修改——这是六边形架构的对称性验证。
Example: 单体简单六边形架构
单一 SpringBoot 模块内的六边形包结构,适用于小型项目快速落地。
场景
5 人以下团队、单数据库、单一 REST API 入口的系统,无需 Maven 多模块即可获得端口/适配器隔离的好处。
目录树 & 包结构
order-service/
└── src/main/java/com/example/order/
├── OrderApplication.java # SpringBoot 入口
├── web/ # 主适配器(Primary Adapters)
│ └── OrderController.java # POST /orders, GET /orders/{id}
├── application/ # 应用层
│ ├── service/
│ │ ├── CreateOrderService.java # implements CreateOrderUseCase
│ │ └── GetOrderService.java # implements GetOrderUseCase
│ └── dto/
│ ├── CreateOrderRequest.java # 入站 DTO
│ └── OrderResponse.java # 出站 DTO
├── domain/ # 领域核心(零框架依赖)
│ ├── model/
│ │ ├── Order.java # 聚合根
│ │ ├── OrderId.java # 值对象
│ │ ├── OrderItem.java # 实体
│ │ └── OrderStatus.java # 枚举
│ ├── port/ # ★ 端口定义
│ │ ├── inbound/
│ │ │ ├── CreateOrderUseCase.java # 入站端口
│ │ │ └── GetOrderUseCase.java # 入站端口
│ │ └── outbound/
│ │ ├── OrderRepository.java # 出站端口(持久化)
│ │ └── EventPublisher.java # 出站端口(消息)
│ └── event/
│ └── OrderCreatedEvent.java # 领域事件
└── infrastructure/ # 次适配器(Secondary Adapters)
├── persistence/
│ ├── JpaOrderRepository.java # implements OrderRepository
│ ├── OrderJpaEntity.java # JPA Entity
│ └── OrderMapper.java # Domain ⇄ PO 映射
├── messaging/
│ └── KafkaEventPublisher.java # implements EventPublisher
└── config/
└── BeanConfig.java # DI 装配依赖关系
web (Controller) → application (Service, DTO) → domain (Model, Port)
infrastructure (Adapter) → domain (Model, Port) → 外部(DB, MQ)┌── web/ ──────────────────────────┐
│ OrderController │ 主适配器:协议转换
└──────────────┬───────────────────┘
│ 依赖
▼
┌── application/ ──────────────────┐
│ CreateOrderService │ 应用服务:用例编排
│ (implements CreateOrderUseCase) │
└──────────┬───────────────────────┘
│ 依赖(注入端口接口)
▼
┌── domain/ ───────────────────────┐
│ port/inbound/CreateOrderUseCase │ ★ 入站端口(接口)
│ port/outbound/OrderRepository │ ★ 出站端口(接口)
│ model/Order (Aggregate Root) │ 领域逻辑
└──────────────────────────────────┘
▲
│ 实现
┌── infrastructure/ ───────────────┐
│ JpaOrderRepository │ 次适配器:JPA 实现
│ KafkaEventPublisher │ 次适配器:Kafka 实现
└──────────────────────────────────┘适用场景
| 维度 | 值 | 说明 |
|---|---|---|
| 团队规模 | 1-5 人 | 小团队无需多模块 |
| 入口数量 | 1 个 (REST) | 单入口无需多主适配器 |
| 外部依赖 | 1 DB + 1 MQ | 简单基础设施 |
| 领域复杂度 | 低 | 1-2 个聚合根 |
| 项目生命周期 | MVP → 早期产品 | 后续可演进到多模块 |
关键设计决策
1. 单模块内包隔离 — 用 package 可见性替代 Maven module 隔离,简化构建 2. 端口仍在 domain 包内 — 保持端口定义的领域纯粹性 3. infrastructure 包实现所有出站端口 — 技术实现集中管理 4. Controller 直接依赖 UseCase 接口 — 不引入应用层中间件
演进路径
单模块六边形 (本示例)
│ 团队增长 / 入口增加
▼
Maven 多模块六边形 (08-monolith-multi-module)
│ 服务拆分
▼
微服务六边形 (09-12)Example: 单体复杂六边形架构
多端口 + 多适配器 + 多聚合根的单体应用,适用于中大型单体系统的内部模块化。
场景
10-20 人团队、多入口(REST + gRPC + CLI)、多外部依赖(Postgres + Redis + RabbitMQ + Stripe),领域包含多个聚合根和跨聚合的领域服务。
目录树 & 包结构
order-management/
└── src/main/java/com/example/ordermgmt/
├── OrderMgmtApplication.java
│
├── domain/ # 领域核心
│ ├── model/
│ │ ├── order/ # 聚合 1
│ │ │ ├── Order.java # 聚合根
│ │ │ ├── OrderId.java # 值对象
│ │ │ ├── OrderItem.java # 实体
│ │ │ ├── OrderStatus.java # 枚举
│ │ │ └── OrderPaidEvent.java # 领域事件
│ │ ├── payment/ # 聚合 2
│ │ │ ├── Payment.java # 聚合根
│ │ │ ├── PaymentId.java # 值对象
│ │ │ ├── PaymentMethod.java # 值对象
│ │ │ └── PaymentCompletedEvent.java # 领域事件
│ │ ├── inventory/ # 聚合 3
│ │ │ ├── Inventory.java # 聚合根
│ │ │ ├── StockId.java # 值对象
│ │ │ └── StockReservedEvent.java # 领域事件
│ │ └── shared/ # 共享值对象
│ │ ├── Money.java
│ │ ├── CustomerId.java
│ │ └── Address.java
│ ├── service/ # 领域服务(跨聚合)
│ │ ├── OrderPaymentSaga.java # 订单-支付协调
│ │ └── InventoryReservationService.java # 库存预留
│ ├── port/
│ │ ├── inbound/ # 入站端口
│ │ │ ├── CreateOrderUseCase.java
│ │ │ ├── PayOrderUseCase.java
│ │ │ ├── QueryOrderUseCase.java
│ │ │ ├── InventoryQueryUseCase.java
│ │ │ └── GenerateReportUseCase.java
│ │ └── outbound/ # 出站端口
│ │ ├── OrderRepository.java # 聚合 1 持久化
│ │ ├── PaymentRepository.java # 聚合 2 持久化
│ │ ├── InventoryRepository.java # 聚合 3 持久化
│ │ ├── PaymentGateway.java # 外部支付
│ │ ├── EventPublisher.java # 消息发布
│ │ ├── CacheService.java # 缓存抽象
│ │ └── NotificationService.java # 通知抽象
│ └── specification/
│ ├── OrderSpecification.java # 规格模式
│ └── PaymentSpecification.java
│
├── application/ # 应用层
│ ├── service/
│ │ ├── CreateOrderServiceImpl.java
│ │ ├── PayOrderServiceImpl.java
│ │ ├── InventoryQueryServiceImpl.java
│ │ └── ReportGenerationServiceImpl.java
│ └── dto/
│ ├── OrderDTO.java
│ └── PaymentDTO.java
│
├── adapter/ # 适配器层
│ ├── inbound/
│ │ ├── rest/
│ │ │ ├── OrderController.java # REST 入口
│ │ │ ├── PaymentController.java
│ │ │ └── ReportController.java
│ │ ├── grpc/
│ │ │ ├── OrderGrpcService.java # gRPC 入口
│ │ │ └── PaymentGrpcService.java
│ │ └── cli/
│ │ └── ReportCliCommand.java # CLI 入口
│ └── outbound/
│ ├── persistence/
│ │ ├── PostgresOrderRepository.java
│ │ ├── PostgresPaymentRepository.java
│ │ ├── PostgresInventoryRepository.java
│ │ └── mapper/
│ │ ├── OrderMapper.java
│ │ ├── PaymentMapper.java
│ │ └── InventoryMapper.java
│ ├── external/
│ │ ├── StripePaymentGateway.java # implements PaymentGateway
│ │ └── TwilioNotificationService.java # implements NotificationService
│ ├── messaging/
│ │ └── RabbitMQEventPublisher.java # implements EventPublisher
│ └── cache/
│ └── RedisCacheService.java # implements CacheService
│
└── configuration/ # 配置层
├── AdapterConfig.java # 次适配器装配
├── UseCaseConfig.java # 应用服务装配
└── CacheConfig.java # 缓存配置依赖关系
adapter/inbound (REST/gRPC/CLI)
│ 依赖 UseCase 接口
▼
application/service (UseCase 实现)
│ 依赖出站端口接口
▼
domain/port/outbound (接口)
│ 实现
▼
adapter/outbound/persistence+external+messaging+cache (适配器实现)┌─────────────────────────────────────────────────────────────┐
│ adapter/inbound/ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ REST (Order) │ │ gRPC (Order) │ │ CLI (Report) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────┬───────────┘ │
│ │ │ │ │
│ domain/port/inbound/ │ │ │
│ ┌──────────────┐ ┌──────┴───────┐ ┌──────────┴───────────┐ │
│ │ CreateOrder │ │ PayOrder │ │ GenerateReport │ │
│ │ UseCase │ │ UseCase │ │ UseCase │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────┬───────────┘ │
│ │ │ │ │
│ domain/model/ domain/service/ │
│ ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ Order │ │ Payment │ │ Saga │ │ InventoryReserve │ │
│ │ (AR) │ │ (AR) │ │ Service │ │ Service │ │
│ └────┬────┘ └────┬────┘ └──────────┘ └───────────────────┘ │
│ │ │ │
│ domain/port/outbound/ │
│ ┌───────────┐ ┌───────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ OrderRepo │ │ PaymentGwy│ │ EventPub │ │ CacheSvc │ │
│ └─────┬─────┘ └─────┬─────┘ └────┬─────┘ └──────┬───────┘ │
│ │ │ │ │ │
│ adapter/outbound/ │ │ │ │
│ ┌─────────────┐ ┌───┴────────┐ ┌─┴──────────┐ ┌──┴───────┐ │
│ │ Postgres │ │ Stripe │ │ RabbitMQ │ │ Redis │ │
│ │ (3 repos) │ │ Gateway │ │ Publisher │ │ Cache │ │
│ └─────────────┘ └────────────┘ └────────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘适用场景
| 维度 | 值 | 说明 |
|---|---|---|
| 团队规模 | 10-20 人 | 需要内部包级别的模块化 |
| 入口数量 | 3+ (REST/gRPC/CLI) | 多入口共享同一套业务逻辑 |
| 外部依赖 | 5+ | 支付网关、消息队列、缓存、通知 |
| 领域复杂度 | 中-高 | 3+ 聚合根、跨聚合领域服务、Saga |
| 项目生命周期 | 成长期-成熟期 | 业务复杂但暂未拆分微服务 |
关键设计决策
1. 多聚合根隔离 — 每个聚合有独立的 model 子包,聚合间通过领域事件松耦合 2. 领域服务处理跨聚合逻辑 — Saga 模式协调 Order ↔ Payment 一致性 3. 适配器按传递方式组织 — inbound 按协议(rest/grpc/cli),outbound 按技术(persistence/external/messaging/cache) 4. 共享值对象放在 shared 包 — Money/CustomerId 被多个聚合复用
演进路径
单体复杂六边形 (本示例)
│ 隔离有界上下文
▼
Maven 多模块 (08-monolith-multi-module)
│ 独立部署需求
▼
微服务六边形 (09-12)Example: 单体多模块六边形架构
Maven 多模块的六边形架构,通过编译期依赖约束强制边界,适用于中大型团队。
场景
10-30 人团队、多入口(REST + gRPC + Kafka Consumer)、多外部依赖,需要通过 Maven 模块边界强制依赖规则,防止 Adapter 代码泄漏到 Domain。
Maven 模块结构
order-system/
├── pom.xml # 父 POM(版本管理)
├── order-domain/ # domain — 零框架依赖
│ ├── pom.xml # 仅依赖 Java stdlib + ArchUnit
│ └── src/main/java/com/example/order/domain/
│ ├── model/
│ │ ├── order/
│ │ │ ├── Order.java # 聚合根
│ │ │ ├── OrderId.java # 值对象
│ │ │ ├── OrderItem.java # 实体
│ │ │ └── OrderStatus.java # 枚举
│ │ └── payment/
│ │ ├── Payment.java # 聚合根
│ │ ├── PaymentId.java # 值对象
│ │ └── PaymentMethod.java # 值对象
│ ├── service/
│ │ └── OrderPaymentSaga.java # 领域服务
│ ├── port/
│ │ ├── inbound/
│ │ │ ├── CreateOrderUseCase.java
│ │ │ ├── PayOrderUseCase.java
│ │ │ └── QueryOrderUseCase.java
│ │ └── outbound/
│ │ ├── OrderRepository.java
│ │ ├── PaymentRepository.java
│ │ ├── PaymentGateway.java
│ │ └── EventPublisher.java
│ └── event/
│ ├── OrderCreatedEvent.java
│ └── PaymentCompletedEvent.java
│
├── order-application/ # application — 仅依赖 domain
│ ├── pom.xml # 依赖 order-domain
│ └── src/main/java/com/example/order/application/
│ └── service/
│ ├── CreateOrderServiceImpl.java # implements CreateOrderUseCase
│ ├── PayOrderServiceImpl.java # implements PayOrderUseCase
│ └── QueryOrderServiceImpl.java # implements QueryOrderUseCase
│
├── order-adapter-inbound/ # 主适配器 — 依赖 application
│ ├── pom.xml # 依赖 order-application + Spring Web/gRPC
│ └── src/main/java/com/example/order/adapter/inbound/
│ ├── rest/
│ │ └── OrderController.java
│ ├── grpc/
│ │ └── OrderGrpcService.java
│ └── kafka/
│ │ └── OrderKafkaConsumer.java # Kafka 驱动入口
│ └── dto/
│ ├── CreateOrderRequest.java
│ └── OrderResponse.java
│
├── order-adapter-outbound/ # 次适配器 — 依赖 domain
│ ├── pom.xml # 依赖 order-domain + Spring Data JPA/Kafka/Stripe
│ └── src/main/java/com/example/order/adapter/outbound/
│ ├── persistence/
│ │ ├── PostgresOrderRepository.java
│ │ ├── PostgresPaymentRepository.java
│ │ ├── entity/
│ │ │ ├── OrderJpaEntity.java
│ │ │ └── PaymentJpaEntity.java
│ │ └── mapper/
│ │ ├── OrderMapper.java
│ │ └── PaymentMapper.java
│ ├── external/
│ │ └── StripePaymentGateway.java
│ └── messaging/
│ └── KafkaEventPublisher.java
│
└── order-app/ # 启动器 — 依赖所有模块
├── pom.xml # 依赖全部子模块 + Spring Boot
└── src/main/java/com/example/order/
├── OrderApplication.java # @SpringBootApplication
└── configuration/
└── AdapterConfig.java # DI 装配模块依赖图
┌─────────────────────────────────────────────────────┐
│ │
│ order-app (启动器) │
│ ↓ ↓ ↓ ↓ │
│ domain application adapter-in adapter-out │
│ │
└─────────────────────────────────────────────────────┘
order-domain ← 零框架依赖(只依赖 Java stdlib)
↑
│ 编译期依赖
order-application ← 只依赖 order-domain
↑ ↑
│ │
order-adapter-inbound │ ← 依赖 order-application + Spring MVC/gRPC
│ │
└── order-adapter-outbound ← 依赖 order-domain + Spring Data/Kafka/StripeMaven 依赖关系(pom.xml 约束):
order-domain: 无内部依赖
order-application: order-domain
order-adapter-inbound: order-application + Spring Web + Spring gRPC
order-adapter-outbound: order-domain + Spring Data JPA + Kafka Client + Stripe SDK
order-app: order-adapter-inbound + order-adapter-outbound + Spring Boot编译期边界验证
// order-domain/pom.xml — 零框架依赖宣言
<dependencies>
<!-- 仅 Java 标准库 + 测试框架 -->
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>// ArchUnit 测试 — 验证 domain 模块纯净度
@AnalyzeClasses(packages = "com.example.order.domain")
class DomainPurityTest {
@ArchTest
static final ArchRule no_framework_dependency = noClasses()
.should().dependOnClassesThat()
.resideInAnyPackage(
"org.springframework..",
"jakarta.persistence..",
"com.fasterxml.."
);
}适用场景
| 维度 | 值 | 说明 |
|---|---|---|
| 团队规模 | 10-30 人 | 需要编译期依赖约束 |
| 入口数量 | 3+ (REST/gRPC/Kafka) | 多入口分别独立模块 |
| 外部依赖 | 5+ | 次适配器独立模块便于替换 |
| 领域复杂度 | 中-高 | 多聚合,通过模块边界强制隔离 |
| 项目生命周期 | 成熟期 | 架构已稳定,需要强制而非建议的边界 |
关键设计决策
1. domain 模块零框架依赖 — 通过 pom.xml 不引入 Spring/JPA,辅以 ArchUnit 测试验证 2. application 只依赖 domain — 不依赖任何框架和适配器模块 3. adapter-inbound 和 adapter-outbound 独立 — 主/次适配器各自独立模块,可独立演进 4. order-app 作为组装层 — 唯一包含 @SpringBootApplication 和 DI 配置的模块
对比单模块
| 维度 | 单模块六边形 (06) | 多模块六边形 (08) |
|---|---|---|
| 构建复杂度 | 低 | 中(需管理 5 个 pom.xml) |
| 边界保护 | 约定(包名) | 强制(编译期) |
| 团队并行开发 | 易冲突 | 模块级独立开发 |
| 新人学习成本 | 低 | 中(需理解模块结构) |
| 适合团队 | 1-5 人 | 10-30 人 |
Example: 微服务简单六边形架构
单微服务内的六边形架构,每个服务独立部署,是六边形 + 微服务的最小组合。
场景
微服务架构下,单个服务(如 order-service)内部采用六边形架构。服务间通过 API Gateway 通信,服务内部端口适配器隔离。
目录树 & 包结构
order-service/ # 独立部署单元
├── pom.xml
├── Dockerfile
├── src/main/java/com/example/order/
│ ├── OrderServiceApplication.java # SpringBoot 入口
│ ├── domain/ # 领域核心
│ │ ├── model/
│ │ │ ├── Order.java # 聚合根
│ │ │ ├── OrderId.java # 值对象
│ │ │ └── OrderStatus.java # 枚举
│ │ ├── port/
│ │ │ ├── inbound/
│ │ │ │ ├── CreateOrderUseCase.java
│ │ │ │ └── GetOrderUseCase.java
│ │ │ └── outbound/
│ │ │ ├── OrderRepository.java
│ │ │ └── NotificationPort.java # 通知其他服务
│ │ └── event/
│ │ └── OrderCreatedEvent.java
│ ├── application/
│ │ └── service/
│ │ ├── CreateOrderServiceImpl.java
│ │ └── GetOrderServiceImpl.java
│ ├── adapter/
│ │ ├── inbound/
│ │ │ └── rest/
│ │ │ └── OrderController.java # REST API
│ │ └── outbound/
│ │ ├── persistence/
│ │ │ ├── PostgresOrderRepository.java
│ │ │ └── OrderJpaEntity.java
│ │ └── notification/
│ │ └── KafkaNotificationAdapter.java # 发事件给其他服务
│ └── configuration/
│ └── ServiceConfig.java
│
├── src/main/resources/
│ ├── application.yml # 服务配置
│ └── db/migration/
│ └── V1__create_order_table.sql
│
└── src/test/java/com/example/order/ # 分层测试
├── domain/
│ └── OrderTest.java # 领域层单元测试(零 Mock)
├── application/
│ └── CreateOrderServiceImplTest.java # 应用层测试(Mock 端口)
└── adapter/
└── rest/
└── OrderControllerTest.java # 适配器集成测试微服务全景图
┌─────────────┐
│ API Gateway │
└──────┬──────┘
┌───────────────┼───────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│order-service│ │user-service │ │payment-svc │
│ (本示例) │ │ (六边形) │ │ (六边形) │
│ │ │ │ │ │
│ domain ──── │ │ domain ──── │ │ domain ──── │ │
│ port ◄────┼─┤ port ◄────┼─┤ port ◄─── │ │
│ app ◄───────│ │ app ◄───────│ │ app ◄───────│ │
│ adapter ──► │ │ adapter ──► │ │ adapter ──► │ │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Postgres │ │ Postgres │ │ Postgres │
│ (独立DB) │ │ (独立DB) │ │ (独立DB) │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐
│ Kafka │
└─────────────┘依赖关系
服务外部
┌───────────────────────────────────────────────────┐
│ │
│ API Gateway ──► REST Controller │
│ │
│ ┌───────────── adapter/inbound/ ───────────────┐ │
│ │ OrderController │ │
│ │ │ 依赖 UseCase 接口 │ │
│ │ ▼ │ │
│ │ CreateOrderUseCase (domain/port/inbound/) │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌───────────── application/ ───────────────────┐ │
│ │ CreateOrderServiceImpl │ │
│ │ │ 依赖出站端口 │ │
│ │ ▼ │ │
│ │ OrderRepository (domain/port/outbound/) │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌───────────── adapter/outbound/ ──────────────┐ │
│ │ PostgresOrderRepository → PostgreSQL │ │
│ │ KafkaNotificationAdapter → Kafka │ │
│ └──────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────┘适用场景
| 维度 | 值 | 说明 |
|---|---|---|
| 团队规模 | 3-8 人/服务 | 小团队负责单服务 |
| 部署单元 | 独立容器/Pod | Kubernetes 部署 |
| 数据库 | 服务自有(Database per Service) | 不跨服务共享 DB |
| 服务间通信 | 同步(REST/gRPC)+ 异步(Kafka) | 通过端口抽象远程调用 |
| 领域复杂度 | 低-中 | 单服务 1-2 个聚合根 |
关键设计决策
1. 服务间调用通过端口抽象 — 对其他服务的依赖定义为出站端口(NotificationPort),而非直接 HTTP 调用 2. 数据库隔离 — 每个服务独享数据库,通过事件实现最终一致性 3. API Gateway 作为统一入口 — 主适配器只暴露 REST,Gateway 负责路由/限流/认证 4. 轻量级六边形 — 单服务内结构简单,避免过度抽象
与其他示例的关系
09-microservice-simple (本示例)
│ 服务内部复杂度增加
▼
10-microservice-complex (多聚合 + Saga)
│ 团队规模扩大
▼
11-microservice-multi-module (Maven 多模块微服务)
│ 多服务 + 多模块
▼
12-microservice-complex-multi (最复杂组合)Example: 微服务复杂六边形架构
多聚合 + Saga + 多适配器的微服务六边形架构,每个服务内部结构复杂,适用于核心业务域。
场景
负责核心交易链路的 order-service,包含 4 个聚合根、跨聚合 Saga、多外部依赖。服务本身领域复杂度高,但仍是单模块部署。
目录树 & 包结构
order-service/
├── pom.xml
├── src/main/java/com/example/order/
│ ├── OrderServiceApplication.java
│ │
│ ├── domain/
│ │ ├── model/
│ │ │ ├── order/
│ │ │ │ ├── Order.java # 聚合根 1
│ │ │ │ ├── OrderId.java
│ │ │ │ ├── OrderItem.java
│ │ │ │ ├── OrderStatus.java
│ │ │ │ └── OrderCreatedEvent.java
│ │ │ ├── payment/
│ │ │ │ ├── Payment.java # 聚合根 2
│ │ │ │ ├── PaymentId.java
│ │ │ │ ├── PaymentMethod.java
│ │ │ │ └── PaymentCompletedEvent.java
│ │ │ ├── delivery/
│ │ │ │ ├── Delivery.java # 聚合根 3
│ │ │ │ ├── DeliveryId.java
│ │ │ │ └── DeliveryStatus.java
│ │ │ ├── invoice/
│ │ │ │ ├── Invoice.java # 聚合根 4
│ │ │ │ ├── InvoiceId.java
│ │ │ │ └── InvoiceNumber.java
│ │ │ └── shared/
│ │ │ ├── Money.java
│ │ │ ├── CustomerId.java
│ │ │ ├── ProductId.java
│ │ │ └── Address.java
│ │ ├── service/
│ │ │ ├── OrderPaymentSaga.java # Saga 编排
│ │ │ ├── DeliverySchedulingService.java # 领域服务
│ │ │ └── InvoiceGenerationService.java # 领域服务
│ │ ├── port/
│ │ │ ├── inbound/
│ │ │ │ ├── CreateOrderUseCase.java
│ │ │ │ ├── PayOrderUseCase.java
│ │ │ │ ├── TrackDeliveryUseCase.java
│ │ │ │ ├── GenerateInvoiceUseCase.java
│ │ │ │ └── CancelOrderUseCase.java
│ │ │ └── outbound/
│ │ │ ├── OrderRepository.java
│ │ │ ├── PaymentRepository.java
│ │ │ ├── DeliveryRepository.java
│ │ │ ├── InvoiceRepository.java
│ │ │ ├── PaymentGateway.java # 外部支付
│ │ │ ├── LogisticsProvider.java # 外部物流
│ │ │ ├── InventoryClient.java # 库存服务(跨服务)
│ │ │ ├── NotificationPort.java # 通知(跨服务)
│ │ │ └── EventPublisher.java # 事件总线
│ │ └── event/
│ │ └── handler/
│ │ ├── PaymentCompletedEventHandler.java
│ │ └── InventoryReservedEventHandler.java
│ │
│ ├── application/
│ │ └── service/
│ │ ├── CreateOrderServiceImpl.java
│ │ ├── PayOrderServiceImpl.java
│ │ ├── CancelOrderServiceImpl.java
│ │ └── SagaOrchestratorImpl.java # Saga 执行者
│ │
│ ├── adapter/
│ │ ├── inbound/
│ │ │ ├── rest/
│ │ │ │ ├── OrderController.java
│ │ │ │ └── PaymentController.java
│ │ │ ├── grpc/
│ │ │ │ └── OrderGrpcService.java
│ │ │ └── kafka/
│ │ │ ├── InventoryEventListener.java # 驱动型入口(异步事件)
│ │ │ └── PaymentEventListener.java
│ │ └── outbound/
│ │ ├── persistence/
│ │ │ ├── PostgresOrderRepository.java
│ │ │ ├── PostgresPaymentRepository.java
│ │ │ ├── PostgresDeliveryRepository.java
│ │ │ ├── PostgresInvoiceRepository.java
│ │ │ └── entity/ (JPA Entities)
│ │ ├── external/
│ │ │ ├── StripePaymentGateway.java
│ │ │ └── FedExLogisticsProvider.java
│ │ ├── microservice/
│ │ │ ├── RestInventoryClient.java # HTTP 调用 inventory-service
│ │ │ └── GrpcNotificationAdapter.java # gRPC 调用 notification-service
│ │ └── messaging/
│ │ └── KafkaEventPublisher.java
│ │
│ └── configuration/
│ ├── AdapterConfig.java
│ ├── UseCaseConfig.java
│ └── SagaConfig.java
│
└── src/test/
├── java/
│ ├── domain/
│ │ ├── OrderTest.java
│ │ └── OrderPaymentSagaTest.java
│ ├── application/
│ │ └── SagaOrchestratorImplTest.java
│ └── adapter/
│ ├── rest/OrderControllerTest.java
│ └── microservice/RestInventoryClientTest.java
└── resources/
└── contract/ # CDC 契约测试
└── inventory-service.yaml依赖关系
adapter/inbound/ (REST/gRPC/Kafka)
│ 依赖 UseCase 接口
▼
application/service/
│ 依赖端口接口 + Saga 编排
▼
domain/
├── port/inbound/ (被 application 实现)
├── port/outbound/ (被 adapter/outbound 实现)
├── model/ (Order/Payment/Delivery/Invoice 4 个聚合根)
├── service/ (Saga/领域服务)
└── event/handler/ (领域事件处理器)
│
│ 实现
▼
adapter/outbound/
├── persistence/ → PostgreSQL
├── external/ → Stripe / FedEx
├── microservice/ → inventory-service / notification-service
└── messaging/ → KafkaSaga 编排示例:
OrderPaymentSaga {
1. ReserveInventory → inventory-service (出站端口)
2. ChargePayment → Stripe (出站端口)
3. ConfirmDelivery → FedEx (出站端口)
4. GenerateInvoice → 领域服务 (本地)
5. PublishEvents → Kafka (出站端口)
}适用场景
| 维度 | 值 | 说明 |
|---|---|---|
| 团队规模 | 5-15 人/服务 | 核心服务需要较多人力 |
| 聚合根数量 | 4+ | 复杂业务域,多聚合在同一服务内 |
| Saga 编排 | 是 | 跨聚合 + 跨服务的事务协调 |
| 外部依赖 | 8+ | 支付/物流/其他微服务/消息队列 |
| 服务间通信 | 同步+异步 | REST + gRPC + Kafka 混合 |
关键设计决策
1. 跨服务调用通过端口抽象 — InventoryClient 是端口而非直接 HTTP 调用,可替换为 gRPC 或消息 2. Saga 作为领域服务 — 协调跨聚合一致性,补偿事务 3. 事件驱动入口 — Kafka Listener 作为主适配器,异步驱动业务 4. CDC 契约测试 — 验证对其他微服务的调用契约
端口统计
| 端口类型 | 数量 | 说明 |
|---|---|---|
| 入站端口 (UseCase) | 5 | Create/Pay/Track/Generate/Cancel |
| 出站端口 (Repository) | 4 | Order/Payment/Delivery/Invoice |
| 出站端口 (External) | 2 | PaymentGateway/LogisticsProvider |
| 出站端口 (Microservice) | 2 | InventoryClient/NotificationPort |
| 出站端口 (Messaging) | 1 | EventPublisher |
| 合计 | 14 | 单服务端口数反映了领域复杂度 |
Example: 微服务多模块六边形架构
微服务 + Maven 多模块的组合,每个微服务内部按六边形模块拆分,适用于大型团队。
场景
核心服务(order-service)内部使用 Maven 多模块,通过编译期约束保证架构边界;同时作为独立服务在 Kubernetes 集群中部署。
服务内部模块结构
order-service/
├── pom.xml # 父 POM
├── Dockerfile
├── k8s/
│ ├── deployment.yaml
│ └── service.yaml
│
├── order-domain/ # domain 模块
│ ├── pom.xml # 零框架依赖
│ └── src/main/java/com/example/order/domain/
│ ├── model/
│ │ ├── order/Order.java
│ │ ├── order/OrderId.java
│ │ └── payment/Payment.java
│ ├── port/
│ │ ├── inbound/
│ │ │ ├── CreateOrderUseCase.java
│ │ │ └── GetOrderUseCase.java
│ │ └── outbound/
│ │ ├── OrderRepository.java
│ │ ├── PaymentGateway.java
│ │ └── EventPublisher.java
│ └── event/
│ └── OrderCreatedEvent.java
│
├── order-application/ # application 模块
│ ├── pom.xml # 依赖 order-domain
│ └── src/main/java/com/example/order/application/
│ ├── service/
│ │ ├── CreateOrderServiceImpl.java
│ │ └── GetOrderServiceImpl.java
│ └── dto/OrderDTO.java
│
├── order-adapter-inbound/ # 主适配器模块
│ ├── pom.xml # 依赖 order-application + Spring Web
│ └── src/main/java/com/example/order/adapter/inbound/
│ ├── rest/
│ │ └── OrderController.java
│ └── grpc/
│ └── OrderGrpcService.java
│
├── order-adapter-outbound/ # 次适配器模块
│ ├── pom.xml # 依赖 order-domain + Spring Data/Kafka/Stripe
│ └── src/main/java/com/example/order/adapter/outbound/
│ ├── persistence/
│ │ ├── PostgresOrderRepository.java
│ │ └── entity/OrderJpaEntity.java
│ ├── external/
│ │ └── StripePaymentGateway.java
│ └── messaging/
│ └── KafkaEventPublisher.java
│
├── order-adapter-outbound-inventory/ # 跨服务适配器模块(可选独立)
│ ├── pom.xml # 依赖 order-domain + gRPC Client
│ └── src/main/java/.../adapter/outbound/microservice/
│ └── GrpcInventoryClient.java
│
└── order-app/ # 启动器模块
├── pom.xml # 依赖所有模块 + Spring Boot
└── src/main/java/com/example/order/
├── OrderApp.java
└── config/
├── AdapterConfig.java
└── UseCaseConfig.java跨服务调用架构
┌───────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌─ order-service ──────────────────────────────────────┐ │
│ │ │ │
│ │ order-app (启动器) │ │
│ │ ├── order-domain (端口 + 领域模型) │ │
│ │ ├── order-application (UseCase 实现) │ │
│ │ ├── order-adapter-inbound (REST/gRPC 入口) │ │
│ │ └── order-adapter-outbound │ │
│ │ ├── PostgreSQL │ │
│ │ ├── Kafka │ │
│ │ └── Stripe API │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ gRPC │
│ ┌─ inventory-service ──┐ │
│ │ (同样六边形多模块) │ │
│ │ domain/application/ │ │
│ │ adapter-inbound/ │ ← 暴露 gRPC Inventory API │
│ │ adapter-outbound/ │ │
│ └───────────────────────┘ │
│ │
│ ┌─ payment-service ─────┐ │
│ │ (同样六边形多模块) │ │
│ │ domain/application/ │ │
│ │ adapter-inbound/ │ │
│ │ adapter-outbound/ │ ← Stripe, PayPal │
│ └───────────────────────┘ │
└───────────────────────────────────────────────────────────┘模块依赖图(单服务内部)
┌──────────────────┐
│ order-app │ ← 依赖所有模块
│ (SpringBoot) │
└──┬───┬───┬───┬──┘
│ │ │ │
┌────────────┼───┼───┼───┼────────────┐
│ │ │ │ │ │
┌──────▼──────┐ ┌───▼───▼───▼───▼───┐ ┌──────▼──────────────┐
│order-domain │ │order-application │ │order-adapter-outbound│
│ (零框架依赖) │ │(只依赖 domain) │ │(依赖 domain + 框架) │──────► PostgreSQL
└──────▲──────┘ └────────▲──────────┘ └────────▲─────────────┘───────► Kafka
│ │ │ ───► Stripe
│ ┌──────┴──────┐ │
│ │order-adapter│──────────────┘
│ │ -inbound │ (仅注入时依赖 application)
│ │ │──────► HTTP Client (外部)
│ └─────────────┘──────► gRPC Server (外部)
│
┌──────┴──────────────┐
│order-adapter- │ ← 独立的跨服务调用模块
│outbound-inventory │──────► gRPC → inventory-service
└─────────────────────┘适用场景
| 维度 | 值 | 说明 |
|---|---|---|
| 团队规模 | 8-20 人/服务 | 大团队需要模块级并行开发 |
| 服务数量 | 5-20 | 多个微服务各用多模块六边形 |
| 模块数量/服务 | 5-7 | domain/application/adapter-in/out/app + 可选 |
| 跨服务通信 | gRPC + Kafka | 同步远程调用 + 异步事件 |
| 领域复杂度 | 中-高 | 多聚合 + Saga |
| 项目生命周期 | 成熟期多团队 | 需要强制架构边界 |
关键设计决策
1. 跨服务适配器作为独立模块 — order-adapter-outbound-inventory 独立模块,便于 gRPC → REST HTTP 切换 2. 每个服务独立 Git 仓库 — 服务间解耦,独立 CI/CD 3. 共享 Kernel 放在独立库 — 公共 DTO/事件定义用独立 Maven 依赖 4. app 模块作为组装层 — 不包含业务代码,只负责 DI 和启动
构建脚本示例 (order-service/pom.xml)
<project>
<groupId>com.example</groupId>
<artifactId>order-service</artifactId>
<packaging>pom</packaging>
<modules>
<module>order-domain</module>
<module>order-application</module>
<module>order-adapter-inbound</module>
<module>order-adapter-outbound</module>
<module>order-adapter-outbound-inventory</module>
<module>order-app</module>
</modules>
</project>对比维度
| 维度 | 单模块微服务 (09/10) | 多模块微服务 (11) |
|---|---|---|
| 构建复杂度 | 低 | 中(每服务 5-7 个 pom.xml) |
| 边界保护 | 约定 | 强制(编译期) |
| 团队并行度 | 模块内 | 模块间 |
| IDE 导入 | 简单 | 需 import multi-module project |
| 新人上手 | 快 | 需要理解多模块结构 |
| 适合团队规模 | 3-8 人 | 8-20 人 |
Example: 微服务复杂多模块六边形架构
最复杂的六边形架构组合:多微服务 x 多模块 x 多聚合,适用于超大型团队的分布式系统。
场景
50+ 人团队、15+ 微服务、每个核心服务内部使用 Maven 多模块六边形架构。服务间通过 gRPC + Kafka 通信,CQRS 读写分离,独立数据库。
系统全景
┌──────────────────────────────────────────────────────────────────┐
│ API Gateway (Kong / Envoy) │
└──┬──────────┬──────────┬──────────┬──────────┬───────────────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────────┐
│order │ │paymt │ │inven │ │deliv │ │notifi │
│-svc │ │-svc │ │-svc │ │-svc │ │-svc │
│ │ │ │ │ │ │ │ │ │
│ 6模块 │ │ 5模块 │ │ 5模块 │ │ 5模块 │ │ 4模块 │
│ CQRS │ │ Saga │ │ CQRS │ │ Saga │ │ 简单模块 │
└──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ └────┬─────┘
│ │ │ │ │
└────┬────┴────┬────┴────┬────┴─────┬──────┘
│ │ │ │
┌────▼─────────▼─────────▼──────────▼────┐
│ Kafka Event Bus │
└────────────────────────────────────────┘
│ │ │ │
┌────▼───┐ ┌───▼───┐ ┌───▼───┐ ┌───▼────┐
│ Postgre│ │ Postgre│ │ Postgre│ │ MongoDB│
│ (order)│ │(paymt) │ │(inven) │ │(notif) │
└────────┘ └───────┘ └───────┘ └────────┘order-service 内部(最复杂 — 7 模块 + CQRS)
order-service/
├── pom.xml
├── Dockerfile
├── k8s/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── configmap.yaml
│
├── order-domain/ # 领域核心
│ ├── pom.xml # 零框架依赖
│ └── src/main/java/.../
│ ├── model/
│ │ ├── order/Order.java
│ │ ├── order/OrderId.java
│ │ ├── order/OrderItem.java
│ │ └── order/OrderStatus.java
│ ├── service/
│ │ ├── OrderPricingService.java # 定价领域服务
│ │ └── OrderSagaCoordinator.java # Saga 协调
│ ├── port/
│ │ ├── inbound/
│ │ │ ├── command/ # CQRS Command 端口
│ │ │ │ ├── CreateOrderUseCase.java
│ │ │ │ ├── PayOrderUseCase.java
│ │ │ │ └── CancelOrderUseCase.java
│ │ │ └── query/ # CQRS Query 端口
│ │ │ ├── OrderQueryUseCase.java
│ │ │ └── OrderHistoryUseCase.java
│ │ └── outbound/
│ │ ├── OrderCommandRepository.java # 写库
│ │ ├── OrderQueryRepository.java # 读库(CQRS 分离)
│ │ ├── PaymentGateway.java
│ │ ├── InventoryClient.java
│ │ ├── LogisticsClient.java
│ │ ├── NotificationClient.java
│ │ └── EventPublisher.java
│ └── event/
│ ├── OrderCreatedEvent.java
│ ├── OrderPaidEvent.java
│ └── OrderCancelledEvent.java
│
├── order-application/ # 应用层
│ ├── pom.xml # 依赖 order-domain
│ └── src/main/java/.../
│ ├── command/ # Command 处理
│ │ ├── CreateOrderCommandHandler.java
│ │ ├── PayOrderCommandHandler.java
│ │ └── CancelOrderCommandHandler.java
│ └── query/ # Query 处理
│ ├── OrderQueryHandler.java
│ └── OrderHistoryQueryHandler.java
│
├── order-adapter-inbound/ # 主适配器
│ ├── pom.xml # 依赖 order-application + Spring Web/gRPC
│ └── src/main/java/.../
│ ├── rest/
│ │ ├── OrderCommandController.java # POST /orders, PUT /orders/{id}/pay
│ │ └── OrderQueryController.java # GET /orders, GET /orders/{id}
│ ├── grpc/
│ │ └── OrderGrpcService.java
│ └── kafka/
│ │ ├── PaymentEventListener.java # 异步驱动:支付完成 → 更新订单
│ │ └── InventoryEventListener.java # 异步驱动:库存预留 → 确认
│ └── dto/
│ ├── CreateOrderRequest.java
│ ├── OrderResponse.java
│ └── OrderEventProtoMapper.java
│
├── order-adapter-outbound-command/ # 次适配器 — 写
│ ├── pom.xml # 依赖 order-domain + Spring Data JPA
│ └── src/main/java/.../
│ └── persistence/
│ ├── PostgresOrderCommandRepository.java
│ ├── entity/
│ │ └── OrderJpaEntity.java
│ └── mapper/OrderCommandMapper.java
│
├── order-adapter-outbound-query/ # 次适配器 — 读
│ ├── pom.xml # 依赖 order-domain + Spring Data Elasticsearch
│ └── src/main/java/.../
│ └── persistence/
│ ├── ElasticsearchOrderQueryRepository.java
│ ├── document/
│ │ └── OrderDocument.java # ES 文档
│ └── mapper/OrderQueryMapper.java
│
├── order-adapter-outbound-external/ # 次适配器 — 外部
│ ├── pom.xml # 依赖 order-domain + gRPC/Stripe Client
│ └── src/main/java/.../
│ ├── payment/
│ │ └── StripePaymentGateway.java
│ ├── microservice/
│ │ ├── GrpcInventoryClient.java
│ │ ├── GrpcLogisticsClient.java
│ │ └── GrpcNotificationClient.java
│ └── messaging/
│ └── KafkaEventPublisher.java
│
└── order-app/ # 启动器
├── pom.xml # 依赖所有模块 + Spring Boot
├── src/main/java/.../
│ ├── OrderApp.java
│ └── config/
│ ├── AdapterConfig.java
│ ├── UseCaseConfig.java
│ └── CQRSConfig.java
└── src/main/resources/
├── application-command.yml # 写库配置
├── application-query.yml # 读库配置
└── application-external.yml # 外部服务配置CQRS 读写分离架构
┌─────────────────┐
│ API Gateway │
└────────┬────────┘
│
┌─────────────┴─────────────┐
│ │
┌─────────▼─────────┐ ┌──────────▼──────────┐
│ Command Side │ │ Query Side │
│ │ │ │
│ CreateOrderCmd │ │ OrderQueryHandler │
│ PayOrderCmd │ │ HistoryQueryHandler │
│ CancelOrderCmd │ │ │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ OrderCommandRepo │ │ OrderQueryRepo │
│ │ │ │ │ │
└───────┼───────────┘ └─────────┼────────────┘
│ │
┌───────▼───────────┐ ┌─────────▼────────────┐
│ PostgreSQL │ │ Elasticsearch │
│ (Normalized) │────►│ (Denormalized/View) │
│ │ CDC │ │
└───────────────────┘ └──────────────────────┘
CDC: Debezium 监听 PostgreSQL WAL → Kafka → Elasticsearch Sink适用场景
| 维度 | 值 | 说明 |
|---|---|---|
| 团队规模 | 50+ 人 | 超大型分布式团队 |
| 微服务数量 | 15+ | 核心交易链路服务 |
| 单服务模块数 | 5-7 | CQRS 需要独立读写模块 |
| CQRS | 是 | PostgreSQL (写) + Elasticsearch (读) |
| Saga | 是 | 分布式 Saga (Orchestration) |
| 事件溯源 | 可选 | Kafka 作为事件存储 |
| CDC | 是 | Debezium 同步读写库 |
| 项目生命周期 | 超大规模成熟期 | 架构投入值得回报 |
端口统计(order-service)
| 类型 | 端口 | 数量 |
|---|---|---|
| Command 入站 | CreateOrder/PayOrder/CancelOrder | 3 |
| Query 入站 | OrderQuery/OrderHistory | 2 |
| 出站 Repository | CommandRepo / QueryRepo | 2 |
| 出站 External | PaymentGateway | 1 |
| 出站 Microservice | Inventory/Logistics/Notification | 3 |
| 出站 Messaging | EventPublisher | 1 |
| 合计 | 12 |
关键设计决策
1. CQRS 端口分离 — command/ 和 query/ 子包下分别定义入站端口 2. 读写持久化独立模块 — adapter-outbound-command 和 adapter-outbound-query 各自独立 3. CDC 同步读写库 — Debezium 监听 PostgreSQL 变更,写入 Elasticsearch 4. 编排式 Saga — OrderSagaCoordinator 作为领域服务,协调分布式事务 5. 事件驱动 + REST 混合入口 — Kafka Listener 处理异步事件,REST/gRPC 处理同步请求
复杂度对比矩阵
简单 (Simple) 复杂 (Complex) 多模块 (Multi-Module)
单体 06-monolith-simple 07-monolith-complex 08-monolith-multi-module
(Monolith)
微服务 09-micro-simple 10-micro-complex 11-micro-multi-module
(Microservice)
微服务×多模块 — 12-micro-complex-multi (本示例)
(Complex Multi) ← 最复杂组合建议引入顺序
06-monolith-simple ──► 07-monolith-complex ──► 08-monolith-multi-module
│
▼
09-microservice-simple ──► 10-microservice-complex
│
▼
11-microservice-multi-module
│
▼
12-microservice-complex-multi (本示例)Port Definitions — 端口定义规范
概述
端口(Port)是六边形架构的核心抽象。端口是定义在领域层(Domain)或应用层(Application)的接口(Interface),规定了业务核心与外部世界之间的通信契约。
端口类型
入站端口(Inbound / Driver Port)
入站端口定义"外部世界如何使用你的系统"。每个入站端口对应一个用例(UseCase)。
命名规范:{Action}UseCase
// 入站端口 — 创建订单用例
public interface CreateOrderUseCase {
OrderCreatedResult execute(CreateOrderCommand command);
}
// 入站端口 — 查询订单用例
public interface GetOrderUseCase {
OrderDTO execute(GetOrderQuery query);
}
// 入站端口 — 支付订单用例
public interface PayOrderUseCase {
PaymentResult execute(PayOrderCommand command);
}出站端口(Outbound / Driven Port)
出站端口定义"你的系统如何使用外部世界"。每个出站端口对应一种外部依赖(数据库、消息队列、外部 API 等)。
命名规范:{Resource}Repository / {Resource}Gateway / {Resource}Port
// 出站端口 — 订单仓储
public interface OrderRepository {
Optional<Order> findById(OrderId id);
void save(Order order);
void delete(Order order);
}
// 出站端口 — 支付网关
public interface PaymentGateway {
PaymentResult charge(Money amount, PaymentMethod method);
RefundResult refund(PaymentId paymentId, Money amount);
}
// 出站端口 — 通知服务
public interface NotificationPort {
void sendEmail(Email to, EmailTemplate template);
void sendSMS(PhoneNumber to, String message);
}
// 出站端口 — 事件发布
public interface EventPublisher {
void publish(DomainEvent event);
void publishAll(List<DomainEvent> events);
}端口粒度原则
| 原则 | 说明 | 违反示例 |
|---|---|---|
| 单一职责 | 一个端口只做一件事 | OrderRepository 同时包含订单和用户操作 |
| 用例粒度 | 入站端口按用例拆分 | OrderUseCase 包含 10+ 方法 → 拆分为 CreateOrderUseCase、PayOrderUseCase |
| 依赖粒度 | 出站端口按外部依赖拆分 | 一个端口包含 DB + MQ + 邮件 → 拆分为独立端口 |
| 不过度设计 | 不为"未来可能"提前抽象 | 日志、序列化库不需要端口抽象 |
端口位置
{project}-domain/
├── port/
│ ├── inbound/ # ★ 入站端口(UseCase 接口)
│ │ ├── CreateOrderUseCase.java
│ │ ├── PayOrderUseCase.java
│ │ └── QueryOrderUseCase.java
│ └── outbound/ # ★ 出站端口(Repository/External 接口)
│ ├── OrderRepository.java
│ ├── PaymentGateway.java
│ ├── NotificationPort.java
│ └── EventPublisher.java端口设计检查清单
- [ ] 端口是否定义在 Domain 层(而非 Adapter 层)?
- [ ] 端口方法是否使用领域类型(而非技术类型如
String、Map)? - [ ] 端口是否避免了 SQL/HTTP/MQ 相关的参数?
- [ ] 端口是否足够小(≤ 5 个方法)?
- [ ] 方法返回值是否为
Optional而非null? - [ ] 入站端口是否以动词开头(
Create、Pay、Cancel)? - [ ] 出站端口是否以资源名词开头(
OrderRepository、PaymentGateway)?
Strong vs Weak Port
Weak Port(技术泄漏)
// ❌ 泄漏了 SQL 概念
public interface OrderRepository {
List<Order> findByQuery(String sql, Map<String, Object> params);
}Strong Port(纯净抽象)
// ✅ 纯领域概念
public interface OrderRepository {
Optional<Order> findById(OrderId id);
List<Order> findByCustomerId(CustomerId customerId, Page page);
List<Order> findByStatus(OrderStatus status);
void save(Order order);
}目录放置约定
| 代码风格 | 端口位置 | 适配器位置 |
|---|---|---|
| DDD 风格 | domain/port/inbound/、domain/port/outbound/ | adapter/inbound/、adapter/outbound/ |
| 应用层风格 | application/ports/driver/、application/ports/driven/ | infrastructure/adapters/driver/、infrastructure/adapters/driven/ |
| 混合风格 | 聚合仓储在 domain/{aggregate}/repository/,应用端口在 application/ports/ | infrastructure/ 或 adapter/ |
推荐 DDD 风格:将 all ports 定义在 domain/port/ 下,保持领域层所有权清晰。