Alistair Cockburn (2005) — Business logic is isolated through Ports, and external systems are connected through Adapters.Target Audience: Backend architects/technical leads (team > 5 people), DDD practitioners.Customization Mechanism: Provides three configuration dimensions: tech stack/entry type/team size. When loading this skill, it is recommended to first read Workflow and When to Use to confirm the match.
"Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases." — Alistair Cockburn
Workflow
Step 1: Primary adapter protocol conversion — REST Controller/CLI receives requests, completes parameter validation and protocol conversion
Step 2: Inbound port invocation — The adapter calls the UseCase interface, triggering application-layer use case orchestration
Step 3: Application service orchestration — Application Service orchestrates use cases and calls domain models to execute business logic
Step 4: Domain model execution — Aggregate roots/entities execute business logic and output results or domain events
Step 5: Outbound port persistence — The application service saves domain state through the Repository interface
Step 6: Secondary adapter technical implementation — The Adapter performs the technical implementation of DB/MQ/external API
Definition Verification Method: All domain-layer unit tests can run without a database and HTTP → boundary is correct.
Rules
- Dependency Rule: The dependency direction must be from outside to inside—Adapter → Application → Domain. The Domain layer has zero external dependencies.
- Port Granularity Rule: Each port has a single responsibility; a port containing 10+ methods must be split immediately.
- Exception Conversion Rule: Technical exceptions (SQLException, TimeoutException) must be caught inside the Adapter and converted into domain exceptions, and must not leak upward.
When to Use
✅ Suitable Scenarios
- Multi-entry systems(REST + CLI + MQ + gRPC): Multiple external systems drive the same business logic simultaneously
- Frequent infrastructure changes(switching DB/MQ/cache): Only the Adapter implementation needs to be replaced, with zero modifications to the Domain
- Ultimate testability: Mocking ports allows testing all domain logic without depending on a database
- Microservice architecture standardization: Team > 5 people, requiring consistent architectural conventions across services
- The team is familiar with abstract design: Able to design port granularity reasonably and avoid over-abstraction
Boundary
⚠️ Conditions Required
- The team understands abstract design: Port granularity requires domain knowledge and abstraction ability; otherwise it may be defined improperly
- Project size is moderate: For small projects, Layered is simpler; hexagonal adds indirect-layer cost
- Requires DDD domain model cooperation: Defining Port interfaces requires prior knowledge of domain modeling
- Good DI container support: Frameworks such as Spring/Guice can simplify adapter assembly
❌ Not Applicable (with Alternatives)
- Single REST API + simple CRUD → Use
ddd-architecture-layered - Chinese enterprise MyBatis ecosystem → Use
ddd-architecture-cola - Requires strict separation of UseCase + Entity → Use
ddd-architecture-clean - Rapid prototyping / MVP stage → Directly use traditional three layers (without introducing the overhead of hexagonal architecture)
Core Principles
Three Major Abstractions
Driving Side (REST/CLI/gRPC/Test) → Domain Core ← Driven Side (PostgreSQL/RabbitMQ/Redis/Stripe)
| Concept | Description | Code Representation |
|---|---|---|
| Port | Interfaces defined by the domain layer | interface OrderRepository |
| Primary Adapter | How the outside drives the system | REST Controller、CLI Command、gRPC Service |
| Secondary Adapter | How the system drives the outside | JPA RepositoryImpl、Kafka Producer、StripeClient |
Strong Port vs Weak Port
// ❌ Weak — 泄漏 SQL 概念
interface OrderRepository { List<Order> findByQuery(String sql, Map<String, Object> params); }
// ✅ Strong — 纯领域概念
interface OrderRepository {
Optional<Order> findById(OrderId id);
List<Order> findByCustomerId(CustomerId customerId);
void save(Order order);
}
Directory Structure
{project}/
├── {project}-domain/ # 领域核心 + 端口定义(零框架依赖)
│ └── port/{inbound, outbound}/ # ★ 端口接口
├── {project}-application/ # 应用层(UseCase 实现)
├── {project}-adapter/ # 适配器层
│ ├── inbound/ # ★ 主适配器(REST/CLI/gRPC/MQ)
│ └── outbound/ # ★ 次适配器(Persistence/Messaging/External)
└── {project}-configuration/ # 配置层(DI 装配)
Development Standards
Layer Responsibilities
| Layer | Dependencies | Allowed Actions | Prohibited Actions |
|---|---|---|---|
| Domain | None | Entity behavior, value objects, domain events, port definitions | import framework annotations, SQL, HTTP |
| Application | Domain | Use case orchestration, transaction management, port invocation | if/else business decisions, directly operating the DB |
| Adapter | Application | Protocol conversion, parameter validation, exception mapping | Containing business logic, directly manipulating Domain internal state |
| Configuration | All | DI assembly, Profile configuration | Containing business code |
Coding Standards
// 入站端口(Domain)
public interface CreateOrderUseCase { OrderCreatedResult execute(CreateOrderCommand command); }
// 出站端口(Domain)
public interface OrderRepository { void save(Order order); Optional<Order> findById(OrderId id); }
// 应用服务(实现入站端口,注入出站端口)
@ApplicationService
public class CreateOrderService implements CreateOrderUseCase {
private final OrderRepository orderRepository;
@Override @Transactional
public OrderCreatedResult execute(CreateOrderCommand command) {
Order order = Order.create(command.getCustomerId());
orderRepository.save(order);
return OrderCreatedResult.from(order);
}
}
// 主适配器(仅协议转换)
@RestController
public class OrderController {
@PostMapping("/orders")
public ResponseEntity<CreateOrderResponse> create(@RequestBody @Valid CreateOrderRequest req) {
var result = createOrderUseCase.execute(req.toCommand());
return ResponseEntity.status(201).body(CreateOrderResponse.from(result));
}
}
// 次适配器(技术实现)
@Repository
public class PostgresOrderRepository implements OrderRepository {
@Override public void save(Order order) { jpaRepo.save(mapper.toPO(order)); }
}
Implementation Steps
Phase 1: 定义端口(1-2 天)→ 入站端口(UseCase) → 出站端口(Repository/Gateway)
Phase 2: 领域模型(2-3 天)→ 聚合根/实体/值对象 → 领域服务 → 领域事件
Phase 3: 应用服务(1-2 天)→ UseCase 实现(注入端口,编排调用)
Phase 4: 适配器(2-4 天) → 主适配器(REST/gRPC/CLI) → 次适配器(DB/MQ/External)
Phase 5: DI 装配 + 测试(1-2 天)→ DI 配置 → 端口 Mock 测试 → 适配器集成测试
Security & Stability
- This skill is a pure documentation-based architecture guide,does not collect, process, or upload user data。
- All code templates are for teaching reference. Replace external service URLs, keys, and credentials with environment variable configuration.
- Port/Adapter isolation ensures that domain logic does not directly depend on HTTP/DB/MQ libraries—the Adapter layer handles all I/O.
- When implementing Secondary Adapters, always set connection/read timeouts, and implement the circuit breaker pattern for critical paths.
Gotchas (8 Common Pitfalls)
- Port definitions are too broad: Each port does only one thing. A port containing 10+ methods is considered a "god port" and should be split immediately.
- Primary adapter contains business logic: A Controller can only perform parameter conversion and call the UseCase; it must not contain if/else business decisions.
- Secondary adapter forgets exception conversion: Technical exceptions (SQLException, TimeoutException) must be converted into domain exceptions inside the adapter.
- Domain layer depends on frameworks: The Domain module must not import Spring/JPA/MyBatis annotations.
- Inconsistent port naming: Inbound ports are named with actions (
CreateOrderUseCase), outbound ports are named with resources (OrderRepository)。 - Transactions placed in the adapter layer: Transactions are controlled by the application layer; adapters are not responsible for transaction management.
- Over-abstraction: Do not create ports in advance for "might change in the future." Extract ports only when replacement is actually needed.
- When user requirements are unclear, give assumptions first: When the user has not specified the tech stack/entry type/team size, give an assumed version first, then ask follow-up questions.
FAQ (8 Common Questions)
Q1: What is the difference between hexagonal and clean architecture? A: Hexagonal was proposed by Cockburn (2005), with ports/adapters at its core; Clean was proposed by Uncle Bob (2012), emphasizing the separation of UseCase and Entity. Hexagonal focuses more on "symmetry" (primary/secondary adapters), while Clean focuses more on the "dependency rule."
Q2: Should ports be placed in the Domain layer or the Application layer? A: It is recommended to place them in the Domain layer. The Domain is the business core, and ports are abstractions of the business's external dependencies.
Q3: How can excessive abstraction of ports be avoided? A: YAGNI principle: define ports only for currently confirmed change requirements.
Q4: Should a UseCase interface have one method or multiple methods? A: It is recommended to have one method per interface (Interface Segregation Principle). However, highly related methods (such as save/findById in OrderRepository) can be placed in the same interface.
Q5: Can DTOs in the Controller layer and Entities in the Domain layer be shared? A: No. DTOs are data carriers for the adapter layer, while Entities are business models of the domain layer, and Mapper conversion is required.
Q6: How does hexagonal architecture handle queries? A: Queries are also performed through ports. Use QueryOrderUseCase to return read-only DTOs; complex queries can use CQRS.
Q7: How can you verify whether the hexagonal boundaries are correct? A: Can all unit tests of the Domain layer be run without starting a database and HTTP? If yes → correct. If no → there are leaks.
Q8: What should be done if the team is unfamiliar with hexagonal architecture? A: Introduce it gradually. First ensure the purity of the Domain layer and the definition of ports, then gradually extract adapters.
References
| File | Purpose |
|---|---|
| references/01-port-definitions.md | Port Definition Specification — inbound/outbound ports, naming, granularity, Strong/Weak |
| references/02-primary-adapters.md | Primary Adapter Details — implementations of four adapters: REST/CLI/gRPC/MQ |
| references/03-secondary-adapters.md | Secondary Adapter Details — JPA/MyBatis/Stripe/RabbitMQ/InMemory |
| references/04-domain-model.md | Domain Model Design — aggregate roots, entities, value objects, domain services, domain events |
| references/05-application-services.md | Application Service Layer — UseCase implementation, orchestration specifications, command objects |
| references/06-di-configuration.md | DI Configuration — Spring Config, Profile switching, multi-environment adapters |
| references/07-testing.md | Testing Strategy — four-level testing: domain layer/application layer/adapter/architecture |
| references/08-migration.md | Incremental Migration — migration guide from traditional three-tier to hexagonal |
| examples/01-order-hexagonal-complete.md | Complete Order Hexagonal Example — full flow of Domain/Port/Service/Adapter |
| examples/02-user-registration-example.md | User Registration Example — value objects, verification codes, domain events |
| examples/03-multi-entry-points-example.md | Multi-Entry System Example — the same UseCase invoked by REST/CLI/Kafka/gRPC |
| examples/04-adapter-swapping-example.md | Adapter Replaceability Example — Postgres→MongoDB, Stripe→PayPal with zero code changes |
| examples/05-port-swapping-test.md | Primary Adapter Replaceability Example — three entry points REST/gRPC/CLI + port-level testing |
| examples/06-monolith-simple.md | Monolith Simple Hexagonal — single-module four packages + ports/adapters subpackage structure |
| examples/07-monolith-complex.md | Monolith Complex Hexagonal — multiple ports + multiple adapters + multiple aggregate roots |
| examples/08-monolith-multi-module.md | Monolith Multi-Module Hexagonal — Maven multi-module compile-time boundary constraints |
| examples/09-microservice-simple.md | Microservice Simple Hexagonal — minimal hexagonal structure within a single microservice |
| examples/10-microservice-complex.md | Microservice Complex Hexagonal — multiple aggregates + Saga + cross-service calls |
| examples/11-microservice-multi-module.md | Microservice Multi-Module Hexagonal — Maven multi-module inside each service |
| examples/12-microservice-complex-multi.md | Microservice Complex Multi-Module — CQRS + read-write separation + CDC + distributed Saga |
Primary Sources
- Hexagonal Architecture — Alistair Cockburn (2005)
- Hexagonal Architecture Explained — Cockburn & Garrido de Paz (2024)
- Domain-Driven Design: The Blue Book — Eric Evans (2003)
- AWS: Hexagonal Architecture Pattern
Navigation
- → Next: hand off to the
ddd-domain-designerskill — design domain models for hexagonal architecture. Install:npx skills add full-stack-skills/ddd-skills --skill ddd-domain-designer. - 🔗 Related:
ddd-testing-strategist— port mock testing (Install:npx skills add full-stack-skills/ddd-skills --skill ddd-testing-strategist) |ddd-api-designer— hexagonal API design (Install:npx skills add full-stack-skills/ddd-skills --skill ddd-api-designer).
💡 Hexagon = ports + adapters. Verification method: without starting the database and HTTP, you can execute business logic just by running CLI/unit tests → the boundary is correct.