
Ddd Architecture Clean
- 15 installs
- 1 repo stars
- Updated July 29, 2026
- full-statck-skills/ddd-skills
Implements Robert Martin's Clean Architecture with enterprise business rules, use-case interactors, and the inward dependency rule using Java/Spring Boot.
About
Guides Clean Architecture implementation with entities, use-case ports, interactors, and the strict inward dependency rule. A developer uses it to build or migrate a DDD project onto Clean Architecture.
- UseCase input/output port and interactor design
- Dependency-direction validation guidance
Ddd Architecture Clean 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-cleanAdd 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 Robert Martin's Clean Architecture with enterprise business rules, use-case interactors, and the inward dependency rule using Java/Spring Boot.
Files
DDD Architecture — Clean
Clean Architecture by Robert C. Martin (Uncle Bob): UseCase-centric, strict dependency rule — source code dependencies must point only inward.
Quick Start
直接说明你的需求,例如: "帮我建整洁架构项目骨架" / "订单模块实现 CreateOrder UseCase,从实体到控制器" / "检查项目依赖方向是否正确" / "从三层迁移到整洁架构,先迁订单"。
信息不足时我先给参考版本,再列出需要补充的具体信息。
Workflow
Step 1: 确认基础概念
确保团队对 DDD 实体、值对象、聚合根有一致理解。不清晰则先参考 ddd-architecture-awesome。
Step 2: 定义 Enterprise Business Rules
识别聚合根 → 设计实体 + 值对象 → 实现业务规则 → 定义领域事件。参考 01-core-entities、order-entity example
Step 3: 定义 UseCase 端口
为每个 UseCase 定义独立 Input Port → Output Port → UseCase DTO。参考 02-usecase-ports
Step 4: 实现 UseCase Interactor
编写 Interactor → 编排实体 + 端口调用 → 发布领域事件。参考 03-interactors、create-order example
Step 5: 实现适配器层
Controller → Presenter → Repository Impl → Gateway Impl → DI 配置。参考 04-adapters、05-framework-config
Step 6: 验证与测试
Enterprise 单元测试 → UseCase 集成测试 → Adapter 集成测试 → ArchUnit 验证。参考 06-dependency-rules、07-testing-strategy
When to Use / When NOT to
| ✅ 适用 | ❌ 不适用 |
|---|---|
| 企业级核心系统(订单、支付、库存) | 临时脚本、小工具 |
| 业务规则独立于交付机制 | 前端重后端轻的简单 CRUD |
| 需要严格模块物理隔离(15-50 人团队) | 团队 < 5 人、需快速迭代 |
| 微服务内部标准化架构 | 简单三层架构已足够 |
| UseCase 驱动的复杂业务编排 | 无复杂业务逻辑的管理后台 |
Boundary
| 类别 | 能力 | 说明 |
|---|---|---|
| ✅ 擅长 | 严格分层的企业级系统 | 15-50 人团队,模块间强隔离 |
| ✅ 擅长 | UseCase 驱动设计 | 每个用例独立 Interactor + Port |
| ✅ 擅长 | 依赖规则自动化检查 | ArchUnit 全自动验证分层合规 |
| ✅ 擅长 | 多语言落地 | Java/Go/TypeScript/C# 均可实现 |
| ⚠️ 需条件 | 团队理解 Interactor 模式 | 否则学习成本高,需培训 |
| ⚠️ 需条件 | 项目有一定规模 | 小项目用 Layered/Onion 更合适 |
| ⚠️ 需条件 | 需配合 DDD 领域模型 | 单独使用 Clean Architecture 过于抽象 |
| ❌ 超出范围 | 简单 CRUD 项目 | 用 ddd-architecture-layered |
| ❌ 超出范围 | 中文 Spring Boot 生态 | 用 ddd-architecture-cola |
| ❌ 超出范围 | 需要可视化环状模型 | 用 ddd-architecture-onion |
| ❌ 超出范围 | 需要端口适配器概念 | 用 ddd-architecture-hexagonal |
受众说明
| 用户类型 | 使用方式 |
|---|---|
| 后端架构师 / 技术负责人 | 直接使用,选型并按照 Workflow 6 步落地 |
| Java 开发者 | 参考 examples/ 代码模板,按步骤实现 UseCase |
| DDD 初学者 | 先读 ddd-architecture-awesome 了解概念,再回来看本 Skill |
| 多语言团队(Go/TypeScript/C#) | 架构规则通用,参考 references/ 中语言无关的部分 |
定制化:触发时说明你的技术栈(Java/Go/TS)、模块名称(如订单/支付),我会针对性地生成代码模板。
核心架构
Enterprise ← UseCase ← Adapter ← Framework详细原理(四层结构、架构对比、数据流转)参考 architecture-principles。目录结构参考 directory-structure。
开发规范
| 规范 | 说明 | 违规示例 |
|---|---|---|
| Entity 零框架依赖 | 不可 import Spring/JPA/Jackson | @Entity 出现在 core/entity/ |
| 每个 UseCase 独立端口 | 一个 UseCase = 一个 Input Port 接口 | 多个 UseCase 共享同一个接口 |
| Interactor 只编排不做业务 | 业务 if/else 必须在 Entity 中 | Interactor 中有状态机判断 |
| Output Port 在 UseCase 定义 | Repository 接口定义在 usecase 层 | Adapter 定义 Repository 接口 |
| 数据转换在 Adapter 层 | Controller DTO 不可穿越到 UseCase | @RequestBody DTO 直接传入 Interactor |
| 事务在 Framework 层管理 | @Transactional 只在 adapter/repository | Entity 方法上有 @Transactional |
详细规范请参考 references/06-dependency-rules.md。
Gotchas
| # | 陷阱 | 现象 | 正确做法 |
|---|---|---|---|
| 1 | UseCase 包含业务逻辑 | Interactor 中有 if/else 状态判断 | 抽到 Entity/Domain Service |
| 2 | Input Port 共享 | 多个 UseCase 共用一个接口 | 每个 UseCase 独立 Input Port |
| 3 | Controller DTO 穿越层 | 框架 DTO 传入 UseCase 层 | Adapter 层完成 DTO ↔ Domain 转换 |
| 4 | Entity 上有 @Entity 注解 | JPA 注解泄露到 Enterprise 层 | 在 Adapter 层创建独立的 JPA Entity |
| 5 | Interactor 直接调 Adapter | 跳过 Output Port 调用实现类 | 通过 Output Port 接口调用 |
| 6 | Output Port 放在 Adapter 层 | UseCase 依赖 Adapter 包 | Output Port 接口定义在 UseCase 层 |
| 7 | UseCase 返回 Entity 对象 | Interactor 返回 Order 而非 DTO | 返回 UseCase 专属 Output DTO |
| 8 | 过度设计简单查询 | 读操作也走完整 Input→Interactor→Output | 简单查询直接走 Repository |
| 9 | Adapter 包含业务逻辑 | Controller 中有 if/else | 业务逻辑全在 Enterprise 层 |
| 10 | 缺少 ArchUnit 检查 | 依赖违规无法自动发现 | CI 中集成 ArchUnit 测试 |
| 11 | 领域对象可变 | ValueObject 有 setter 方法 | 所有值对象不可变 (record/final) |
| 12 | UseCase 粒度不当 | Interactor 过大 (200+ 行) | 一个 Interactor 只做一个业务操作 |
| 13 | 忽略领域事件 | 关键操作后无事件发布 | 状态变更必须产生领域事件 |
| 14 | 事务在 UseCase 层 | Interactor 上有 @Transactional | 事务在 Adapter/Repository 层 |
| 15 | Entity 构造函数暴露 | Entity 用 public 构造函数 | 使用 static factory 方法(如 Order.create()) |
FAQ
| # | 问题 | 回答 |
|---|---|---|
| 1 | Clean Architecture 和六边形架构有什么区别? | 整洁架构以 UseCase 为组织核心,强调四层严格隔离;六边形以 Port/Adapter 为抽象,强调驱动/被驱动端口对称性。核心目标一致:内层不依赖外层。 |
| 2 | 什么时候用 Interactor vs Domain Service? | Interactor 在 UseCase 层做编排(调 Entity + 调 Port);Domain Service 在 Enterprise 层封装跨实体的业务规则(如 PricingService)。 |
| 3 | Service 在哪里写业务逻辑? | 都没有。Entity 中有业务方法(pay/cancel),DomainService 封装跨实体规则,Interactor 只编排不决策。 |
| 4 | 每个 UseCase 都要有独立的 Input Port 吗? | 是。这遵循接口隔离原则(ISP)。IOrderService 这种大接口是反模式。 |
| 5 | 一个 UseCase 有多个输出怎么办? | 每个输出独立为一个 Output Port。如 OrderRepository 为持久化,EventPublisher 为事件,PaymentGateway 为支付。 |
| 6 | Entity 层可以引用 Repository 接口吗? | 不可以。Enterprise 层不能知道任何 Output Port 的存在。Repository 接口在 UseCase 层定义。 |
| 7 | 简单查询也走 UseCase 层吗? | 不。纯读操作可以直接调用 Repository(查询不改变状态)。写操作必须走 UseCase。 |
| 8 | 如何组织多个 UseCase? | 按业务聚合组织:order/usecase/ 下放所有 Order 相关的 CreateOrder/PayOrder/CancelOrder。 |
| 9 | Interactor 中如何做事务? | Framework 层通过声明式事务(@Transactional)包裹 Interactor 调用。 |
| 10 | JPA Entity 和 Domain Entity 要分开吗? | 要。JPA Entity(@Entity)在 Adapter 层,Domain Entity(纯 POJO)在 Enterprise 层。通过 Converter 转换。 |
| 11 | 项目从三层架构迁移要多久? | 小型 (6 周) / 中型 (12 周) / 大型 (20 周)。使用 Strangler Fig 模式按 UseCase 逐步迁移。 |
| 12 | 如何保证依赖规则不被破坏? | 在 Framework 模块中写 ArchUnit 测试(参考 examples/04-archunit-test.md),CI 中每次提交自动检查。 |
| 13 | 值对象和实体的区别? | 值对象不可变、无 ID、按属性相等(如 Money);实体可变、有唯一 ID、按 ID 相等(如 Order)。 |
| 14 | 领域事件是同步还是异步发布? | Interactor 中同步收集事件并发布到 EventPublisher Port。异步处理由 Adapter 实现(写消息队列)。 |
| 15 | 适配器层可以有多个实现吗? | 可以。一个 Output Port 可以有多个 Adapter 实现:JPA/MyBatis/InMemory,通过 Spring Profile 切换。 |
Keywords
Clean Architecture, 整洁架构, Robert C. Martin, Uncle Bob, Enterprise Business Rules, Use Case, Interactor, Input Port, Output Port, Interface Adapter, Dependency Rule, 依赖倒置, 接口隔离, DDD, 领域驱动设计, 分层架构, 严格分层, 用例驱动, 领域模型, 实体, 值对象, 聚合根, 领域事件, ArchUnit, 依赖规则检查
References
Internal
| 文件 | 内容 |
|---|---|
| references/architecture-principles.md | 四层结构、依赖规则、架构对比、数据流转、目录结构 |
| references/01-core-entities.md | Enterprise 层实体、值对象、领域事件、异常模板与测试 |
| references/02-usecase-ports.md | Input/Output Port 定义、DTO 设计、端口设计规则 |
| references/03-interactors.md | Interactor 实现模板、复杂编排、查询 Interactor、测试 |
| references/04-adapters.md | Controller、Repository Impl、Gateway 适配器实现 |
| references/05-framework-config.md | Spring DI 配置、Security、Persistence、多 Profile |
| references/06-dependency-rules.md | 依赖规则矩阵、ArchUnit 全量测试集、CI 集成 |
| references/07-testing-strategy.md | 分层测试策略、Test Doubles、覆盖率目标 |
| references/08-migration-guide.md | 三层→整洁架构迁移指南、Strangler Fig 模式 |
| examples/01-order-entity.md | 完整 Order 实体代码 + 状态机 + 单元测试 |
| examples/02-create-order-usecase.md | 完整 CreateOrder UseCase 实现 + 测试 + Test Doubles |
| examples/03-repository-implementation.md | Repository Adapter JPA 实现 + 集成测试 |
| examples/04-archunit-test.md | 完整 ArchUnit 依赖规则测试套件 |
| examples/05-domain-event-handling.md | 领域事件定义、发布、消费完整实现 |
| examples/06-monolith-simple.md | 单体 Clean 简单版:单模块包级四层,目录树+依赖方向+ArchUnit |
| examples/07-monolith-complex.md | 单体 Clean 复杂版:多聚合+多 Interactor,共享内核+领域隔离 |
| examples/08-monolith-multi-module.md | 单体 Clean 多模块版:Maven 模块级分层,编译期强制依赖方向 |
| examples/09-microservice-simple.md | 微服务 Clean 简单版:包级四层+事件总线+Kafka 适配器 |
| examples/10-microservice-complex.md | 微服务 Clean 复杂版:CQRS + Saga + Outbox + 多子域 |
| examples/11-microservice-multi-module.md | 微服务 Clean 多模块版:6 模块+API 契约独立发布 |
| examples/12-microservice-complex-multi.md | 微服务 Clean 复杂多模块:8 模块+全模式矩阵+子域编排 |
External
- The Clean Architecture — Robert C. Martin (2012)
- Clean Architecture: A Craftsman's Guide — Robert C. Martin (2017)
- Domain-Driven Design: The Blue Book — Eric Evans (2003)
- Get Your Hands Dirty on Clean Architecture — Tom Hombergs
- thombergs/buckpal — Java Reference Implementation
- Hexagonal Architecture — Alistair Cockburn
DDD Skills 生态
| 前置/后续 | Skill |
|---|---|
| ← 前置 | ddd-architecture-selector — 架构选型 |
| → 后续 | ddd-domain-designer — 领域建模 |
| → 后续 | ddd-code-reviewer — 代码审查 |
| → 后续 | ddd-architecture-evaluator — 架构评估 |
| 🔗 相关 | ddd-architecture-hexagonal — 六边形架构 |
| 🔗 相关 | ddd-architecture-layered — 分层架构 |
🧭 DDD Skills Journey
📍 You are here: `ddd-architecture-clean` — Step 3: 整洁架构落地
awesome(入门) → selector(选型) → `clean(整洁架构)` + layered/onion/hexagonal/cola → domain-designer/cqrs/api-designer → code-reviewer → testing/devops/evaluator → architecture-doc
**← selector | → domain-designer | 🔗 api-designer · testing-strategist | 🏠 awesome
---
Security & Stability
- 所有代码模板为教育用途。生产环境请用环境变量替换占位凭证。
- 整洁架构的依赖规则(只能向内)天然阻止领域代码访问 I/O — 内置安全优势。
- Interactor 模式确保每个 UseCase 可独立测试,无需框架或数据库依赖。
- 不包含可执行脚本。本 Skill 仅提供架构指导和代码生成模式。
Example: Order Entity (Enterprise Business Rules Layer)
File: order-core/src/main/java/com/example/core/entity/Order.java
A complete rich domain model for an Order entity following Clean Architecture principles.
package com.example.core.entity;
import com.example.core.valueobject.*;
import com.example.core.exception.OrderDomainException;
import com.example.core.event.*;
import java.time.Instant;
import java.util.*;
/**
* ★ Order — Core Enterprise Business Rule Entity.
* Zero framework dependencies. Pure business logic.
*/
public class Order {
private final OrderId id;
private final CustomerId customerId;
private final List<OrderItem> items;
private Money totalAmount;
private OrderStatus status;
private final List<DomainEvent> domainEvents;
private final Instant createdAt;
private Instant updatedAt;
// ── Constructor (factory method preferred) ──
private Order(OrderId id, CustomerId customerId) {
this.id = Objects.requireNonNull(id, "OrderId must not be null");
this.customerId = Objects.requireNonNull(customerId, "CustomerId must not be null");
this.items = new ArrayList<>();
this.totalAmount = Money.ZERO;
this.status = OrderStatus.DRAFT;
this.domainEvents = new ArrayList<>();
this.createdAt = Instant.now();
this.updatedAt = this.createdAt;
}
/**
* ★ Static factory — preferred over public constructor.
* Enforces creation rules and records the OrderCreated event.
*/
public static Order create(OrderId id, CustomerId customerId) {
Order order = new Order(id, customerId);
order.addEvent(new OrderCreatedEvent(id, customerId));
return order;
}
// ── Business Behavior (Rich Domain Model) ──
/**
* Add an item to this order.
* Items can only be added while the order is in DRAFT status.
*/
public void addItem(OrderItem item) {
assertDraftStatus("add items");
this.items.add(Objects.requireNonNull(item, "Item must not be null"));
recalculateTotal();
this.updatedAt = Instant.now();
}
/**
* Remove an item from this order.
* Only allowed in DRAFT status.
*/
public void removeItem(ProductId productId) {
assertDraftStatus("remove items");
boolean removed = this.items.removeIf(item -> item.productId().equals(productId));
if (!removed) {
throw new OrderDomainException(
"Product " + productId.value() + " not found in order " + id.value());
}
recalculateTotal();
this.updatedAt = Instant.now();
}
/**
* Submit the order — transitions from DRAFT to SUBMITTED.
* Validates that the order has at least one item.
*/
public void submit() {
assertDraftStatus("submit");
if (items.isEmpty()) {
throw new OrderDomainException("Cannot submit empty order " + id.value());
}
this.status = OrderStatus.SUBMITTED;
this.updatedAt = Instant.now();
addEvent(new OrderSubmittedEvent(id, customerId, totalAmount));
}
/**
* Pay for this order. Requires SUBMITTED status.
*/
public void pay(PaymentId paymentId) {
assertStatus(OrderStatus.SUBMITTED, "pay");
this.status = OrderStatus.PAID;
this.updatedAt = Instant.now();
addEvent(new OrderPaidEvent(id, paymentId));
}
/**
* Cancel the order. Allowed from DRAFT or SUBMITTED.
*/
public void cancel(String reason) {
if (status == OrderStatus.PAID || status == OrderStatus.CANCELLED) {
throw new OrderDomainException(
"Cannot cancel order " + id.value() + " in status " + status);
}
this.status = OrderStatus.CANCELLED;
this.updatedAt = Instant.now();
addEvent(new OrderCancelledEvent(id, reason));
}
/**
* Ship the order (triggered by warehouse system).
*/
public void ship(TrackingId trackingId) {
assertStatus(OrderStatus.PAID, "ship");
this.status = OrderStatus.SHIPPED;
this.updatedAt = Instant.now();
addEvent(new OrderShippedEvent(id, trackingId));
}
/**
* Mark as delivered.
*/
public void deliver() {
assertStatus(OrderStatus.SHIPPED, "deliver");
this.status = OrderStatus.DELIVERED;
this.updatedAt = Instant.now();
addEvent(new OrderDeliveredEvent(id));
}
// ── Internal Helpers ──
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::subtotal)
.reduce(Money.ZERO, Money::add);
}
private void assertDraftStatus(String action) {
if (this.status != OrderStatus.DRAFT) {
throw new OrderDomainException(
"Cannot " + action + " on order " + id.value()
+ " in status " + status + " (must be DRAFT)");
}
}
private void assertStatus(OrderStatus expected, String action) {
if (this.status != expected) {
throw new OrderDomainException(
"Cannot " + action + " order " + id.value()
+ " in status " + status + " (must be " + expected + ")");
}
}
private void addEvent(DomainEvent event) {
this.domainEvents.add(Objects.requireNonNull(event));
}
// ── Getters (no setters — behavior is in methods) ──
public OrderId id() { return id; }
public CustomerId customerId() { return customerId; }
public List<OrderItem> items() { return Collections.unmodifiableList(items); }
public Money totalAmount() { return totalAmount; }
public OrderStatus status() { return status; }
public Instant createdAt() { return createdAt; }
public Instant updatedAt() { return updatedAt; }
public List<DomainEvent> domainEvents() {
return Collections.unmodifiableList(domainEvents);
}
public void clearEvents() {
domainEvents.clear();
}
// ── equals/hashCode based on identity ──
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Order order = (Order) o;
return id.equals(order.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return "Order{id=" + id.value()
+ ", status=" + status
+ ", total=" + totalAmount
+ ", items=" + items.size()
+ "}";
}
}Unit Test
package com.example.core.entity;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class OrderTest {
@Test
void shouldCreateOrder() {
Order order = Order.create(OrderId.generate(), CustomerId.of("CUST-001"));
assertThat(order.status()).isEqualTo(OrderStatus.DRAFT);
assertThat(order.items()).isEmpty();
assertThat(order.totalAmount()).isEqualTo(Money.ZERO);
assertThat(order.domainEvents()).hasSize(1);
}
@Test
void shouldAddItem() {
Order order = givenDraftOrder();
order.addItem(new OrderItem(ProductId.of("PROD-001"), 2, Money.of(10, "USD")));
assertThat(order.items()).hasSize(1);
assertThat(order.totalAmount()).isEqualTo(Money.of(20, "USD"));
}
@Test
void shouldSubmit() {
Order order = givenDraftOrder();
order.addItem(new OrderItem(ProductId.of("PROD-001"), 1, Money.of(10, "USD")));
order.submit();
assertThat(order.status()).isEqualTo(OrderStatus.SUBMITTED);
}
@Test
void shouldNotSubmitEmptyOrder() {
Order order = givenDraftOrder();
assertThatThrownBy(order::submit)
.isInstanceOf(OrderDomainException.class)
.hasMessageContaining("Cannot submit empty order");
}
@Test
void shouldPay() {
Order order = givenSubmittedOrder();
order.pay(PaymentId.generate());
assertThat(order.status()).isEqualTo(OrderStatus.PAID);
}
@Test
void shouldNotPayWhenNotSubmitted() {
Order order = givenDraftOrder();
assertThatThrownBy(() -> order.pay(PaymentId.generate()))
.isInstanceOf(OrderDomainException.class);
}
@Test
void shouldCancelDraftOrder() {
Order order = givenDraftOrder();
order.cancel("Changed mind");
assertThat(order.status()).isEqualTo(OrderStatus.CANCELLED);
}
@Test
void shouldNotCancelPaidOrder() {
Order order = givenSubmittedOrder();
order.pay(PaymentId.generate());
assertThatThrownBy(() -> order.cancel("No reason"))
.isInstanceOf(OrderDomainException.class);
}
@Test
void shouldEmitEventsThroughLifecycle() {
Order order = givenDraftOrder();
order.addItem(new OrderItem(ProductId.of("PROD-001"), 1, Money.of(10, "USD")));
order.submit();
order.pay(PaymentId.generate());
assertThat(order.domainEvents())
.extracting("class")
.containsExactly(
OrderCreatedEvent.class,
OrderSubmittedEvent.class,
OrderPaidEvent.class
);
}
private Order givenDraftOrder() {
return Order.create(OrderId.generate(), CustomerId.of("CUST-001"));
}
private Order givenSubmittedOrder() {
Order order = givenDraftOrder();
order.addItem(new OrderItem(ProductId.of("PROD-001"), 1, Money.of(10, "USD")));
order.submit();
order.clearEvents(); // start fresh
return order;
}
}State Machine
┌──────────┐
│ DRAFT │
└────┬─────┘
│ submit()
┌────▼─────┐
│SUBMITTED │
└────┬─────┘
│ pay()
┌────▼─────┐
│ PAID │
└────┬─────┘
│ ship()
┌────▼─────┐
│ SHIPPED │
└────┬─────┘
│ deliver()
┌────▼──────┐
│ DELIVERED │
└───────────┘
cancel() allowed: DRAFT, SUBMITTEDExample: CreateOrder UseCase Implementation
Files in this UseCase
order-usecase/
├── port/
│ ├── input/
│ │ └── CreateOrderUseCase.java ← Input Port (interface)
│ └── output/
│ ├── OrderRepository.java ← Output Port (interface)
│ ├── EventPublisher.java ← Output Port (interface)
│ └── InventoryGateway.java ← Output Port (interface)
├── interactor/
│ └── CreateOrderInteractor.java ← UseCase Implementation
└── dto/
├── CreateOrderInput.java ← Input DTO
└── CreateOrderOutput.java ← Output DTOInput Port
package com.example.usecase.port.input;
import com.example.usecase.dto.input.CreateOrderInput;
import com.example.usecase.dto.output.CreateOrderOutput;
/**
* ★ Input Port — UseCase interface.
* One UseCase = One Port (Interface Segregation Principle).
*/
public interface CreateOrderUseCase {
CreateOrderOutput execute(CreateOrderInput input);
}Input DTO
package com.example.usecase.dto.input;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.util.Currency;
import java.util.List;
/**
* UseCase-specific input data.
* No HTTP/DTO framework types — pure data carrier.
*/
public record CreateOrderInput(
@NotBlank String customerId,
@NotEmpty List<@Valid OrderItemInput> items
) {
/**
* Nested record for each order item.
*/
public record OrderItemInput(
@NotBlank String productId,
@Min(1) int quantity,
@NotNull BigDecimal unitPrice,
@NotBlank String currency
) {}
}Output DTO
package com.example.usecase.dto.output;
import com.example.core.entity.Order;
import com.example.core.event.OrderCreatedEvent;
import com.example.core.valueobject.Money;
import com.example.core.valueobject.OrderId;
import com.example.core.valueobject.OrderStatus;
/**
* UseCase-specific output data.
* Created from the entity after the UseCase executes.
*/
public record CreateOrderOutput(
OrderId orderId,
Money totalAmount,
OrderStatus status,
int itemCount
) {
public static CreateOrderOutput from(Order order) {
return new CreateOrderOutput(
order.id(),
order.totalAmount(),
order.status(),
order.items().size()
);
}
}Output Ports
package com.example.usecase.port.output;
import com.example.core.entity.Order;
import com.example.core.valueobject.OrderId;
import java.util.Optional;
/**
* ★ Output Port — what the UseCase needs from outside.
* Implemented by the Adapter layer.
*/
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(OrderId id);
void delete(OrderId id);
}
public interface EventPublisher {
void publish(DomainEvent event);
void publishAll(List<DomainEvent> events);
}
public interface InventoryGateway {
boolean reserveStock(ProductId productId, int quantity);
void releaseStock(ProductId productId, int quantity);
}Interactor (UseCase Implementation)
package com.example.usecase.interactor;
import com.example.core.entity.Order;
import com.example.core.entity.OrderItem;
import com.example.core.valueobject.*;
import com.example.core.exception.OrderDomainException;
import com.example.usecase.dto.input.CreateOrderInput;
import com.example.usecase.dto.output.CreateOrderOutput;
import com.example.usecase.port.input.CreateOrderUseCase;
import com.example.usecase.port.output.OrderRepository;
import com.example.usecase.port.output.EventPublisher;
import com.example.usecase.port.output.InventoryGateway;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Currency;
import java.util.List;
/**
* ★ Interactor — implements CreateOrder UseCase.
* Orchestrates the flow: validate → create → persist → publish events.
* Does NOT contain business logic (that's in the Entity).
*/
public class CreateOrderInteractor implements CreateOrderUseCase {
private static final Logger log = LoggerFactory.getLogger(CreateOrderInteractor.class);
private final OrderRepository orderRepository;
private final EventPublisher eventPublisher;
private final InventoryGateway inventoryGateway;
public CreateOrderInteractor(
OrderRepository orderRepository,
EventPublisher eventPublisher,
InventoryGateway inventoryGateway) {
this.orderRepository = orderRepository;
this.eventPublisher = eventPublisher;
this.inventoryGateway = inventoryGateway;
}
@Override
public CreateOrderOutput execute(CreateOrderInput input) {
// 1. Validate input (basic validation via records, detailed validation here)
validateInput(input);
// 2. Create the Order entity (Enterprise Business Rules)
OrderId orderId = OrderId.generate();
CustomerId customerId = CustomerId.of(input.customerId());
Order order = Order.create(orderId, customerId);
// 3. Add items (delegates to Entity behavior)
for (CreateOrderInput.OrderItemInput itemInput : input.items()) {
OrderItem item = OrderItem.create(
ProductId.of(itemInput.productId()),
itemInput.quantity(),
Money.of(itemInput.unitPrice(), Currency.getInstance(itemInput.currency()))
);
order.addItem(item);
}
// 4. Reserve inventory (through Output Port)
for (CreateOrderInput.OrderItemInput itemInput : input.items()) {
boolean reserved = inventoryGateway.reserveStock(
ProductId.of(itemInput.productId()),
itemInput.quantity()
);
if (!reserved) {
// Rollback previous reservations
rollbackReservations(input);
throw new OrderDomainException(
"Insufficient stock for product: " + itemInput.productId());
}
}
// 5. Submit the order (entity handles state transition)
order.submit();
// 6. Persist (through Output Port)
Order savedOrder = orderRepository.save(order);
// 7. Publish domain events (through Output Port)
eventPublisher.publishAll(order.domainEvents());
order.clearEvents();
// 8. Return result
log.info("Order created: {} for customer {}",
savedOrder.id().value(), input.customerId());
return CreateOrderOutput.from(savedOrder);
}
private void validateInput(CreateOrderInput input) {
if (input.customerId() == null || input.customerId().isBlank()) {
throw new IllegalArgumentException("Customer ID is required");
}
if (input.items() == null || input.items().isEmpty()) {
throw new IllegalArgumentException("At least one item is required");
}
for (CreateOrderInput.OrderItemInput item : input.items()) {
if (item.unitPrice().compareTo(java.math.BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException(
"Unit price must be positive for product: " + item.productId());
}
}
}
private void rollbackReservations(CreateOrderInput input) {
for (CreateOrderInput.OrderItemInput itemInput : input.items()) {
try {
inventoryGateway.releaseStock(
ProductId.of(itemInput.productId()),
itemInput.quantity()
);
} catch (Exception e) {
log.warn("Failed to rollback inventory for product: {}", itemInput.productId(), e);
}
}
}
}Unit Test
package com.example.usecase.interactor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class CreateOrderInteractorTest {
private InMemoryOrderRepository orderRepo;
private SpyEventPublisher eventPublisher;
private SpyInventoryGateway inventoryGateway;
private CreateOrderInteractor interactor;
@BeforeEach
void setUp() {
orderRepo = new InMemoryOrderRepository();
eventPublisher = new SpyEventPublisher();
inventoryGateway = new SpyInventoryGateway();
interactor = new CreateOrderInteractor(
orderRepo, eventPublisher, inventoryGateway);
}
@Test
void shouldCreateOrder() {
var input = new CreateOrderInput(
"CUST-001",
List.of(new OrderItemInput("PROD-001", 2, new BigDecimal("10.00"), "USD"))
);
var output = interactor.execute(input);
assertThat(output.orderId()).isNotNull();
assertThat(output.status()).isEqualTo(OrderStatus.SUBMITTED);
assertThat(output.itemCount()).isEqualTo(1);
assertThat(output.totalAmount()).isEqualTo(Money.of(20, "USD"));
}
@Test
void shouldReserveInventory() {
var input = validInput();
interactor.execute(input);
assertThat(inventoryGateway.reservations())
.containsEntry("PROD-001", 2);
}
@Test
void shouldPublishEvents() {
var input = validInput();
interactor.execute(input);
assertThat(eventPublisher.publishedEvents())
.hasSize(2) // OrderCreated + OrderSubmitted
.anyMatch(e -> e instanceof OrderCreatedEvent)
.anyMatch(e -> e instanceof OrderSubmittedEvent);
}
@Test
void shouldPersistOrder() {
var input = validInput();
var output = interactor.execute(input);
var saved = orderRepo.findById(output.orderId());
assertThat(saved).isPresent();
assertThat(saved.get().status()).isEqualTo(OrderStatus.SUBMITTED);
}
@Test
void shouldFailForEmptyItems() {
var input = new CreateOrderInput("CUST-001", List.of());
assertThatThrownBy(() -> interactor.execute(input))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void shouldFailWhenStockInsufficient() {
inventoryGateway.shouldFailFor("PROD-001");
var input = validInput();
assertThatThrownBy(() -> interactor.execute(input))
.isInstanceOf(OrderDomainException.class)
.hasMessageContaining("Insufficient stock");
}
@Test
void shouldRollbackReservationsOnFailure() {
inventoryGateway.shouldFailFor("PROD-002");
var input = new CreateOrderInput("CUST-001", List.of(
new OrderItemInput("PROD-001", 1, BigDecimal.TEN, "USD"),
new OrderItemInput("PROD-002", 1, BigDecimal.TEN, "USD") // this fails
));
assertThatThrownBy(() -> interactor.execute(input))
.isInstanceOf(OrderDomainException.class);
// PROD-001 reservation should be rolled back
assertThat(inventoryGateway.releasedStock())
.containsEntry("PROD-001", 1);
}
private CreateOrderInput validInput() {
return new CreateOrderInput(
"CUST-001",
List.of(new OrderItemInput("PROD-001", 2, new BigDecimal("10.00"), "USD"))
);
}
}In-Memory Test Doubles
// InMemoryOrderRepository.java
public class InMemoryOrderRepository implements OrderRepository {
private final Map<OrderId, Order> store = new HashMap<>();
@Override
public Order save(Order order) {
store.put(order.id(), order);
return order;
}
@Override
public Optional<Order> findById(OrderId id) {
return Optional.ofNullable(store.get(id));
}
@Override
public void delete(OrderId id) {
store.remove(id);
}
public void clear() { store.clear(); }
}
// SpyEventPublisher.java
public class SpyEventPublisher implements EventPublisher {
private final List<DomainEvent> events = new ArrayList<>();
@Override
public void publish(DomainEvent event) {
events.add(event);
}
@Override
public void publishAll(List<DomainEvent> events) {
this.events.addAll(events);
}
public List<DomainEvent> publishedEvents() { return List.copyOf(events); }
public void clear() { events.clear(); }
}
// SpyInventoryGateway.java
public class SpyInventoryGateway implements InventoryGateway {
private final Map<String, Integer> reservations = new HashMap<>();
private final Map<String, Integer> released = new HashMap<>();
private final Set<String> failFor = new HashSet<>();
@Override
public boolean reserveStock(ProductId productId, int quantity) {
if (failFor.contains(productId.value())) {
return false;
}
reservations.merge(productId.value(), quantity, Integer::sum);
return true;
}
@Override
public void releaseStock(ProductId productId, int quantity) {
released.merge(productId.value(), quantity, Integer::sum);
}
public void shouldFailFor(String productId) {
failFor.add(productId);
}
public Map<String, Integer> reservations() { return Map.copyOf(reservations); }
public Map<String, Integer> releasedStock() { return Map.copyOf(released); }
}Key Points
| Concept | How It's Applied Here |
|---|---|
| Dependency Rule | Interactor depends on Enterprise (Order) and Output Ports (interfaces), not on implementations |
| Single Responsibility | Interactor only orchestrates; entity contains business rules |
| Interface Segregation | Each UseCase gets its own Input Port interface |
| Dependency Inversion | Output Ports defined in UseCase layer, implemented in Adapter layer |
| Event Sourcing | Domain events collected in entity, published after persistence |
Example: Repository Implementation (Adapter Layer)
Overview
This example shows the complete chain from Output Port interface → JPA Entity → Repository Implementation → Unit Test.
Package Structure
order-adapter/
├── repository/
│ ├── JpaOrderRepository.java ← Implements OrderRepository Output Port
│ └── entity/
│ └── OrderEntity.java ← JPA persistence entity
├── converter/
│ └── OrderPersistenceConverter.java ← Domain ↔ Persistence mapping
└── gateway/
└── EventPublisherAdapter.java ← Implements EventPublisher Output PortStep 1: Output Port (UseCase Layer — what adapter implements)
// File: order-usecase/src/main/java/.../port/output/OrderRepository.java
package com.example.usecase.port.output;
import com.example.core.entity.Order;
import com.example.core.valueobject.OrderId;
import java.util.Optional;
/**
* ★ Output Port — defined in UseCase layer.
* The Adapter implements this interface.
*/
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(OrderId id);
void delete(OrderId id);
boolean existsByCustomerId(CustomerId customerId);
}Step 2: JPA Entity (Adapter Layer)
// File: order-adapter/src/main/java/.../repository/entity/OrderEntity.java
package com.example.adapter.repository.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Instant;
/**
* ★ JPA Entity — lives in the Adapter layer.
* This is SEPARATE from the domain Order entity.
* Framework annotations are confined here.
*/
@Entity
@Table(name = "orders")
public class OrderEntity {
@Id
@Column(length = 36)
private String id;
@Column(name = "customer_id", length = 36, nullable = false)
private String customerId;
@Column(name = "total_amount", precision = 19, scale = 2, nullable = false)
private BigDecimal totalAmount;
@Column(name = "currency", length = 3, nullable = false)
private String currency;
@Enumerated(EnumType.STRING)
@Column(name = "status", length = 20, nullable = false)
private OrderStatus status;
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@Version
private Long version;
// JPA requires default constructor
protected OrderEntity() {}
public OrderEntity(String id, String customerId,
BigDecimal totalAmount, String currency,
OrderStatus status,
Instant createdAt, Instant updatedAt) {
this.id = id;
this.customerId = customerId;
this.totalAmount = totalAmount;
this.currency = currency;
this.status = status;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
// ── Getters (used by converter) ──
public String getId() { return id; }
public String getCustomerId() { return customerId; }
public BigDecimal getTotalAmount() { return totalAmount; }
public String getCurrency() { return currency; }
public OrderStatus getStatus() { return status; }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public Long getVersion() { return version; }
public void setStatus(OrderStatus status) { this.status = status; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
/**
* JPA Entity for order items (part of Order aggregate).
*/
@Entity
@Table(name = "order_items")
public class OrderItemEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_id", length = 36, nullable = false)
private String orderId;
@Column(name = "product_id", length = 36, nullable = false)
private String productId;
@Column(nullable = false)
private int quantity;
@Column(name = "unit_price", precision = 19, scale = 2, nullable = false)
private BigDecimal unitPrice;
@Column(name = "unit_currency", length = 3, nullable = false)
private String unitCurrency;
protected OrderItemEntity() {}
// Constructor, getters...
}Step 3: Spring Data JPA Repository
// File: order-adapter/src/main/java/.../repository/SpringDataOrderJpaRepository.java
package com.example.adapter.repository;
import com.example.adapter.repository.entity.OrderEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* ★ Spring Data JPA Repository — framework-specific.
* Not to be confused with the domain Output Port.
*/
@Repository
public interface SpringDataOrderJpaRepository extends JpaRepository<OrderEntity, String> {
boolean existsByCustomerId(String customerId);
}Step 4: Persistence Converter
// File: order-adapter/src/main/java/.../converter/OrderPersistenceConverter.java
package com.example.adapter.converter;
import com.example.core.entity.Order;
import com.example.core.entity.OrderItem;
import com.example.core.valueobject.*;
import com.example.adapter.repository.entity.OrderEntity;
import org.springframework.stereotype.Component;
import java.util.Currency;
import java.util.stream.Collectors;
/**
* ★ Converter — maps between Domain Entity and JPA Entity.
* This is the ONLY place that knows about both representations.
*/
@Component
public class OrderPersistenceConverter {
public OrderEntity toPersistence(Order order) {
return new OrderEntity(
order.id().value(),
order.customerId().value(),
order.totalAmount().amount(),
order.totalAmount().currency().getCurrencyCode(),
order.status(),
order.createdAt(),
order.updatedAt()
);
}
public Order toDomain(OrderEntity entity) {
// Reconstruct domain entity from persistence state
OrderId id = OrderId.from(entity.getId());
CustomerId customerId = CustomerId.of(entity.getCustomerId());
Order order = Order.reconstruct(
id,
customerId,
Money.of(entity.getTotalAmount(), Currency.getInstance(entity.getCurrency())),
entity.getStatus(),
entity.getCreatedAt(),
entity.getUpdatedAt()
);
// Note: OrderItems would be loaded via a separate query or join
return order;
}
}Step 5: Repository Implementation (Impl)
// File: order-adapter/src/main/java/.../repository/JpaOrderRepository.java
package com.example.adapter.repository;
import com.example.core.entity.Order;
import com.example.core.valueobject.CustomerId;
import com.example.core.valueobject.OrderId;
import com.example.usecase.port.output.OrderRepository;
import com.example.adapter.converter.OrderPersistenceConverter;
import com.example.adapter.repository.entity.OrderEntity;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
/**
* ★ Repository Implementation — Adapter Layer.
* Implements the Output Port defined in the UseCase layer.
* All JPA/framework concerns are confined to this class.
*/
@Repository
@Transactional
public class JpaOrderRepository implements OrderRepository {
private final SpringDataOrderJpaRepository jpaRepo;
private final OrderPersistenceConverter converter;
public JpaOrderRepository(
SpringDataOrderJpaRepository jpaRepo,
OrderPersistenceConverter converter) {
this.jpaRepo = jpaRepo;
this.converter = converter;
}
@Override
@Transactional
public Order save(Order order) {
OrderEntity entity = converter.toPersistence(order);
OrderEntity saved = jpaRepo.save(entity);
return converter.toDomain(saved);
}
@Override
@Transactional(readOnly = true)
public Optional<Order> findById(OrderId id) {
return jpaRepo.findById(id.value())
.map(converter::toDomain);
}
@Override
@Transactional
public void delete(OrderId id) {
jpaRepo.deleteById(id.value());
}
@Override
@Transactional(readOnly = true)
public boolean existsByCustomerId(CustomerId customerId) {
return jpaRepo.existsByCustomerId(customerId.value());
}
}Step 6: Repository Integration Test
// File: order-adapter/src/test/java/.../repository/JpaOrderRepositoryTest.java
package com.example.adapter.repository;
import com.example.core.entity.Order;
import com.example.core.valueobject.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
/**
* ★ Integration test for the repository implementation.
* Uses a real (in-memory) database via @DataJpaTest.
*/
@DataJpaTest
@Import({JpaOrderRepository.class, OrderPersistenceConverter.class})
@ActiveProfiles("test")
class JpaOrderRepositoryTest {
@Autowired
private JpaOrderRepository repository;
@Autowired
private TestEntityManager em;
@Test
void shouldSaveAndFindOrder() {
Order order = givenOrder();
repository.save(order);
Optional<Order> found = repository.findById(order.id());
assertThat(found).isPresent();
assertThat(found.get().id()).isEqualTo(order.id());
assertThat(found.get().status()).isEqualTo(OrderStatus.DRAFT);
assertThat(found.get().totalAmount()).isEqualTo(Money.of(100, "USD"));
}
@Test
void shouldDeleteOrder() {
Order order = givenOrder();
repository.save(order);
repository.delete(order.id());
assertThat(repository.findById(order.id())).isEmpty();
}
@Test
void shouldReturnEmptyForMissingOrder() {
Optional<Order> found = repository.findById(OrderId.generate());
assertThat(found).isEmpty();
}
private Order givenOrder() {
return Order.create(
OrderId.generate(),
CustomerId.of("CUST-001")
);
}
}Event Publisher Adapter
// File: order-adapter/src/main/java/.../gateway/EventPublisherAdapter.java
package com.example.adapter.gateway;
import com.example.core.event.DomainEvent;
import com.example.usecase.port.output.EventPublisher;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* ★ Driven Adapter — implements EventPublisher Output Port.
* Uses RabbitMQ for production, configurable per profile.
*/
@Component
public class EventPublisherAdapter implements EventPublisher {
private static final Logger log = LoggerFactory.getLogger(EventPublisherAdapter.class);
private static final String EXCHANGE = "domain.events";
private final RabbitTemplate rabbitTemplate;
private final ObjectMapper objectMapper;
public EventPublisherAdapter(RabbitTemplate rabbitTemplate, ObjectMapper objectMapper) {
this.rabbitTemplate = rabbitTemplate;
this.objectMapper = objectMapper;
}
@Override
public void publish(DomainEvent event) {
try {
String json = objectMapper.writeValueAsString(event);
String routingKey = event.getClass().getSimpleName();
rabbitTemplate.convertAndSend(EXCHANGE, routingKey, json);
log.info("Published event: {} (key={})", event.eventId(), routingKey);
} catch (JsonProcessingException e) {
log.error("Failed to serialize event: {}", event.eventId(), e);
throw new EventPublishException("Failed to publish event", e);
}
}
@Override
public void publishAll(List<DomainEvent> events) {
events.forEach(this::publish);
}
}Key Design Decisions
| Decision | Rationale |
|---|---|
| Separate JPA Entity from Domain Entity | Domain Order is pure POJO; JPA OrderEntity has @Entity annotation. Prevents framework leak. |
| Converter in Adapter layer | Only the adapter knows about both representations. UseCase layer only sees Domain types. |
| Output Port as interface | UseCase defines what it needs; Adapter implements it. Full Dependency Inversion. |
| @Transactional at repository level | Transaction management is an infrastructure concern, not domain. |
| Spring Data JPA confined to Adapter | No @Repository or JPA annotations leak into UseCase or Enterprise layers. |
Example: ArchUnit Dependency Verification Test
File: order-framework/src/test/java/.../archunit/CleanArchitectureTest.java
Complete ArchUnit test suite for verifying Clean Architecture dependency rules automatically.
Complete Test Suite
package com.example.framework.archunit;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import static com.tngtech.archunit.library.Architectures.*;
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.*;
/**
* Automated Clean Architecture dependency verification.
* Run as part of CI/CD pipeline. Fails build on violations.
*/
class CleanArchitectureTest {
private static JavaClasses classes;
@BeforeAll
static void setUp() {
classes = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.example..");
}
// ─────────────────────────────────────────────
// Layer Dependency Rules
// ─────────────────────────────────────────────
@Nested
@DisplayName("Layer Dependency Rules")
class LayerDependencyTests {
@Test
@DisplayName("Enterprise layer must not depend on outer layers")
void enterpriseShouldNotDependOnOuterLayers() {
noClasses()
.that().resideInAnyPackage("com.example.core..")
.should().dependOnClassesThat()
.resideInAnyPackage(
"com.example.usecase..",
"com.example.adapter..",
"com.example.framework.."
)
.because("Enterprise Business Rules (core) must be completely "
+ "independent of outer layers (Clean Architecture rule)")
.check(classes);
}
@Test
@DisplayName("UseCase layer must not depend on Adapter or Framework")
void useCaseShouldNotDependOnAdapterOrFramework() {
noClasses()
.that().resideInAnyPackage("com.example.usecase..")
.should().dependOnClassesThat()
.resideInAnyPackage(
"com.example.adapter..",
"com.example.framework.."
)
.because("UseCase layer must only depend on Enterprise layer "
+ "and its own packages")
.check(classes);
}
@Test
@DisplayName("Adapter layer must not depend on Framework")
void adapterShouldNotDependOnFramework() {
noClasses()
.that().resideInAnyPackage("com.example.adapter..")
.should().dependOnClassesThat()
.resideInAnyPackage("com.example.framework..")
.because("Adapter layer must not know about framework configuration");
}
@Test
@DisplayName("No layer should have circular dependencies")
void noCircularDependencies() {
slices().matching("com.example.(*)..")
.should().beFreeOfCycles()
.because("Cycles between layers violate the Dependency Rule");
}
}
// ─────────────────────────────────────────────
// Layer Architecture Definition
// ─────────────────────────────────────────────
@Nested
@DisplayName("Clean Architecture Layers")
class CleanArchitectureDefinitionTests {
@Test
@DisplayName("All layers follow Clean Architecture dependency direction")
void shouldFollowCleanArchitecture() {
layeredArchitecture()
.consideringAllDependencies()
// Define layers
.layer("Enterprise")
.definedBy("com.example.core..")
.layer("UseCase")
.definedBy("com.example.usecase..")
.layer("Adapter")
.definedBy("com.example.adapter..")
.layer("Framework")
.definedBy("com.example.framework..")
// Dependency constraints: outer can depend on inner, NOT reverse
.whereLayer("Enterprise")
.mayOnlyBeAccessedByLayers("UseCase", "Adapter", "Framework")
.whereLayer("UseCase")
.mayOnlyBeAccessedByLayers("Adapter", "Framework")
.whereLayer("Adapter")
.mayOnlyBeAccessedByLayers("Framework")
.because("Clean Architecture: source code dependencies "
+ "must point only inward")
.check(classes);
}
}
// ─────────────────────────────────────────────
// Enterprise Layer Purity
// ─────────────────────────────────────────────
@Nested
@DisplayName("Enterprise Layer Purity")
class EnterprisePurityTests {
@Test
@DisplayName("Enterprise entities must not use Spring annotations")
void entitiesShouldNotUseSpringAnnotations() {
noClasses()
.that().resideInAnyPackage("com.example.core.entity..")
.should().beAnnotatedWith("org.springframework.stereotype.Service")
.orShould().beAnnotatedWith("org.springframework.stereotype.Component")
.orShould().beAnnotatedWith("org.springframework.web.bind.annotation.RestController")
.orShould().beAnnotatedWith("org.springframework.stereotype.Repository")
.because("Enterprise entities must be pure POJOs — "
+ "no framework annotations allowed")
.check(classes);
}
@Test
@DisplayName("Enterprise layer must not use JPA annotations")
void entitiesShouldNotUseJpaAnnotations() {
noClasses()
.that().resideInAnyPackage("com.example.core..")
.should().beAnnotatedWith("jakarta.persistence.Entity")
.orShould().beAnnotatedWith("jakarta.persistence.Table")
.orShould().beAnnotatedWith("jakarta.persistence.Column")
.orShould().beAnnotatedWith("jakarta.persistence.Id")
.because("Enterprise layer must be JPA-free — "
+ "persistence is an infrastructure concern")
.check(classes);
}
@Test
@DisplayName("Enterprise layer must not import framework packages")
void entitiesShouldNotImportFrameworkPackages() {
noClasses()
.that().resideInAnyPackage("com.example.core..")
.should().dependOnClassesThat()
.resideInAnyPackage(
"org.springframework..",
"jakarta.persistence..",
"jakarta.servlet..",
"com.fasterxml.jackson..",
"org.apache..",
"org.hibernate.."
)
.because("Enterprise Business Rules must have zero framework dependencies")
.check(classes);
}
}
// ─────────────────────────────────────────────
// UseCase Layer Rules
// ─────────────────────────────────────────────
@Nested
@DisplayName("UseCase Layer Rules")
class UseCaseLayerTests {
@Test
@DisplayName("UseCase ports must be interfaces")
void portsShouldBeInterfaces() {
classes()
.that().resideInAnyPackage("com.example.usecase.port..")
.should().beInterfaces()
.because("Ports are contracts — must be interfaces")
.check(classes);
}
@Test
@DisplayName("UseCase interactors must implement a port interface")
void interactorsShouldImplementPort() {
classes()
.that().resideInAnyPackage("com.example.usecase.interactor..")
.should().implement(
(java.lang.reflect.Type) null // simplified: check naming convention instead
)
.because("Every Interactor must implement an Input Port")
.check(classes);
// Alternative: check naming convention
classes()
.that().resideInAnyPackage("com.example.usecase.interactor..")
.should().haveSimpleNameEndingWith("Interactor")
.because("UseCase implementations should be named *Interactor")
.check(classes);
}
@Test
@DisplayName("UseCase must not import Spring's @Service annotation")
void useCaseShouldNotUseServiceAnnotation() {
noClasses()
.that().resideInAnyPackage("com.example.usecase..")
.should().beAnnotatedWith("org.springframework.stereotype.Service")
.because("UseCase layer uses @Service from Spring — "
+ "use framework config for wiring instead")
.allowEmptyShould(true)
.check(classes);
}
}
// ─────────────────────────────────────────────
// Adapter Layer Rules
// ─────────────────────────────────────────────
@Nested
@DisplayName("Adapter Layer Rules")
class AdapterLayerTests {
@Test
@DisplayName("Adapters implementing Output Ports must have matching names")
void repositoryImplementationsShouldHaveCorrectSuffix() {
classes()
.that().resideInAnyPackage("com.example.adapter.repository..")
.and().areNotInterfaces()
.should().haveSimpleNameEndingWith("Repository")
.orShould().haveSimpleNameEndingWith("Impl")
.because("Repository adapters should be named *Repository or *Impl")
.check(classes);
}
@Test
@DisplayName("Controllers must be in the adapter layer")
void controllersShouldBeInAdapterLayer() {
classes()
.that().areAnnotatedWith("org.springframework.web.bind.annotation.RestController")
.or().areAnnotatedWith("org.springframework.stereotype.Controller")
.should().resideInAnyPackage("com.example.adapter.controller..")
.because("Controllers belong in the Adapter layer, not UseCase or Enterprise")
.check(classes);
}
@Test
@DisplayName("JPA entities must be in the adapter layer")
void jpaEntitiesShouldBeInAdapterLayer() {
classes()
.that().areAnnotatedWith("jakarta.persistence.Entity")
.should().resideInAnyPackage("com.example.adapter..")
.because("JPA entities belong in the Adapter layer, "
+ "separate from domain entities")
.check(classes);
}
}
// ─────────────────────────────────────────────
// Naming Conventions
// ─────────────────────────────────────────────
@Nested
@DisplayName("Naming Conventions")
class NamingConventionTests {
@Test
@DisplayName("Enterprise entities should not be suffixed with 'Entity'")
void domainEntitiesShouldNotBeNamedEntity() {
classes()
.that().resideInAnyPackage("com.example.core.entity..")
.should().haveSimpleNameNotEndingWith("Entity")
.because("Domain entities are just domain classes; "
+ "'Entity' suffix implies JPA Entity (which is in adapter)")
.check(classes);
}
@Test
@DisplayName("Value objects should be immutable (final fields)")
void valueObjectsShouldBeFinal() {
// Simplified check: value objects should have 'final' modifier
// or be Java records
noClasses()
.that().resideInAnyPackage("com.example.core.valueobject..")
.should().haveOnlyFinalFields()
.orShould().beRecords();
}
@Test
@DisplayName("Repository interfaces should be in port.output package")
void repositoryInterfacesShouldBeInPortOutput() {
classes()
.that().haveSimpleNameEndingWith("Repository")
.and().areInterfaces()
.and().resideInAnyPackage("com.example.usecase..")
.should().resideInAnyPackage("com.example.usecase.port.output..")
.because("Repository interfaces (Output Ports) belong in "
+ "usecase.port.output package")
.check(classes);
}
}
// ─────────────────────────────────────────────
// Anti-pattern Detection
// ─────────────────────────────────────────────
@Nested
@DisplayName("Anti-pattern Detection")
class AntiPatternTests {
@Test
@DisplayName("No domain service should have @Transactional")
void domainServiceShouldNotBeTransactional() {
noMethods()
.that().areDeclaredInClassesThat()
.resideInAnyPackage("com.example.core..")
.should().beAnnotatedWith("org.springframework.transaction.annotation.Transactional")
.because("Transactions belong in the Adapter/Infrastructure layer, "
+ "not in Enterprise Business Rules")
.check(classes);
}
@Test
@DisplayName("Enterprise services should not autowire")
void enterpriseLayerShouldNotAutowired() {
noClasses()
.that().resideInAnyPackage("com.example.core..")
.should().beAnnotatedWith("org.springframework.beans.factory.annotation.Autowired")
.because("Enterprise layer uses constructor injection via framework config, "
+ "not field injection with @Autowired")
.check(classes);
}
}
}Maven Setup
<!-- pom.xml -->
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>Test Execution
# Run ArchUnit tests only
./mvnw test -pl order-framework -Dtest=CleanArchitectureTest
# Include as part of the verify lifecycle
./mvnw verifyCI Pipeline Integration
# .github/workflows/architecture.yml
name: Architecture Compliance
on: [pull_request]
jobs:
archunit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
- name: Cache Maven
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
- name: Run ArchUnit Tests
run: |
./mvnw test -pl order-framework \
-Dtest=CleanArchitectureTest \
-DfailIfNoTests=false
- name: Generate Report (on failure)
if: failure()
run: echo "Architecture violations found. Check test output above."Expected Output
CleanArchitectureTest -
✔ Layer Dependency Rules
✔ Enterprise layer must not depend on outer layers
✔ UseCase layer must not depend on Adapter or Framework
✔ Adapter layer must not depend on Framework
✔ No layer should have circular dependencies
✔ Clean Architecture Layers
✔ All layers follow Clean Architecture dependency direction
✔ Enterprise Layer Purity
✔ Enterprise entities must not use Spring annotations
✔ Enterprise layer must not use JPA annotations
✔ Enterprise layer must not import framework packages
✔ UseCase Layer Rules
✔ UseCase ports must be interfaces
✔ UseCase interactors must implement a port interface
✔ UseCase must not import Spring's @Service annotation
✔ Adapter Layer Rules
✔ Repository implementations should have correct suffix
✔ Controllers must be in the adapter layer
✔ JPA entities must be in the adapter layer
✔ Naming Conventions
✔ Enterprise entities should not be suffixed with 'Entity'
✔ Value objects should be immutable
✔ Repository interfaces should be in port.output package
✔ Anti-pattern Detection
✔ No domain service should have @Transactional
✔ Enterprise layer should not autowireKey Points
| Test Category | Why It Matters |
|---|---|
| Layer Dependencies | Enforces the Clean Architecture dependency rule at compile level |
| Enterprise Purity | Prevents framework leaks into the most critical layer |
| Port Interfaces | Ensures the interface-segregation principle is followed |
| Name Conventions | Makes architecture violations obvious from file names alone |
| Anti-patterns | Catches common mistakes that violate DDD principles |
Run ArchUnit tests in CI on every pull request. If a violation is found, the build fails before the code reaches production.
领域事件处理示例
展示整洁架构中领域事件的定义、发布与消费的完整实现。
1. Enterprise 层 — 领域事件定义
// core/event/DomainEvent.java — 抽象基类(零框架依赖)
public abstract class DomainEvent {
private final String eventId;
private final String aggregateId;
private final Instant occurredOn;
protected DomainEvent(String aggregateId) {
this.eventId = UUID.randomUUID().toString();
this.aggregateId = aggregateId;
this.occurredOn = Instant.now();
}
public String getEventId() { return eventId; }
public String getAggregateId() { return aggregateId; }
public Instant getOccurredOn() { return occurredOn; }
}
// core/event/OrderCreatedEvent.java
public class OrderCreatedEvent extends DomainEvent {
private final OrderId orderId;
private final Money totalAmount;
private final List<OrderItem> items;
public OrderCreatedEvent(Order order) {
super(order.getId().getValue());
this.orderId = order.getId();
this.totalAmount = order.getTotalAmount();
this.items = new ArrayList<>(order.getItems());
}
public OrderId getOrderId() { return orderId; }
public Money getTotalAmount() { return totalAmount; }
public List<OrderItem> getItems() { return items; }
}
// core/event/OrderPaidEvent.java
public class OrderPaidEvent extends DomainEvent {
private final OrderId orderId;
private final Money paidAmount;
public OrderPaidEvent(Order order) {
super(order.getId().getValue());
this.orderId = order.getId();
this.paidAmount = order.getPaidAmount();
}
}2. UseCase 层 — 事件发布端口
// usecase/port/output/EventPublisher.java
public interface EventPublisher {
void publish(DomainEvent event);
}
// usecase/port/output/EventBus.java — 批量发布(性能优化)
public interface EventBus {
void publishAll(List<DomainEvent> events);
}3. Enterprise 层 — 实体中产生事件
// core/entity/Order.java
public class Order {
private OrderId id;
private OrderStatus status;
private List<DomainEvent> domainEvents = new ArrayList<>();
// 公有工厂方法(非公开构造函数)
public static Order create(OrderId id, CustomerId customerId, List<OrderItem> items) {
Order order = new Order(id, customerId, items);
order.status = OrderStatus.CREATED;
order.addDomainEvent(new OrderCreatedEvent(order));
return order;
}
public void pay(Money amount) {
if (status != OrderStatus.CREATED) {
throw new OrderDomainException("只有 CREATED 状态的订单可以支付");
}
this.paidAmount = amount;
this.status = OrderStatus.PAID;
addDomainEvent(new OrderPaidEvent(this));
}
// 内部收集事件,Interactor 通过 drain 获取
private void addDomainEvent(DomainEvent event) {
this.domainEvents.add(event);
}
public List<DomainEvent> drainEvents() {
var events = List.copyOf(this.domainEvents);
this.domainEvents.clear();
return events;
}
}4. UseCase 层 — Interactor 发布事件
// usecase/interactor/PayOrderInteractor.java
public class PayOrderInteractor implements PayOrderUseCase {
private final OrderRepository orderRepository;
private final EventPublisher eventPublisher;
public PayOrderInteractor(OrderRepository orderRepository, EventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.eventPublisher = eventPublisher;
}
@Override
public PayOrderOutput execute(PayOrderInput input) {
// 1. 加载实体
Order order = orderRepository.findById(new OrderId(input.getOrderId()))
.orElseThrow(() -> new OrderDomainException("订单不存在"));
// 2. 执行业务方法(内部产生事件)
order.pay(new Money(input.getAmount()));
// 3. 持久化
orderRepository.save(order);
// 4. 发布领域事件(Interactor 只编排不处理事件)
order.drainEvents().forEach(eventPublisher::publish);
// 5. 返回 DTO
return new PayOrderOutput(order.getId().getValue(), order.getStatus().name());
}
}5. Adapter 层 — 事件发布实现
// adapter/event/RabbitMqEventPublisher.java
@Component
public class RabbitMqEventPublisher implements EventPublisher {
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void publish(DomainEvent event) {
// 同步发布到消息队列
String routingKey = event.getClass().getSimpleName();
rabbitTemplate.convertAndSend("domain-events", routingKey, event);
}
}
// adapter/event/InMemoryEventPublisher.java — 测试用
public class InMemoryEventPublisher implements EventPublisher {
private final List<DomainEvent> publishedEvents = new ArrayList<>();
@Override
public void publish(DomainEvent event) {
publishedEvents.add(event);
}
public List<DomainEvent> getPublishedEvents() { return List.copyOf(publishedEvents); }
public void clear() { publishedEvents.clear(); }
}6. 事件消费(在 Adapter 层)
// adapter/listener/OrderEventListener.java
@Component
public class OrderEventListener {
private final PaymentGateway paymentGateway;
private final NotificationService notificationService;
@EventListener
public void handleOrderPaid(OrderPaidEvent event) {
// 异步处理:发发票、通知仓库、更新物流
notificationService.sendOrderConfirmation(event.getOrderId());
}
}验证要点
- 依赖规则:EventPublisher 接口在 UseCase 层,实现类在 Adapter 层
- 实体不依赖事件基础设施:Entity 只用
List<DomainEvent>收集,不 import 任何消息中间件 - Interactor 不处理事件:只负责 drain 和 publish,具体处理在 Adapter 层
- Drain 模式:Entity 的 drainEvents() 在持久化后调用,确保不丢失事件
- 可测试性:InMemoryEventPublisher 让单元测试零依赖
06 — 单体 Clean 架构(简单版)
单一 Maven 模块内按包名划分四层:entities + usecases + interface-adapters + frameworks。
目录树
order-service/
├── src/main/java/com/example/order/
│ ├── enterprise/ # Enterprise Business Rules
│ │ ├── entity/
│ │ │ ├── Order.java # 聚合根
│ │ │ └── OrderItem.java # 值对象
│ │ ├── vo/
│ │ │ ├── OrderId.java
│ │ │ ├── Money.java
│ │ │ └── OrderStatus.java
│ │ └── event/
│ │ ├── DomainEvent.java
│ │ └── OrderCreatedEvent.java
│ │
│ ├── usecase/ # Application Business Rules
│ │ ├── port/
│ │ │ ├── input/
│ │ │ │ └── CreateOrderUseCase.java
│ │ │ └── output/
│ │ │ ├── OrderRepository.java
│ │ │ ├── PaymentGateway.java
│ │ │ └── EventPublisher.java
│ │ ├── dto/
│ │ │ ├── CreateOrderRequest.java
│ │ │ └── CreateOrderResponse.java
│ │ └── interactor/
│ │ └── CreateOrderInteractor.java
│ │
│ ├── adapter/ # Interface Adapters
│ │ ├── controller/
│ │ │ └── OrderController.java
│ │ ├── repository/
│ │ │ ├── OrderJpaRepository.java
│ │ │ └── OrderRepositoryImpl.java
│ │ ├── gateway/
│ │ │ └── PaymentGatewayImpl.java
│ │ └── presenter/
│ │ └── CreateOrderPresenter.java
│ │
│ └── framework/ # Frameworks & Drivers
│ └── config/
│ ├── PersistenceConfig.java
│ ├── UseCaseConfig.java
│ └── WebConfig.java
│
├── src/test/java/com/example/order/
│ ├── enterprise/entity/OrderTest.java
│ ├── usecase/interactor/CreateOrderInteractorTest.java
│ ├── adapter/repository/OrderRepositoryImplTest.java
│ └── architecture/
│ └── ArchitectureTest.java
│
└── pom.xml包结构关系
┌─────────────────────────────────────────────────┐
│ framework │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Config │ │ Spring DI│ │ DevTools │ │
│ └────┬─────┘ └──────────┘ └──────────┘ │
│ │ 依赖 │
├───────▼─────────────────────────────────────────┤
│ adapter │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Controller│ │Repository│ │ Gateway │ │
│ │ │ │ Impl │ │ Impl │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
├───────▼──────────────▼──────────────▼───────────┤
│ usecase │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Interactor│ │Port(input│ │Port(out) │ │
│ │ │ │ /output) │ │ │ │
│ └────┬─────┘ └──────────┘ └──────────┘ │
│ │ │
├───────▼─────────────────────────────────────────┤
│ enterprise │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Entity │ │ ValueObj │ │ Event │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘依赖方向
framework ──► adapter ──► usecase ──► enterprise
│ │ │
└── 只依赖外层 ─┴── 不依赖内层 ─┘- enterprise: 零依赖(不依赖任何外部包,只依赖 Java 标准库)
- usecase: 只依赖
enterprise包 - adapter: 依赖
usecase端口 +enterprise实体 - framework: 依赖
adapter+ Spring Boot
ArchUnit 验证规则
@AnalyzeClasses(packages = "com.example.order")
public class ArchitectureTest {
@ArchTest
static final ArchRule enterprise_no_deps = classes()
.that().resideInAPackage("..enterprise..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage("java..", "..enterprise..");
@ArchTest
static final ArchRule usecase_no_framework = classes()
.that().resideInAPackage("..usecase..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage("..enterprise..", "..usecase..", "java..");
@ArchTest
static final ArchRule adapter_no_web = classes()
.that().resideInAPackage("..adapter.repository..")
.should().onlyHaveDependentClassesThat()
.resideInAPackage("..adapter..");
}适用场景
| 维度 | 说明 |
|---|---|
| 团队规模 | 3-8 人,单团队维护 |
| 项目复杂度 | 1-3 个聚合根,< 20 个 UseCase |
| 模块数 | 单一 Maven 模块(单仓库) |
| 部署方式 | 单体部署,一个 Spring Boot JAR |
| 演进路径 | 简单版 → 复杂版(07)→ 多模块(08)→ 微服务拆分 |
| 典型业务 | 中小型电商后台、CMS、内部工具系统 |
优缺点
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 包级隔离,结构清晰 | 编译期无法强制分层约束(需 ArchUnit 补救) |
| 学习成本低,团队快速上手 | 包依赖靠约定,新人可能放错位置 |
| 一套 CI 管道,部署简单 | 单一模块耦合,难以模块级独立发布 |
| 适合从三层架构渐进迁移 | 大型项目包膨胀,定位文件困难 |
07 — 单体 Clean 架构(复杂版)
多聚合根 + 多 Interactor,单模块内按领域划分子包,每个领域独立四层。
目录树
order-service/
├── src/main/java/com/example/
│ └── order/
│ ├── shared/ # 共享内核
│ │ ├── domain/
│ │ │ ├── Identifier.java # 通用 ID 基类
│ │ │ ├── Money.java
│ │ │ └── DomainEvent.java
│ │ └── event/
│ │ └── EventPublisher.java # 事件发布端口
│ │
│ ├── order/ # 订单领域
│ │ ├── enterprise/
│ │ │ ├── entity/
│ │ │ │ ├── Order.java # 聚合根
│ │ │ │ └── OrderItem.java
│ │ │ ├── vo/
│ │ │ │ ├── OrderId.java
│ │ │ │ └── OrderStatus.java
│ │ │ └── event/
│ │ │ ├── OrderCreatedEvent.java
│ │ │ └── OrderPaidEvent.java
│ │ ├── usecase/
│ │ │ ├── port/input/
│ │ │ │ ├── CreateOrderUseCase.java
│ │ │ │ ├── PayOrderUseCase.java
│ │ │ │ └── QueryOrderUseCase.java
│ │ │ ├── port/output/
│ │ │ │ └── OrderRepository.java
│ │ │ ├── dto/
│ │ │ │ ├── CreateOrderRequest.java
│ │ │ │ ├── CreateOrderResponse.java
│ │ │ │ └── OrderDTO.java
│ │ │ └── interactor/
│ │ │ ├── CreateOrderInteractor.java
│ │ │ ├── PayOrderInteractor.java
│ │ │ └── QueryOrderInteractor.java
│ │ ├── adapter/
│ │ │ ├── controller/
│ │ │ │ └── OrderController.java
│ │ │ ├── repository/
│ │ │ │ ├── OrderJpaEntity.java
│ │ │ │ ├── OrderItemJpaEntity.java
│ │ │ │ └── OrderRepositoryImpl.java
│ │ │ └── presenter/
│ │ │ └── OrderPresenter.java
│ │ └── framework/
│ │ └── config/
│ │ └── OrderDomainConfig.java
│ │
│ ├── payment/ # 支付领域
│ │ ├── enterprise/
│ │ │ ├── entity/
│ │ │ │ └── Payment.java # 聚合根
│ │ │ ├── vo/
│ │ │ │ ├── PaymentId.java
│ │ │ │ └── PaymentStatus.java
│ │ │ └── event/
│ │ │ └── PaymentCompletedEvent.java
│ │ ├── usecase/
│ │ │ ├── port/input/
│ │ │ │ └── ProcessPaymentUseCase.java
│ │ │ ├── port/output/
│ │ │ │ ├── PaymentRepository.java
│ │ │ │ └── PaymentGateway.java
│ │ │ ├── dto/
│ │ │ │ └── PaymentRequest.java
│ │ │ └── interactor/
│ │ │ └── ProcessPaymentInteractor.java
│ │ ├── adapter/
│ │ │ ├── controller/
│ │ │ │ └── PaymentController.java
│ │ │ ├── repository/
│ │ │ │ └── PaymentRepositoryImpl.java
│ │ │ └── gateway/
│ │ │ └── AlipayGatewayImpl.java
│ │ └── framework/
│ │ └── config/
│ │ └── PaymentDomainConfig.java
│ │
│ └── inventory/ # 库存领域
│ ├── enterprise/
│ │ ├── entity/
│ │ │ └── Inventory.java
│ │ └── vo/
│ │ ├── SkuId.java
│ │ └── Quantity.java
│ ├── usecase/
│ │ ├── port/input/
│ │ │ ├── ReserveInventoryUseCase.java
│ │ │ └── ReleaseInventoryUseCase.java
│ │ ├── port/output/
│ │ │ └── InventoryRepository.java
│ │ └── interactor/
│ │ ├── ReserveInventoryInteractor.java
│ │ └── ReleaseInventoryInteractor.java
│ ├── adapter/
│ │ └── repository/
│ │ └── InventoryRepositoryImpl.java
│ └── framework/
│ └── config/
│ └── InventoryDomainConfig.java
│
├── src/test/java/com/example/order/
│ ├── order/enterprise/entity/OrderTest.java
│ ├── order/usecase/interactor/
│ │ ├── CreateOrderInteractorTest.java
│ │ └── PayOrderInteractorTest.java
│ ├── payment/usecase/interactor/
│ │ └── ProcessPaymentInteractorTest.java
│ ├── architecture/
│ │ └── ArchitectureTest.java
│ └── integration/
│ └── OrderPaymentIntegrationTest.java
│
└── pom.xml领域间交互规则
┌──────────────────────────────────────────────────────┐
│ shared 共享内核 │
│ DomainEvent, Money, Identifier │
└──┬─────────────────┬──────────────────┬──────────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ order │ │ payment │ │ inventory │
│ 领域 │◄──│ 领域 │──►│ 领域 │
│ │ │ │ │ │
└──────────┘ └──────────┘ └──────────────┘
│ │ │
└─────── 通过事件异步通信 ────────┘- 领域间不得直接依赖:order 不能 import payment 的类
- 共享内核:shared 包放跨领域共用类型(Money、DomainEvent)
- 领域通信:通过领域事件异步解耦(OrderCreated → ReserveInventory)
- UseCase 编排跨领域:上级编排 Interactor 可依赖多个领域的 output port
依赖方向(领域内 + 领域间)
framework ──► adapter ──► usecase ──► enterprise
│ │ │
└── 仅依赖适配层 ─┴── 仅依赖端口 ─┘
领域间:
order ──► shared ◄── payment ──► shared ◄── inventory
└── 不得互相 import ──────────┘ArchUnit 验证(领域隔离)
@ArchTest
static final ArchRule no_domain_dependency = classes()
.that().resideInAPackage("..order..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage(
"..order..", // 同领域
"..shared..", // 共享内核
"java.."
);
@ArchTest
static final ArchRule order_not_depend_payment = noClasses()
.that().resideInAPackage("..order..")
.should().dependOnClassesThat()
.resideInAPackage("..payment..");适用场景
| 维度 | 说明 |
|---|---|
| 团队规模 | 8-20 人,多团队协作 |
| 项目复杂度 | 3-8 个聚合根,20-60 个 UseCase |
| 领域数 | 3-6 个核心领域 |
| 部署方式 | 单体部署 |
| 演进方向 | 复杂版 → 多模块版(08)→ 领域拆分微服务 |
| 典型业务 | 中型电商平台(订单+支付+库存+物流)、SaaS 后台 |
优缺点
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 领域隔离清晰,适合多人并行开发 | 编译期无法防止领域间直接依赖 |
| 事件驱动解耦,领域独立性强 | shared 包膨胀风险(垃圾场效应) |
| 为微服务拆分做好领域边界准备 | 共享内核变更影响所有领域 |
| 单一构建,CI 简单 | 领域边界依赖 ArchUnit 人工维护 |
08 — 单体 Clean 架构(多模块版)
每层一个 Maven 模块,编译期强制依赖方向,适合需要物理隔离的单体项目。
目录树
order-platform/
├── pom.xml # 父 POM(module 聚合)
│
├── order-enterprise/ # 模块1: Enterprise Business Rules
│ ├── pom.xml # 零外部依赖
│ └── src/main/java/com/example/order/enterprise/
│ ├── entity/
│ │ ├── Order.java
│ │ └── OrderItem.java
│ ├── vo/
│ │ ├── OrderId.java
│ │ ├── Money.java
│ │ └── OrderStatus.java
│ └── event/
│ ├── DomainEvent.java
│ └── OrderCreatedEvent.java
│
├── order-usecase/ # 模块2: Application Business Rules
│ ├── pom.xml # 只依赖 order-enterprise
│ └── src/main/java/com/example/order/usecase/
│ ├── port/input/
│ │ ├── CreateOrderUseCase.java
│ │ └── QueryOrderUseCase.java
│ ├── port/output/
│ │ ├── OrderRepository.java
│ │ └── PaymentGateway.java
│ ├── dto/
│ │ ├── CreateOrderRequest.java
│ │ ├── CreateOrderResponse.java
│ │ └── OrderSummaryDTO.java
│ └── interactor/
│ ├── CreateOrderInteractor.java
│ └── QueryOrderInteractor.java
│
├── order-adapter/ # 模块3: Interface Adapters
│ ├── pom.xml # 依赖 order-usecase + order-enterprise
│ └── src/main/java/com/example/order/adapter/
│ ├── controller/
│ │ └── OrderController.java
│ ├── repository/
│ │ ├── OrderJpaEntity.java
│ │ ├── OrderJpaRepository.java
│ │ └── OrderRepositoryImpl.java
│ └── gateway/
│ └── AlipayGatewayImpl.java
│
├── order-boot/ # 模块4: Frameworks & Drivers
│ ├── pom.xml # 依赖 order-adapter + Spring Boot
│ └── src/main/java/com/example/order/boot/
│ ├── OrderApplication.java # @SpringBootApplication
│ └── config/
│ ├── UseCaseConfig.java # @Configuration: Bean 装配
│ ├── PersistenceConfig.java
│ └── SecurityConfig.java
│
├── order-acceptance/ # 模块5: 验收测试(可选)
│ ├── pom.xml # 依赖 order-boot (test scope)
│ └── src/test/java/com/example/order/acceptance/
│ └── OrderAcceptanceTest.java
│
└── order-arch-test/ # 模块6: 架构测试(可选)
├── pom.xml # 依赖所有模块 (test scope)
└── src/test/java/com/example/order/arch/
└── DependencyRuleTest.javaMaven 依赖关系
<!-- order-enterprise/pom.xml -->
<!-- 零外部依赖,只含 Java 标准库 -->
<dependencies>
<!-- 无 -->
</dependencies>
<!-- order-usecase/pom.xml -->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>order-enterprise</artifactId>
</dependency>
</dependencies>
<!-- order-adapter/pom.xml -->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>order-usecase</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
<!-- order-boot/pom.xml -->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>order-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>order-usecase</artifactId>
</dependency>
<!-- Spring Boot Starter -->
</dependencies>依赖方向图
┌────────────────────────────────────────────┐
│ order-boot │
│ SpringBoot App + DI Config │
│ 依赖: adapter, usecase, enterprise │
└────────────────┬───────────────────────────┘
│
┌────────────────▼───────────────────────────┐
│ order-adapter │
│ Controller, Repository Impl, Gateway │
│ 依赖: usecase, enterprise │
└────────────────┬───────────────────────────┘
│
┌────────────────▼───────────────────────────┐
│ order-usecase │
│ Interactor, Port (in/out), DTO │
│ 依赖: enterprise │
└────────────────┬───────────────────────────┘
│
┌────────────────▼───────────────────────────┐
│ order-enterprise │
│ Entity, Value Object, Domain Event │
│ 零外部依赖 │
└────────────────────────────────────────────┘编译期强制 vs 运行时检查
| 机制 | 简单版(06) | 多模块版(08) |
|---|---|---|
| 分层隔离 | 包名约定 | Maven 模块依赖 |
| 违规检测 | ArchUnit 运行时 | 编译期报错 |
| usecase 引用 Spring | ArchUnit 拦截 | Maven 不解析类,编译失败 |
| 循环依赖 | 可能发生 | Maven 拒绝构建 |
| 构建速度 | 快 | 增量构建后可接受 |
适用场景
| 维度 | 说明 |
|---|---|
| 团队规模 | 8-25 人,多团队协作 |
| 项目复杂度 | 3-8 个聚合根,30-80 个 UseCase |
| 隔离要求 | 需要编译期强制分层隔离 |
| 演进方向 | 可进一步拆分为领域模块 + 微服务 |
| 典型业务 | 企业级中台、大型电商、金融核心系统 |
优缺点
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 编译期强制依赖方向,杜绝腐化 | 模块数多,构建配置复杂 |
| 各模块可独立构建、测试 | 新人上手成本较高 |
| 为微服务拆分提供天然的模块边界 | 跨模块测试配置繁琐 |
| 适合大型团队并行开发 | 单体中过度模块化增加维护成本 |
09 — 微服务 Clean 架构(简单版)
单个微服务内按包名划分四层,适用单体拆分后的第一个独立微服务。
目录树
order-service/ # 独立微服务
├── Dockerfile
├── pom.xml
├── src/main/java/com/example/order/
│ ├── enterprise/ # Enterprise Business Rules
│ │ ├── entity/
│ │ │ └── Order.java
│ │ ├── vo/
│ │ │ ├── OrderId.java
│ │ │ ├── Money.java
│ │ │ └── OrderStatus.java
│ │ └── event/
│ │ ├── DomainEvent.java
│ │ └── OrderCreatedEvent.java
│ │
│ ├── usecase/ # Application Business Rules
│ │ ├── port/input/
│ │ │ ├── CreateOrderUseCase.java
│ │ │ └── QueryOrderUseCase.java
│ │ ├── port/output/
│ │ │ ├── OrderRepository.java
│ │ │ └── EventBus.java # 发布事件到 MQ
│ │ ├── dto/
│ │ │ ├── CreateOrderRequest.java
│ │ │ └── OrderDTO.java
│ │ └── interactor/
│ │ ├── CreateOrderInteractor.java
│ │ └── QueryOrderInteractor.java
│ │
│ ├── adapter/ # Interface Adapters
│ │ ├── controller/
│ │ │ └── OrderController.java
│ │ ├── repository/
│ │ │ ├── OrderJpaEntity.java
│ │ │ └── OrderRepositoryImpl.java
│ │ ├── messaging/
│ │ │ ├── KafkaEventBus.java
│ │ │ └── OrderEventConsumer.java # 消费其他服务事件
│ │ └── client/ # 外部服务调用适配器
│ │ └── InventoryServiceClient.java
│ │
│ └── framework/ # Frameworks & Drivers
│ └── config/
│ ├── OrderApplication.java
│ ├── UseCaseConfig.java
│ ├── PersistenceConfig.java
│ ├── KafkaConfig.java
│ └── FeignClientConfig.java
│
├── src/main/resources/
│ ├── application.yml
│ └── db/migration/ # Flyway 迁移脚本
│ └── V1__create_order_table.sql
│
└── src/test/java/com/example/order/
├── enterprise/entity/OrderTest.java
├── usecase/interactor/CreateOrderInteractorTest.java
├── adapter/repository/OrderRepositoryImplTest.java
├── integration/
│ └── OrderServiceIntegrationTest.java
└── architecture/
└── ArchitectureTest.java服务拓扑
┌──────────────┐
│ API Gateway │
└──────┬───────┘
│ HTTP/REST
┌────────────────┼────────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ order │ │ payment │ │ inventory │
│ service │ │ service │ │ service │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└────────────────┼────────────────┘
│ Async Events
┌──────▼───────┐
│ Kafka │
└──────────────┘
每个微服务内部使用整洁架构四层结构依赖方向(微服务内 + 微服务间)
微服务内(同 06-monolith-simple):
framework ──► adapter ──► usecase ──► enterprise微服务间:
order ◄──► payment (通过 REST API 同步调用 + Kafka 异步事件)
order ◄──► inventory (通过 REST API 同步调用 + Kafka 异步事件)
禁止:微服务间直接共享代码/实体,只通过 API Contract 通信微服务特有关注点
1. 远程调用适配器
// adapter/client/InventoryServiceClient.java
@Component
public class InventoryServiceClient implements ReserveInventoryPort {
private final InventoryFeignClient feignClient;
@Override
public ReserveResult reserve(ReserveCommand cmd) {
// 将领域命令转为 HTTP DTO
var request = InventoryReserveRequest.from(cmd);
var response = feignClient.reserve(request);
return response.toDomain(); // 转回领域对象
}
}2. 事件总线适配器
// adapter/messaging/KafkaEventBus.java
@Component
public class KafkaEventBus implements EventBus {
private final KafkaTemplate<String, DomainEvent> kafka;
@Override
public void publish(DomainEvent event) {
kafka.send("order-events", event.getAggregateId(), event);
}
}3. 分布式事务
- Saga 编排:由 UseCase Interactor 编排本地事务 + 补偿逻辑
- Outbox 模式:领域事件先写本地 outbox 表,再异步投递到 Kafka
- 幂等消费:消费者按 eventId 去重
适用场景
| 维度 | 说明 |
|---|---|
| 团队规模 | 4-10 人/服务 |
| 项目复杂度 | 1-2 个聚合根/服务,< 15 个 UseCase/服务 |
| 服务数 | 3-8 个微服务 |
| 通信方式 | REST(同步)+ Kafka(异步) |
| 部署方式 | Docker + K8s,独立部署 |
| 典型业务 | 从单体拆分出的核心领域微服务(订单/支付/库存) |
优缺点
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 独立部署、扩展、技术栈选择 | 分布式复杂度(网络、事务、一致性) |
| 整洁架构保证微服务内部质量 | 重复的四层结构模板代码 |
| 清晰的服务边界 | 需额外处理服务发现、配置中心 |
| 适合小团队独立交付 | 跨服务调试困难 |
10 — 微服务 Clean 架构(复杂版)
单微服务内含多聚合根 + 多 Interactor + 领域事件 + CQRS + Saga 编排。
目录树
order-service/
├── Dockerfile
├── pom.xml
├── src/main/java/com/example/order/
│ ├── shared/ # 微服务内共享内核
│ │ ├── domain/
│ │ │ ├── Identifier.java
│ │ │ ├── Money.java
│ │ │ └── DomainEvent.java
│ │ └── event/
│ │ └── EventPublisher.java
│ │
│ ├── order/ # 订单子域
│ │ ├── enterprise/
│ │ │ ├── entity/Order.java
│ │ │ ├── entity/OrderItem.java
│ │ │ ├── vo/OrderId.java
│ │ │ ├── vo/OrderStatus.java
│ │ │ └── event/
│ │ │ ├── OrderCreatedEvent.java
│ │ │ ├── OrderPaidEvent.java
│ │ │ └── OrderCancelledEvent.java
│ │ ├── usecase/
│ │ │ ├── port/input/
│ │ │ │ ├── CreateOrderUseCase.java
│ │ │ │ ├── PayOrderUseCase.java
│ │ │ │ └── CancelOrderUseCase.java
│ │ │ ├── port/output/
│ │ │ │ ├── OrderRepository.java
│ │ │ │ └── PaymentPort.java
│ │ │ ├── dto/
│ │ │ │ ├── CreateOrderRequest.java
│ │ │ │ └── OrderDTO.java
│ │ │ └── interactor/
│ │ │ ├── CreateOrderInteractor.java
│ │ │ └── PayOrderSagaInteractor.java # Saga 编排器
│ │ ├── adapter/
│ │ │ ├── controller/OrderController.java
│ │ │ ├── repository/
│ │ │ │ ├── OrderJpaEntity.java
│ │ │ │ └── OrderRepositoryImpl.java
│ │ │ └── messaging/
│ │ │ ├── OrderEventPublisher.java
│ │ │ └── PaymentEventConsumer.java
│ │ └── framework/
│ │ └── config/OrderDomainConfig.java
│ │
│ ├── fulfillment/ # 履约子域
│ │ ├── enterprise/
│ │ │ ├── entity/FulfillmentOrder.java
│ │ │ ├── vo/FulfillmentId.java
│ │ │ ├── vo/FulfillmentStatus.java
│ │ │ └── event/
│ │ │ └── FulfillmentStartedEvent.java
│ │ ├── usecase/
│ │ │ ├── port/input/
│ │ │ │ ├── StartFulfillmentUseCase.java
│ │ │ │ └── CompleteFulfillmentUseCase.java
│ │ │ ├── port/output/
│ │ │ │ └── FulfillmentRepository.java
│ │ │ └── interactor/
│ │ │ └── StartFulfillmentInteractor.java
│ │ └── adapter/repository/
│ │ └── FulfillmentRepositoryImpl.java
│ │
│ └── query/ # CQRS 查询端
│ ├── dto/
│ │ ├── OrderSummaryDTO.java
│ │ └── OrderDetailDTO.java
│ ├── port/
│ │ ├── QueryOrderUseCase.java
│ │ └── QueryFulfillmentUseCase.java
│ ├── interactor/
│ │ ├── QueryOrderInteractor.java
│ │ └── QueryFulfillmentInteractor.java
│ └── adapter/
│ ├── controller/
│ │ ├── OrderQueryController.java
│ │ └── FulfillmentQueryController.java
│ └── repository/
│ ├── OrderReadRepository.java
│ └── FulfillmentReadRepository.java
│
├── src/main/resources/
│ ├── application.yml
│ ├── application-kafka.yml
│ └── db/migration/
│ ├── V1__create_order_table.sql
│ ├── V2__create_outbox_table.sql
│ └── V3__create_fulfillment_table.sql
│
└── src/test/java/com/example/order/
├── order/usecase/interactor/
│ ├── CreateOrderInteractorTest.java
│ └── PayOrderSagaInteractorTest.java
├── integration/
│ ├── OrderSagaIntegrationTest.java
│ └── EventPublishingIntegrationTest.java
└── architecture/
└── ArchitectureTest.java核心模式
1. CQRS 分离
┌─────────────────────────────────────────────────┐
│ Order Service │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Command Side │ │ Query Side │ │
│ │ │ │ │ │
│ │ OrderController │ │ OrderQueryCtrl │ │
│ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ │ │
│ │ Interactor │ │ QueryInteractor │ │
│ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ │ │
│ │ OrderRepository │ │ OrderReadRepo │ │
│ │ (JPA Entity) │ │ (Native SQL) │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ 同一张 MySQL 表,但读写使用不同数据模型 │
└─────────────────────────────────────────────────┘2. Saga 编排
// order/usecase/interactor/PayOrderSagaInteractor.java
public class PayOrderSagaInteractor implements PayOrderUseCase {
@Override
public PayOrderResponse execute(PayOrderRequest request) {
// Step 1: 锁定库存(调用 inventory service)
var reserveResult = reserveInventoryPort.reserve(request);
if (reserveResult.isFailure()) return PayOrderResponse.failed();
// Step 2: 扣款(调用 payment service)
var paymentResult = paymentPort.charge(request);
if (paymentResult.isFailure()) {
// 补偿: 释放库存
releaseInventoryPort.release(request.getOrderId());
return PayOrderResponse.failed();
}
// Step 3: 更新订单状态
var order = orderRepository.findById(request.getOrderId());
order.markAsPaid();
orderRepository.save(order);
// Step 4: 发布事件
eventPublisher.publish(new OrderPaidEvent(order));
return PayOrderResponse.success(order);
}
}3. Outbox 模式(事件可靠性)
// Saga Interactor 发布事件时同步写 outbox
order.markAsPaid();
order.addEvent(new OrderPaidEvent(order)); // 添加到 outbox 列表
orderRepository.save(order); // 事务提交,事件落表
// OutboxScheduler 异步轮询投递到 Kafka
@Scheduled(fixedDelay = 1000)
public void publishOutboxEvents() {
var events = outboxRepository.findUnpublished(100);
events.forEach(e -> {
kafkaTemplate.send(e.getTopic(), e.getPayload());
outboxRepository.markAsPublished(e.getId());
});
}适用场景
| 维度 | 说明 |
|---|---|
| 团队规模 | 8-20 人/服务 |
| 项目复杂度 | 3-5 个聚合根/服务,15-40 个 UseCase/服务 |
| 架构模式 | CQRS + Saga + Outbox + Event-Driven |
| 通信方式 | REST(同步)+ Kafka(异步事件总线) |
| 典型业务 | 复杂电商订单、物流履约、保险理赔 |
优缺点
| ✅ 优点 | ❌ 缺点 |
|---|---|
| CQRS 读写分离,性能可独立优化 | 架构复杂度高,需团队有成熟 DDD 经验 |
| Saga 保证跨服务数据一致性 | 最终一致性增加业务复杂度 |
| 事件驱动解耦,可扩展性强 | Outbox 调度器增加运维负担 |
| 子域内整洁架构,子域间事件通信 | 需要可靠的消息基础设施 |
11 — 微服务 Clean 架构(多模块版)
单微服务按四层拆分为 Maven 模块,编译期强制依赖方向。
目录树
order-service/
├── pom.xml # 父 POM
├── Dockerfile
│
├── order-enterprise/ # 模块1: Enterprise
│ ├── pom.xml # 零框架依赖
│ └── src/main/java/com/example/order/enterprise/
│ ├── entity/Order.java
│ ├── entity/OrderItem.java
│ ├── vo/OrderId.java
│ ├── vo/Money.java
│ ├── vo/OrderStatus.java
│ ├── event/DomainEvent.java
│ └── event/OrderCreatedEvent.java
│
├── order-usecase/ # 模块2: Application
│ ├── pom.xml # 只依赖 order-enterprise
│ └── src/main/java/com/example/order/usecase/
│ ├── port/input/
│ │ ├── CreateOrderUseCase.java
│ │ └── QueryOrderUseCase.java
│ ├── port/output/
│ │ ├── OrderRepository.java
│ │ └── EventBus.java
│ ├── dto/
│ │ ├── CreateOrderRequest.java
│ │ └── CreateOrderResponse.java
│ └── interactor/
│ ├── CreateOrderInteractor.java
│ └── QueryOrderInteractor.java
│
├── order-adapter/ # 模块3: Adapters
│ ├── pom.xml # 依赖 usecase + enterprise + Spring
│ └── src/main/java/com/example/order/adapter/
│ ├── controller/OrderController.java
│ ├── repository/
│ │ ├── OrderJpaEntity.java
│ │ ├── OrderJpaRepository.java
│ │ └── OrderRepositoryImpl.java
│ └── messaging/
│ ├── KafkaEventBus.java
│ └── OrderEventConsumer.java
│
├── order-client/ # 模块4: 对外 API(可选)
│ ├── pom.xml # 只含 DTO,无内部依赖
│ └── src/main/java/com/example/order/client/
│ ├── dto/
│ │ ├── CreateOrderRequest.java
│ │ └── OrderResponse.java
│ └── api/
│ └── OrderServiceApi.java # Feign Interface
│
├── order-boot/ # 模块5: Boot & Config
│ ├── pom.xml # 依赖所有模块
│ └── src/main/java/com/example/order/boot/
│ ├── OrderApplication.java
│ └── config/
│ ├── UseCaseConfig.java
│ ├── PersistenceConfig.java
│ └── KafkaConfig.java
│
└── order-integration-test/ # 模块6: 集成测试
├── pom.xml # 依赖所有模块 (test scope)
└── src/test/java/com/example/order/it/
├── OrderServiceIntegrationTest.java
└── KafkaEventIntegrationTest.java模块依赖关系
┌──────────────────┐
│ order-client │ ← 对外暴露的 API 契约(纯 DTO + Feign Interface)
└────────┬─────────┘ 无内部依赖,可独立发布给消费者
│
┌────────▼─────────┐
│ order-boot │ ← Spring Boot 启动 + DI 配置
└────────┬─────────┘ 依赖: adapter, usecase, enterprise, client
│
┌────────▼─────────┐
│ order-adapter │ ← Controller, Repository Impl, Kafka Adapter
└────────┬─────────┘ 依赖: usecase, enterprise
│
┌────────▼─────────┐
│ order-usecase │ ← Interactor, Port 接口
└────────┬─────────┘ 依赖: enterprise
│
┌────────▼─────────┐
│ order-enterprise │ ← Entity, VO, Domain Event
└──────────────────┘ 零外部依赖Maven 模块职责矩阵
| 模块 | 分层 | Spring 依赖 | 可独立构建 | 可独立测试 |
|---|---|---|---|---|
order-enterprise | Enterprise | ❌ 无 | ✅ | ✅ 纯单元测试 |
order-usecase | Application | ❌ 无 | ✅ | ✅ Mock 端口 |
order-adapter | Interface Adapters | ✅ Web+JPA+Kafka | ✅ | ✅ Testcontainers |
order-client | 对外 API | ❌ 无 | ✅ | N/A |
order-boot | Frameworks | ✅ Spring Boot | ✅ | ✅ 集成测试 |
order-integration-test | 测试 | ✅ 所有 | ✅ | ✅ 全链路 |
微服务特有关系
client 模块:API 契约独立发布
order-service 发布时:
order-client-1.0.1.jar → Maven 仓库
payment-service 依赖:
<dependency>
<groupId>com.example</groupId>
<artifactId>order-client</artifactId>
<version>1.0.1</version>
</dependency>其他服务通过 order-client 获得类型安全的 Feign 接口,无需手写 HTTP 调用。
独立构建优化
# 只构建 usecase 层(不触发 adapter 的 JPA/Kafka 编译)
cd order-usecase && mvn test -pl .
# 全量构建(含集成测试)
mvn verify -pl order-integration-test适用场景
| 维度 | 说明 |
|---|---|
| 团队规模 | 8-20 人/服务,模块级独立开发 |
| 项目复杂度 | 2-4 个聚合根/服务,15-30 个 UseCase/服务 |
| 隔离要求 | 编译期强制四层隔离 + 模块间 API 契约化 |
| 发布策略 | 各模块独立版本,API 契约独立演进 |
| 典型业务 | 平台型微服务、多消费者 SaaS API |
优缺点
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 编译期绝对隔离,零腐化风险 | 6 个模块维护成本高 |
| API 契约独立发布,消费者解耦 | 模块间版本依赖管理复杂 |
| 各层独立编译、测试、发布 | 新人需要理解完整的模块图 |
| 天然适配 CI/CD 增量构建 | 小服务过度工程化风险 |
Enterprise Business Rules — Core Entities
Location: {project}-core/entity/
The Enterprise Business Rules layer (innermost) contains:
- Core entities with business behavior (rich domain model)
- Value objects that encapsulate concepts
- Domain exceptions for business rule violations
- Domain events representing meaningful business occurrences
Key Principle
Entities must be framework-free. No Spring/JPA/Jackson annotations. Pure Java/Kotlin/C#.
Entity Template
package com.example.core.entity;
import com.example.core.valueobject.Money;
import com.example.core.valueobject.OrderId;
import com.example.core.valueobject.OrderStatus;
import com.example.core.exception.OrderDomainException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* ★ Core Entity — zero framework dependencies.
* Enterprise Business Rules are completely isolated from
* frameworks, databases, and delivery mechanisms.
*/
public class Order {
private final OrderId id;
private Money totalAmount;
private OrderStatus status;
private final List<OrderItem> items;
private final List<DomainEvent> domainEvents;
// Constructor — always initialize to valid state
public Order(OrderId id, Money totalAmount) {
this.id = id;
this.totalAmount = totalAmount;
this.status = OrderStatus.DRAFT;
this.items = new ArrayList<>();
this.domainEvents = new ArrayList<>();
addDomainEvent(new OrderCreatedEvent(this.id, this.totalAmount));
}
// Business behavior, not just getters/setters
public void pay() {
if (!this.status.canTransitionTo(OrderStatus.PAID)) {
throw new OrderDomainException(
"Order " + id.value() + " cannot transition from "
+ status + " to " + OrderStatus.PAID);
}
this.status = OrderStatus.PAID;
addDomainEvent(new OrderPaidEvent(this.id));
}
public void cancel() {
if (!this.status.canTransitionTo(OrderStatus.CANCELLED)) {
throw new OrderDomainException("Order already " + status);
}
this.status = OrderStatus.CANCELLED;
addDomainEvent(new OrderCancelledEvent(this.id));
}
public void addItem(OrderItem item) {
this.items.add(item);
this.totalAmount = this.totalAmount.add(item.subtotal());
}
public Money calculateTotal() {
return items.stream()
.map(OrderItem::subtotal)
.reduce(Money.ZERO, Money::add);
}
// Getters without setters — immutability by design
public OrderId id() { return id; }
public Money totalAmount() { return totalAmount; }
public OrderStatus status() { return status; }
public List<OrderItem> items() { return Collections.unmodifiableList(items); }
// Domain events for side effects
public List<DomainEvent> domainEvents() {
return Collections.unmodifiableList(domainEvents);
}
public void clearEvents() { domainEvents.clear(); }
private void addDomainEvent(DomainEvent event) {
domainEvents.add(event);
}
}Value Object Template
package com.example.core.valueobject;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Currency;
import java.util.Objects;
/**
* ★ Value Object — immutable by definition.
* Two Money objects with same amount+currency are equal.
*/
public final class Money {
public static final Money ZERO = new Money(BigDecimal.ZERO, Currency.getInstance("USD"));
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
this.amount = amount.setScale(2, RoundingMode.HALF_UP);
this.currency = Objects.requireNonNull(currency);
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money subtract(Money other) { /* ... */ }
public Money multiply(int factor) { /* ... */ }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.compareTo(money.amount) == 0
&& currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount.stripTrailingZeros(), currency);
}
public BigDecimal amount() { return amount; }
public Currency currency() { return currency; }
}Domain Exception Template
package com.example.core.exception;
/**
* Base domain exception — all enterprise rule violations
* use this or its subclasses.
*/
public class DomainException extends RuntimeException {
public DomainException(String message) {
super(message);
}
}
public class OrderDomainException extends DomainException {
public OrderDomainException(String message) {
super(message);
}
}
public class BusinessRuleViolation extends DomainException {
public BusinessRuleViolation(String rule, String detail) {
super("Business rule violated: " + rule + " — " + detail);
}
}Domain Event Templates
package com.example.core.event;
import java.time.Instant;
import java.util.UUID;
public abstract class DomainEvent {
private final UUID eventId;
private final Instant occurredOn;
protected DomainEvent() {
this.eventId = UUID.randomUUID();
this.occurredOn = Instant.now();
}
public UUID eventId() { return eventId; }
public Instant occurredOn() { return occurredOn; }
}
public class OrderCreatedEvent extends DomainEvent {
private final OrderId orderId;
private final Money totalAmount;
public OrderCreatedEvent(OrderId orderId, Money totalAmount) {
this.orderId = orderId;
this.totalAmount = totalAmount;
}
public OrderId orderId() { return orderId; }
public Money totalAmount() { return totalAmount; }
}Testing
class OrderTest {
@Test
void shouldCreateOrder() {
var order = new Order(new OrderId("ORD-001"), Money.ZERO);
assertThat(order.status()).isEqualTo(OrderStatus.DRAFT);
assertThat(order.domainEvents()).hasSize(1);
}
@Test
void shouldPayOrder() {
var order = new Order(new OrderId("ORD-001"), new Money(new BigDecimal("99.00")));
order.pay();
assertThat(order.status()).isEqualTo(OrderStatus.PAID);
}
@Test
void shouldNotPayCancelledOrder() {
var order = new Order(new OrderId("ORD-001"), Money.ZERO);
order.cancel();
assertThatThrownBy(() -> order.pay())
.isInstanceOf(OrderDomainException.class);
}
}