hexagonal-architecture
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHexagonal Architecture
六边形架构
Hexagonal architecture (Ports and Adapters) keeps business logic independent from frameworks, transport, and persistence details. The core app depends on abstract ports, and adapters implement those ports at the edges.
六边形架构(Hexagonal Architecture,又称端口与适配器架构)可让业务逻辑与框架、传输层和持久化细节解耦。应用核心依赖抽象端口,适配器则在架构边缘实现这些端口。
When to Use
适用场景
- Building new features where long-term maintainability and testability matter.
- Refactoring layered or framework-heavy code where domain logic is mixed with I/O concerns.
- Supporting multiple interfaces for the same use case (HTTP, CLI, queue workers, cron jobs).
- Replacing infrastructure (database, external APIs, message bus) without rewriting business rules.
Use this skill when the request involves boundaries, domain-centric design, refactoring tightly coupled services, or decoupling application logic from specific libraries.
- 构建需要长期可维护性和可测试性的新功能
- 重构领域逻辑与I/O逻辑混杂的分层代码或重度依赖框架的代码
- 为同一个用例支持多种接入方式(HTTP、CLI、队列消费者、定时任务)
- 无需重写业务规则即可替换基础设施(数据库、外部API、消息总线)
当需求涉及边界划分、领域中心设计、重构紧耦合服务,或者将应用逻辑与特定库解耦时,可以使用本方案。
Core Concepts
核心概念
- Domain model: Business rules and entities/value objects. No framework imports.
- Use cases (application layer): Orchestrate domain behavior and workflow steps.
- Inbound ports: Contracts describing what the application can do (commands/queries/use-case interfaces).
- Outbound ports: Contracts for dependencies the application needs (repositories, gateways, event publishers, clock, UUID, etc.).
- Adapters: Infrastructure and delivery implementations of ports (HTTP controllers, DB repositories, queue consumers, SDK wrappers).
- Composition root: Single wiring location where concrete adapters are bound to use cases.
Outbound port interfaces usually live in the application layer (or in domain only when the abstraction is truly domain-level), while infrastructure adapters implement them.
Dependency direction is always inward:
- Adapters -> application/domain
- Application -> port interfaces (inbound/outbound contracts)
- Domain -> domain-only abstractions (no framework or infrastructure dependencies)
- Domain -> nothing external
- 领域模型(Domain model):业务规则和实体/值对象,不引入任何框架依赖
- 用例(应用层,Use cases):编排领域行为和工作流步骤
- 入站端口(Inbound ports):描述应用能力的契约(命令/查询/用例接口)
- 出站端口(Outbound ports):应用所需依赖的契约(仓库、网关、事件发布器、时钟、UUID生成器等)
- 适配器(Adapters):端口的基础设施和交付实现(HTTP控制器、数据库仓库、队列消费者、SDK封装器)
- 组合根(Composition root):将具体适配器绑定到用例的统一装配位置
出站端口接口通常存放在应用层(只有当抽象确实属于领域层级时才放在领域层),由基础设施层的适配器实现。
依赖方向始终向内:
- 适配器 -> 应用层/领域层
- 应用层 -> 端口接口(入站/出站契约)
- 领域层 -> 仅领域内的抽象(无框架或基础设施依赖)
- 领域层不依赖任何外部内容
How It Works
工作原理
Step 1: Model a use case boundary
步骤1:建模用例边界
Define a single use case with a clear input and output DTO. Keep transport details (Express , GraphQL , job payload wrappers) outside this boundary.
reqcontext定义单个用例的清晰输入输出DTO,将传输层细节(Express的、GraphQL的、任务负载封装器)排除在边界之外。
reqcontextStep 2: Define outbound ports first
步骤2:优先定义出站端口
Identify every side effect as a port:
- persistence ()
UserRepositoryPort - external calls ()
BillingGatewayPort - cross-cutting (,
LoggerPort)ClockPort
Ports should model capabilities, not technologies.
将每个副作用都抽象为端口:
- 持久化()
UserRepositoryPort - 外部调用()
BillingGatewayPort - 横切关注点(、
LoggerPort)ClockPort
端口应该建模能力,而非具体技术。
Step 3: Implement the use case with pure orchestration
步骤3:用纯编排逻辑实现用例
Use case class/function receives ports via constructor/arguments. It validates application-level invariants, coordinates domain rules, and returns plain data structures.
用例类/函数通过构造函数/参数接收端口依赖,验证应用层 invariants,协调领域规则,返回纯数据结构。
Step 4: Build adapters at the edge
步骤4:在边缘构建适配器
- Inbound adapter converts protocol input to use-case input.
- Outbound adapter maps app contracts to concrete APIs/ORM/query builders.
- Mapping stays in adapters, not inside use cases.
- 入站适配器将协议输入转换为用例输入
- 出站适配器将应用契约映射到具体的API/ORM/查询构造器
- 映射逻辑保留在适配器中,不要放入用例内部
Step 5: Wire everything in a composition root
步骤5:在组合根中装配所有组件
Instantiate adapters, then inject them into use cases. Keep this wiring centralized to avoid hidden service-locator behavior.
实例化适配器,然后注入到用例中。保持装配逻辑集中,避免隐藏的服务定位器行为。
Step 6: Test per boundary
步骤6:按边界分层测试
- Unit test use cases with fake ports.
- Integration test adapters with real infra dependencies.
- E2E test user-facing flows through inbound adapters.
- 用假端口对用例做单元测试
- 用真实基础设施依赖对适配器做集成测试
- 通过入站适配器对面向用户的流程做E2E测试
Architecture Diagram
架构图
mermaid
flowchart LR
Client["Client (HTTP/CLI/Worker)"] --> InboundAdapter["Inbound Adapter"]
InboundAdapter -->|"calls"| UseCase["UseCase (Application Layer)"]
UseCase -->|"uses"| OutboundPort["OutboundPort (Interface)"]
OutboundAdapter["Outbound Adapter"] -->|"implements"| OutboundPort
OutboundAdapter --> ExternalSystem["DB/API/Queue"]
UseCase --> DomainModel["DomainModel"]mermaid
flowchart LR
Client["Client (HTTP/CLI/Worker)"] --> InboundAdapter["Inbound Adapter"]
InboundAdapter -->|"calls"| UseCase["UseCase (Application Layer)"]
UseCase -->|"uses"| OutboundPort["OutboundPort (Interface)"]
OutboundAdapter["Outbound Adapter"] -->|"implements"| OutboundPort
OutboundAdapter --> ExternalSystem["DB/API/Queue"]
UseCase --> DomainModel["DomainModel"]Suggested Module Layout
推荐模块结构
Use feature-first organization with explicit boundaries:
text
src/
features/
orders/
domain/
Order.ts
OrderPolicy.ts
application/
ports/
inbound/
CreateOrder.ts
outbound/
OrderRepositoryPort.ts
PaymentGatewayPort.ts
use-cases/
CreateOrderUseCase.ts
adapters/
inbound/
http/
createOrderRoute.ts
outbound/
postgres/
PostgresOrderRepository.ts
stripe/
StripePaymentGateway.ts
composition/
ordersContainer.ts使用按功能优先的组织方式,边界清晰:
text
src/
features/
orders/
domain/
Order.ts
OrderPolicy.ts
application/
ports/
inbound/
CreateOrder.ts
outbound/
OrderRepositoryPort.ts
PaymentGatewayPort.ts
use-cases/
CreateOrderUseCase.ts
adapters/
inbound/
http/
createOrderRoute.ts
outbound/
postgres/
PostgresOrderRepository.ts
stripe/
StripePaymentGateway.ts
composition/
ordersContainer.tsTypeScript Example
TypeScript示例
Port definitions
端口定义
typescript
export interface OrderRepositoryPort {
save(order: Order): Promise<void>;
findById(orderId: string): Promise<Order | null>;
}
export interface PaymentGatewayPort {
authorize(input: { orderId: string; amountCents: number }): Promise<{ authorizationId: string }>;
}typescript
export interface OrderRepositoryPort {
save(order: Order): Promise<void>;
findById(orderId: string): Promise<Order | null>;
}
export interface PaymentGatewayPort {
authorize(input: { orderId: string; amountCents: number }): Promise<{ authorizationId: string }>;
}Use case
用例
typescript
type CreateOrderInput = {
orderId: string;
amountCents: number;
};
type CreateOrderOutput = {
orderId: string;
authorizationId: string;
};
export class CreateOrderUseCase {
constructor(
private readonly orderRepository: OrderRepositoryPort,
private readonly paymentGateway: PaymentGatewayPort
) {}
async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
const order = Order.create({ id: input.orderId, amountCents: input.amountCents });
const auth = await this.paymentGateway.authorize({
orderId: order.id,
amountCents: order.amountCents,
});
// markAuthorized returns a new Order instance; it does not mutate in place.
const authorizedOrder = order.markAuthorized(auth.authorizationId);
await this.orderRepository.save(authorizedOrder);
return {
orderId: order.id,
authorizationId: auth.authorizationId,
};
}
}typescript
type CreateOrderInput = {
orderId: string;
amountCents: number;
};
type CreateOrderOutput = {
orderId: string;
authorizationId: string;
};
export class CreateOrderUseCase {
constructor(
private readonly orderRepository: OrderRepositoryPort,
private readonly paymentGateway: PaymentGatewayPort
) {}
async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
const order = Order.create({ id: input.orderId, amountCents: input.amountCents });
const auth = await this.paymentGateway.authorize({
orderId: order.id,
amountCents: order.amountCents,
});
// markAuthorized returns a new Order instance; it does not mutate in place.
const authorizedOrder = order.markAuthorized(auth.authorizationId);
await this.orderRepository.save(authorizedOrder);
return {
orderId: order.id,
authorizationId: auth.authorizationId,
};
}
}Outbound adapter
出站适配器
typescript
export class PostgresOrderRepository implements OrderRepositoryPort {
constructor(private readonly db: SqlClient) {}
async save(order: Order): Promise<void> {
await this.db.query(
"insert into orders (id, amount_cents, status, authorization_id) values ($1, $2, $3, $4)",
[order.id, order.amountCents, order.status, order.authorizationId]
);
}
async findById(orderId: string): Promise<Order | null> {
const row = await this.db.oneOrNone("select * from orders where id = $1", [orderId]);
return row ? Order.rehydrate(row) : null;
}
}typescript
export class PostgresOrderRepository implements OrderRepositoryPort {
constructor(private readonly db: SqlClient) {}
async save(order: Order): Promise<void> {
await this.db.query(
"insert into orders (id, amount_cents, status, authorization_id) values ($1, $2, $3, $4)",
[order.id, order.amountCents, order.status, order.authorizationId]
);
}
async findById(orderId: string): Promise<Order | null> {
const row = await this.db.oneOrNone("select * from orders where id = $1", [orderId]);
return row ? Order.rehydrate(row) : null;
}
}Composition root
组合根
typescript
export const buildCreateOrderUseCase = (deps: { db: SqlClient; stripe: StripeClient }) => {
const orderRepository = new PostgresOrderRepository(deps.db);
const paymentGateway = new StripePaymentGateway(deps.stripe);
return new CreateOrderUseCase(orderRepository, paymentGateway);
};typescript
export const buildCreateOrderUseCase = (deps: { db: SqlClient; stripe: StripeClient }) => {
const orderRepository = new PostgresOrderRepository(deps.db);
const paymentGateway = new StripePaymentGateway(deps.stripe);
return new CreateOrderUseCase(orderRepository, paymentGateway);
};Multi-Language Mapping
多语言适配
Use the same boundary rules across ecosystems; only syntax and wiring style change.
- TypeScript/JavaScript
- Ports: as interfaces/types.
application/ports/* - Use cases: classes/functions with constructor/argument injection.
- Adapters: ,
adapters/inbound/*.adapters/outbound/* - Composition: explicit factory/container module (no hidden globals).
- Ports:
- Java
- Packages: ,
domain,application.port.in,application.port.out,application.usecase,adapter.in.adapter.out - Ports: interfaces in .
application.port.* - Use cases: plain classes (Spring is optional, not required).
@Service - Composition: Spring config or manual wiring class; keep wiring out of domain/use-case classes.
- Packages:
- Kotlin
- Modules/packages mirror the Java split (,
domain,application.port,application.usecase).adapter - Ports: Kotlin interfaces.
- Use cases: classes with constructor injection (Koin/Dagger/Spring/manual).
- Composition: module definitions or dedicated composition functions; avoid service locator patterns.
- Modules/packages mirror the Java split (
- Go
- Packages: ,
internal/<feature>/domain,application,ports,adapters/inbound.adapters/outbound - Ports: small interfaces owned by the consuming application package.
- Use cases: structs with interface fields plus explicit constructors.
New... - Composition: wire in (or dedicated wiring package), keep constructors explicit.
cmd/<app>/main.go
- Packages:
在不同技术栈中使用相同的边界规则,仅语法和装配风格不同:
- TypeScript/JavaScript
- 端口:路径下的接口/类型
application/ports/* - 用例:通过构造函数/参数注入依赖的类/函数
- 适配器:、
adapters/inbound/*路径下的实现adapters/outbound/* - 装配:显式的工厂/容器模块(无隐藏全局变量)
- 端口:
- Java
- 包结构:、
domain、application.port.in、application.port.out、application.usecase、adapter.inadapter.out - 端口:包下的接口
application.port.* - 用例:普通类(可选使用Spring ,非必需)
@Service - 装配:Spring配置或手动装配类,避免在领域/用例类中添加装配逻辑
- 包结构:
- Kotlin
- 模块/包结构与Java划分一致(、
domain、application.port、application.usecase)adapter - 端口:Kotlin接口
- 用例:带构造函数注入的类(Koin/Dagger/Spring/手动注入均可)
- 装配:模块定义或专用装配函数,避免使用服务定位器模式
- 模块/包结构与Java划分一致(
- Go
- 包结构:、
internal/<feature>/domain、application、ports、adapters/inboundadapters/outbound - 端口:由消费方应用包持有的小接口
- 用例:带接口字段和显式构造函数的结构体
New... - 装配:在(或专用装配包)中完成,保持构造函数显式
cmd/<app>/main.go
- 包结构:
Anti-Patterns to Avoid
需要避免的反模式
- Domain entities importing ORM models, web framework types, or SDK clients.
- Use cases reading directly from ,
req, or queue metadata.res - Returning database rows directly from use cases without domain/application mapping.
- Letting adapters call each other directly instead of flowing through use-case ports.
- Spreading dependency wiring across many files with hidden global singletons.
- 领域实体引入ORM模型、Web框架类型或SDK客户端依赖
- 用例直接读取、
req或队列元数据res - 用例不做领域/应用层映射直接返回数据库行
- 适配器之间直接调用,而非通过用例端口流转
- 依赖装配分散在多个文件中,使用隐藏的全局单例
Migration Playbook
迁移指南
- Pick one vertical slice (single endpoint/job) with frequent change pain.
- Extract a use-case boundary with explicit input/output types.
- Introduce outbound ports around existing infrastructure calls.
- Move orchestration logic from controllers/services into the use case.
- Keep old adapters, but make them delegate to the new use case.
- Add tests around the new boundary (unit + adapter integration).
- Repeat slice-by-slice; avoid full rewrites.
- 选择一个变更频繁、痛点明显的垂直切片(单个接口/任务)
- 提取用例边界,定义显式的输入输出类型
- 围绕现有基础设施调用引入出站端口
- 将编排逻辑从控制器/服务迁移到用例中
- 保留旧适配器,让它们委托给新的用例实现
- 围绕新边界添加测试(单元测试+适配器集成测试)
- 逐切片重复操作,避免全量重写
Refactoring Existing Systems
重构现有系统
- Strangler approach: keep current endpoints, route one use case at a time through new ports/adapters.
- No big-bang rewrites: migrate per feature slice and preserve behavior with characterization tests.
- Facade first: wrap legacy services behind outbound ports before replacing internals.
- Composition freeze: centralize wiring early so new dependencies do not leak into domain/use-case layers.
- Slice selection rule: prioritize high-churn, low-blast-radius flows first.
- Rollback path: keep a reversible toggle or route switch per migrated slice until production behavior is verified.
- 绞杀者模式:保留现有端点,每次将一个用例路由到新的端口/适配器实现
- 避免大规模重写:按功能切片迁移,通过特性测试保留原有行为
- 门面优先:在替换内部实现前,先将遗留服务包装到出站端口后
- 装配冻结:尽早集中装配逻辑,避免新依赖泄露到领域/用例层
- 切片选择规则:优先迁移高变更频率、低影响范围的流程
- 回滚方案:每个迁移的切片保留可逆开关或路由切换能力,直到生产行为验证通过
Testing Guidance (Same Hexagonal Boundaries)
测试指导(遵循相同的六边形边界)
- Domain tests: test entities/value objects as pure business rules (no mocks, no framework setup).
- Use-case unit tests: test orchestration with fakes/stubs for outbound ports; assert business outcomes and port interactions.
- Outbound adapter contract tests: define shared contract suites at port level and run them against each adapter implementation.
- Inbound adapter tests: verify protocol mapping (HTTP/CLI/queue payload to use-case input and output/error mapping back to protocol).
- Adapter integration tests: run against real infrastructure (DB/API/queue) for serialization, schema/query behavior, retries, and timeouts.
- End-to-end tests: cover critical user journeys through inbound adapter -> use case -> outbound adapter.
- Refactor safety: add characterization tests before extraction; keep them until new boundary behavior is stable and equivalent.
- 领域测试:将实体/值对象作为纯业务规则测试(无需mock,无需框架初始化)
- 用例单元测试:使用假实现/桩模拟出站端口测试编排逻辑,断言业务结果和端口交互
- 出站适配器契约测试:在端口级别定义共享契约套件,针对每个适配器实现运行测试
- 入站适配器测试:验证协议映射(HTTP/CLI/队列负载到用例输入的转换,以及输出/错误到协议的反向映射)
- 适配器集成测试:对接真实基础设施(数据库/API/队列)测试序列化、Schema/查询行为、重试和超时逻辑
- 端到端测试:覆盖核心用户路径,完整走通入站适配器 -> 用例 -> 出站适配器的流程
- 重构安全保障:提取逻辑前添加特性测试,直到新边界行为稳定且与原有逻辑等价后再移除
Best Practices Checklist
最佳实践检查清单
- Domain and use-case layers import only internal types and ports.
- Every external dependency is represented by an outbound port.
- Validation occurs at boundaries (inbound adapter + use-case invariants).
- Use immutable transformations (return new values/entities instead of mutating shared state).
- Errors are translated across boundaries (infra errors -> application/domain errors).
- Composition root is explicit and easy to audit.
- Use cases are testable with simple in-memory fakes for ports.
- Refactoring starts from one vertical slice with behavior-preserving tests.
- Language/framework specifics stay in adapters, never in domain rules.
- 领域和用例层仅导入内部类型和端口
- 每个外部依赖都对应一个出站端口
- 校验逻辑在边界处执行(入站适配器+用例invariants校验)
- 使用不可变转换(返回新值/实体,而非修改共享状态)
- 错误在边界间转换(基础设施错误 -> 应用/领域错误)
- 组合根逻辑显式,易于审计
- 用例可通过简单的内存假端口实现测试
- 重构从单个垂直切片开始,配合行为保留测试
- 语言/框架特定逻辑保留在适配器中,绝不侵入领域规则