SuperAPI AI Directory

ddd-architecture-hexagonal

General & other

View repository

Updated 10-15 21:48

About this skill

DDD Architecture — Hexagonal (Ports & Adapters)

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

  1. Dependency Rule: The dependency direction must be from outside to inside—Adapter → Application → Domain. The Domain layer has zero external dependencies.
  2. Port Granularity Rule: Each port has a single responsibility; a port containing 10+ methods must be split immediately.
  3. 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

  1. Multi-entry systems(REST + CLI + MQ + gRPC): Multiple external systems drive the same business logic simultaneously
  2. Frequent infrastructure changes(switching DB/MQ/cache): Only the Adapter implementation needs to be replaced, with zero modifications to the Domain
  3. Ultimate testability: Mocking ports allows testing all domain logic without depending on a database
  4. Microservice architecture standardization: Team > 5 people, requiring consistent architectural conventions across services
  5. The team is familiar with abstract design: Able to design port granularity reasonably and avoid over-abstraction

Boundary

⚠️ Conditions Required

  1. The team understands abstract design: Port granularity requires domain knowledge and abstraction ability; otherwise it may be defined improperly
  2. Project size is moderate: For small projects, Layered is simpler; hexagonal adds indirect-layer cost
  3. Requires DDD domain model cooperation: Defining Port interfaces requires prior knowledge of domain modeling
  4. Good DI container support: Frameworks such as Spring/Guice can simplify adapter assembly

❌ Not Applicable (with Alternatives)

  1. Single REST API + simple CRUD → Use ddd-architecture-layered
  2. Chinese enterprise MyBatis ecosystem → Use ddd-architecture-cola
  3. Requires strict separation of UseCase + Entity → Use ddd-architecture-clean
  4. 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)

  1. 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.
  2. Primary adapter contains business logic: A Controller can only perform parameter conversion and call the UseCase; it must not contain if/else business decisions.
  3. Secondary adapter forgets exception conversion: Technical exceptions (SQLException, TimeoutException) must be converted into domain exceptions inside the adapter.
  4. Domain layer depends on frameworks: The Domain module must not import Spring/JPA/MyBatis annotations.
  5. Inconsistent port naming: Inbound ports are named with actions (CreateOrderUseCase), outbound ports are named with resources (OrderRepository)。
  6. Transactions placed in the adapter layer: Transactions are controlled by the application layer; adapters are not responsible for transaction management.
  7. Over-abstraction: Do not create ports in advance for "might change in the future." Extract ports only when replacement is actually needed.
  8. 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

Navigation

  • → Next: hand off to the ddd-domain-designer skill — 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.