Hexagonal Architecture (Ports & Adapters)
Core: The application core is technology-agnostic. External tools are interchangeable details. Dependencies always point INWARD.
Focus: The boundary between the application core and external services.
Strengths: High interchangeability (adapters), clear side-effect definitions, and architectural simplicity.
Weaknesses: Potential for "Core" logic bloat and naming ambiguity between Ports and Adapters.
1. The Separation of Concerns
| Layer |
Responsibility |
Components |
| Inside (Core) |
Application Logic & Business Rules |
Use Cases, Services, Interactors, Models |
| Ports (Interfaces) |
Contract for communication with the Core |
Inbound Ports (API), Outbound Ports (SPI) |
| Outside (Adapters) |
Translation between external world and Core |
Controllers, Repositories, Mailers, CLI |
2. Dependency Flow Rule
Dependency Direction: Always INWARD toward the Core.
- The Core must NEVER import anything from an Adapter (e.g., no Express types, no ORM decorators).
- Outer layers depend on Interfaces (Ports) defined inside the Core.
3. Ports: The Boundary Contracts
A. Inbound Ports (Driving)
- Definition: Interfaces that define what the application can do.
- Usage: Called by Controllers or CLI.
- Constraint: Must only use simple DTOs or Core Models.
B. Outbound Ports (Driven)
- Definition: Interfaces that define what the application needs from the outside (Persistence, Messaging).
- Usage: Implemented by Infrastructure (Adapters).
- Constraint: Signature must be technology-agnostic (e.g.,
saveUser() not insertIntoMongo()).
4. Adapters: The Translators
A. Driving Adapters (Input)
- Examples: REST Controllers, GraphQL Resolvers, CLI commands.
- Job: Convert external requests (HTTP, JSON) into Core-friendly data and call an Inbound Port.
B. Driven Adapters (Output)
- Examples: SQL Repositories, Redis Cache, AWS S3 Client.
- Job: Implement an Outbound Port, converting Core data into specific technology formats.
5. Implementation Mapping (Data Integrity)
To avoid "Leaky Abstractions", use Mappers:
- Input Mapper: Controller Request -> Use Case DTO.
- Output Mapper: Use Case Result -> UI Response.
- Persistence Mapper: Core Model <-> DB Schema/Entity.
Rule: Do not use the same class for DB persistence and Application logic.
6. Directory Structure (Agnostic Layout)
This structure focuses on separation by purpose instead of frameworks, allowing for evolution into DDD (subdomains) or Clean Architecture.
src/
├── @shared/ # Global types, agnostic utilities, and Kernel
├── core/ # The Core (Inside)
│ ├── domain/ # Business logic flows, Logic-heavy objects (Agnostic)
│ ├── application/ # Application logic flows, Use Cases / Interactors
│ └── ports/ # INTERFACES (contracts)
│ ├── in/ # Driving Ports (input)
│ └── out/ # Driven Ports (output)
├── infrastructure/ # The Adapters (Outside)
│ ├── adapters/ # Implementation of Outbound Ports
│ │ ├── persistence/ # DB (TypeORM, Prisma, etc.)
│ │ └── external/ # API Clients, Mailers
│ └── transport/ # Entry points (Web, CLI, Grpc, Events); Driving Adapters (Controllers, Routes)
└── main # Composition Root and Dependency Injection Setup
7. Dependency Injection & Composition Root
To ensure total decoupling, the Core never instantiates its own Adapters.
- The Rule: The main/ folder (or a dedicated DI container) is the only place allowed to couple the Core with Infrastructure implementations.
- The Mechanism: Use Constructor-based Dependency Injection.
- The Check: If a Use Case or Domain Service uses the new keyword to instantiate a Repository or an external Service, the architecture is violated.
- Transaction Management: Transactions must be orchestrated at the Application Layer using an abstract Unit of Work or Transaction Port, ensuring the Core remains agnostic of the specific commit/rollback implementation.
8. Error Handling & Exception Mapping
To maintain a pure Core, technological failures must not pollute the business logic.
- Rule: Infrastructure exceptions (e.g., SQLException, AxiosError) MUST NOT pass through Outbound Ports.
- Mechanism: The Adapter must catch technical exceptions and wrap/map them into Domain Exceptions defined inside the Core.
- Flow: Adapter (Catch ToolError) -> Map to DomainError -> Throw -> Use Case (Handle DomainError).
9. Testability & Mocking Strategy
The quality of the architecture is directly measured by how easily the Core can be tested in isolation.
- Core Unit Tests: Must test application/ and domain/ without complex mocking frameworks or heavy dependencies. Use In-Memory Fakes (e.g., InMemoryUserRepository) to satisfy ports/out.
- Adapter Integration Tests: Focus on testing the real implementation of adapters against actual resources (using Docker, TestContainers, or dev-databases).
- The 100% Rule: If testing a Use Case requires starting a database, an HTTP server, or any external IO, the Hexagonal Architecture has been violated.
- Validation: Use Cases should be triggered via Inbound Ports during testing to ensure the contract is respected.
10. Anti-Patterns to Block (Watchlist)
- The "Big Ball of Mud": Business logic directly inside an Express/Fastify controller.
- ORM Contamination: Putting decorators on models inside the application/folder.
- Direct Coupling: The Core importing a specific library (e.g., axios or knex) instead of using a Port.
- Shared DTOs: Using the same DTO for the Web API and the Database.
11. Quality Gate (Hexagonal)
1---2name: hexagonal-architecture3description: Guardian of Hexagonal Architecture (Ports and Adapters pattern). Decouples application logic from external technologies, frameworks, and delivery mechanisms.4---56# Hexagonal Architecture (Ports & Adapters)78**Core:** The application core is technology-agnostic. External tools are interchangeable details. Dependencies always point **INWARD**.9**Focus:** The boundary between the application core and external services.10**Strengths:** High interchangeability (adapters), clear side-effect definitions, and architectural simplicity.11**Weaknesses:** Potential for "Core" logic bloat and naming ambiguity between Ports and Adapters.1213---1415## 1. The Separation of Concerns1617| Layer | Responsibility | Components |18| :--- | :--- | :--- |19| **Inside (Core)** | Application Logic & Business Rules | Use Cases, Services, Interactors, Models |20| **Ports (Interfaces)** | Contract for communication with the Core | Inbound Ports (API), Outbound Ports (SPI) |21| **Outside (Adapters)** | Translation between external world and Core | Controllers, Repositories, Mailers, CLI |2223---2425## 2. Dependency Flow Rule2627**Dependency Direction:** Always **INWARD** toward the Core.28* The Core must NEVER import anything from an Adapter (e.g., no Express types, no ORM decorators).29* Outer layers depend on Interfaces (Ports) defined inside the Core.3031---3233## 3. Ports: The Boundary Contracts3435### A. Inbound Ports (Driving)36* **Definition:** Interfaces that define what the application *can do*.37* **Usage:** Called by Controllers or CLI.38* **Constraint:** Must only use simple DTOs or Core Models.3940### B. Outbound Ports (Driven)41* **Definition:** Interfaces that define what the application *needs* from the outside (Persistence, Messaging).42* **Usage:** Implemented by Infrastructure (Adapters).43* **Constraint:** Signature must be technology-agnostic (e.g., `saveUser()` not `insertIntoMongo()`).4445---4647## 4. Adapters: The Translators4849### A. Driving Adapters (Input)50* **Examples:** REST Controllers, GraphQL Resolvers, CLI commands.51* **Job:** Convert external requests (HTTP, JSON) into Core-friendly data and call an Inbound Port.5253### B. Driven Adapters (Output)54* **Examples:** SQL Repositories, Redis Cache, AWS S3 Client.55* **Job:** Implement an Outbound Port, converting Core data into specific technology formats.5657---5859## 5. Implementation Mapping (Data Integrity)6061To avoid "Leaky Abstractions", use **Mappers**:621. **Input Mapper:** Controller Request -> Use Case DTO.632. **Output Mapper:** Use Case Result -> UI Response.643. **Persistence Mapper:** Core Model <-> DB Schema/Entity.6566> **Rule:** Do not use the same class for DB persistence and Application logic.6768---6970## 6. Directory Structure (Agnostic Layout)7172This structure focuses on separation by **purpose** instead of frameworks, allowing for evolution into DDD (subdomains) or Clean Architecture.7374```text75src/76├── @shared/ # Global types, agnostic utilities, and Kernel77├── core/ # The Core (Inside)78│ ├── domain/ # Business logic flows, Logic-heavy objects (Agnostic)79│ ├── application/ # Application logic flows, Use Cases / Interactors80│ └── ports/ # INTERFACES (contracts)81│ ├── in/ # Driving Ports (input)82│ └── out/ # Driven Ports (output)83├── infrastructure/ # The Adapters (Outside)84│ ├── adapters/ # Implementation of Outbound Ports85│ │ ├── persistence/ # DB (TypeORM, Prisma, etc.)86│ │ └── external/ # API Clients, Mailers87│ └── transport/ # Entry points (Web, CLI, Grpc, Events); Driving Adapters (Controllers, Routes)88└── main # Composition Root and Dependency Injection Setup89```909192## 7. Dependency Injection & Composition Root93To ensure total decoupling, the Core never instantiates its own Adapters.94* **The Rule:** The main/ folder (or a dedicated DI container) is the only place allowed to couple the Core with Infrastructure implementations.95* **The Mechanism:** Use Constructor-based Dependency Injection.96* **The Check:** If a Use Case or Domain Service uses the new keyword to instantiate a Repository or an external Service, the architecture is violated.97* **Transaction Management:** Transactions must be orchestrated at the Application Layer using an abstract Unit of Work or Transaction Port, ensuring the Core remains agnostic of the specific commit/rollback implementation.9899---100101## 8. Error Handling & Exception Mapping102To maintain a pure Core, technological failures must not pollute the business logic.103* **Rule:** Infrastructure exceptions (e.g., SQLException, AxiosError) MUST NOT pass through Outbound Ports.104* **Mechanism:** The Adapter must catch technical exceptions and wrap/map them into Domain Exceptions defined inside the Core.105* **Flow:** Adapter (Catch ToolError) -> Map to DomainError -> Throw -> Use Case (Handle DomainError).106107---108109## 9. Testability & Mocking Strategy110The quality of the architecture is directly measured by how easily the Core can be tested in isolation.111* **Core Unit Tests:** Must test application/ and domain/ without complex mocking frameworks or heavy dependencies. Use In-Memory Fakes (e.g., InMemoryUserRepository) to satisfy ports/out.112* **Adapter Integration Tests:** Focus on testing the real implementation of adapters against actual resources (using Docker, TestContainers, or dev-databases).113* **The 100% Rule:** If testing a Use Case requires starting a database, an HTTP server, or any external IO, the Hexagonal Architecture has been violated.114* **Validation:** Use Cases should be triggered via Inbound Ports during testing to ensure the contract is respected.115116---117118## 10. Anti-Patterns to Block (Watchlist)119- **The "Big Ball of Mud":** Business logic directly inside an Express/Fastify controller.120- **ORM Contamination:** Putting decorators on models inside the application/folder.121- **Direct Coupling:** The Core importing a specific library (e.g., axios or knex) instead of using a Port.122- **Shared DTOs:** Using the same DTO for the Web API and the Database.123124---125126## 11. Quality Gate (Hexagonal)127- [ ] Core is free of framework-specific imports?128- [ ] All external integrations are defined as interfaces in ports/out/?129- [ ] Business logic is testable with zero IO/Database?130- [ ] Dependencies point only toward the Core?131- [ ] All infrastructure errors are mapped to Domain Exceptions?