Hexagonal Architecture
Isolate application core from external systems using ports and adapters.
Hexagonal Architecture: A Complete Guide with Examples in 11 Programming Languages
Hexagonal Architecture — also known as the Ports and Adapters pattern — is a software architectural style that isolates the core business logic of an application from all external systems: databases, HTTP frameworks, message brokers, file systems, third-party APIs, and user interfaces. The application's domain is placed at the center. Everything that the domain needs to communicate with the outside world is expressed as a Port — an abstract interface. The concrete implementations that connect those ports to real external systems are called Adapters. The domain never knows whether it is talking to a real database or an in-memory fake, a real HTTP endpoint or a test client — it only knows the port interface.
In this comprehensive guide, we'll explore Hexagonal Architecture with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and when to use this powerful pattern.
Table of Contents
- What is Hexagonal Architecture?
- Why Use Hexagonal Architecture?
- Hexagonal Architecture Comparison
- Hexagonal Architecture Explained
- Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is Hexagonal Architecture?
Hexagonal Architecture solves a problem that grows with every line of framework code written inside business logic: the domain becomes impossible to test, reuse, or migrate without dragging the entire technology stack along with it.
The naive approach — writing domain logic directly against a database ORM, an HTTP framework, or a third-party SDK — creates deep coupling. Want to test the order placement rule? You need a database. Want to switch from REST to GraphQL? You rewrite the domain. Want to replace PostgreSQL with DynamoDB? You rewrite everything that touches data access, which is nearly everything.
Hexagonal Architecture solves this with a single rule: the domain defines what it needs through interfaces (Ports); the infrastructure provides implementations of those interfaces (Adapters). The domain never imports infrastructure. Infrastructure imports the domain. Dependency always flows inward.
The name "hexagonal" is intentional but not literal — Alistair Cockburn chose a hexagon to convey that the application has multiple equivalent sides (ports), not just a top (UI) and bottom (database). Any side can be driven by a test, a CLI, a REST API, a message queue, or a scheduled job. The hexagon has no preferred entry point.
Think of it like a power socket standard. Your device (domain) has a plug interface (port) that describes what kind of power it needs. The wall socket in Germany, the US, or Japan is the adapter — it provides power in a local format but conforms to the plug's interface. Your device works everywhere because it depends only on the plug specification, not on any specific wall socket.
Why Use Hexagonal Architecture?
Hexagonal Architecture offers several compelling benefits:
- True Testability: The domain can be tested with zero infrastructure — no database, no HTTP server, no message broker. Inject in-memory adapters and test business rules at the speed of function calls
- Technology Independence: The domain has no dependency on any framework, ORM, or library. Switching from Express to Fastify, from PostgreSQL to MongoDB, or from REST to gRPC requires writing a new Adapter, not touching the domain
- Separation of Concerns: Business rules live in the domain; I/O concerns live in adapters. A developer reading the domain sees only business concepts — no SQL, no HTTP status codes, no JSON serialization
- Parallel Development: Frontend, backend, and infrastructure teams can work in parallel once ports are defined — UI teams use a fake adapter; backend teams implement the real one
- Explicit Dependencies: Every external dependency is named as a port. The list of ports is the complete list of the domain's external needs — there are no hidden dependencies buried in import statements
- Longevity: Domain logic written against ports outlasts any specific technology. The business rules from 2015 still work when you migrate to a new framework in 2025 — you only replace the adapters
Hexagonal Architecture Comparison
Let's compare Hexagonal Architecture with related architectural styles:
| Architecture | Dependency Direction | Domain Isolation | Infrastructure Coupling | Test Complexity |
|---|---|---|---|---|
| Hexagonal (Ports & Adapters) | Always inward toward domain | Complete — domain has no infra imports | Zero — adapters are external | Low — fake adapters for all tests |
| Layered (N-Tier) | Top-down (UI → Business → Data) | Partial — business layer often imports data layer | High — business often knows DB schema | Medium — requires mocking DB layer |
| Clean Architecture | Always inward (Entities → Use Cases → Interface Adapters → Frameworks) | Complete | Zero | Low |
| MVC | Controller → Model → View | None — Model often contains ORM entities | High | Medium |
| CQRS | Commands/Queries → Handlers → Store | Partial | Medium | Medium |
Key Distinctions:
- Hexagonal vs. Clean Architecture: Clean Architecture (Robert Martin) is a more detailed elaboration of Hexagonal Architecture. Both share the same fundamental rule — dependencies point inward. Clean Architecture adds named rings (Entities, Use Cases, Interface Adapters, Frameworks) that map directly to Hexagonal's domain, ports, and adapters. They are philosophically identical; Clean Architecture is more prescriptive about layer names.
- Hexagonal vs. Layered Architecture: Layered architecture allows upper layers to depend on lower layers — the business layer typically imports the data layer directly. Hexagonal inverts this: the domain defines what it needs (port), and the infrastructure implements it (adapter). In layered architecture, swapping the database requires changing the business layer; in hexagonal, it requires only replacing an adapter.
- Hexagonal vs. MVC: MVC is a presentation-layer pattern — it describes how UI, business logic, and data are separated within one technology context. Hexagonal is an application-level architecture that sits around MVC: the Controller is a driving adapter; the Model is (ideally) the domain; the database ORM is a driven adapter. MVC and Hexagonal complement each other.
Hexagonal Architecture Explained
Hexagonal Architecture involves three core layers and two categories of ports:
Core Components
-
Domain (Application Core): The heart of the hexagon. Contains all business entities, domain services, and use case logic. Has zero imports of infrastructure code — no ORM annotations, no HTTP framework classes, no database drivers. Pure business logic written in the programming language's native constructs. This is the only layer that truly matters for the business — everything else is plumbing.
-
Ports: Interfaces defined by the domain that describe what the domain needs from or offers to the outside world. There are two kinds:
- Driving Ports (Primary / Input Ports): Interfaces that the domain exposes to callers — the API that external actors (HTTP controllers, CLI commands, message consumers) use to drive the application. Example:
OrderServiceinterface withplace_order(),cancel_order()methods. - Driven Ports (Secondary / Output Ports): Interfaces that the domain requires from infrastructure — what the domain needs but cannot implement itself. Example:
OrderRepositoryinterface,PaymentGatewayinterface,EmailNotifierinterface.
- Driving Ports (Primary / Input Ports): Interfaces that the domain exposes to callers — the API that external actors (HTTP controllers, CLI commands, message consumers) use to drive the application. Example:
-
Adapters: Concrete implementations of ports that live outside the domain. There are two kinds matching the two port types:
- Driving Adapters (Primary / Input Adapters): Call the domain through driving ports. Examples: HTTP REST controller, GraphQL resolver, CLI command handler, gRPC service, message queue consumer, test client.
- Driven Adapters (Secondary / Output Adapters): Implement driven ports to connect the domain to infrastructure. Examples: PostgreSQL repository, Stripe payment adapter, SendGrid email adapter, Redis cache adapter, in-memory fake adapter for testing.
Hexagonal Architecture Variants
- Classic Hexagonal: Pure ports-and-adapters as described by Alistair Cockburn — minimal prescription beyond the inward dependency rule
- Clean Architecture: Adds named concentric rings (Entities, Use Cases, Interface Adapters, Frameworks) with strict cross-ring dependency rules
- Onion Architecture: Similar to Clean Architecture; emphasizes the domain model as the innermost ring with application services in the next ring
- Vertical Slice Architecture: Organizes code by feature (slice) rather than layer — each slice contains its own domain, port, and adapter code; shares infrastructure through common adapters
- Modular Monolith with Hexagonal: Multiple bounded contexts, each as a self-contained hexagon, sharing a single deployable artifact but with explicit inter-module port-based communication
Diagram
Here is the canonical Hexagonal Architecture diagram:
┌─────────────────────────────────────────────┐
│ DRIVING ADAPTERS │
│ (REST Controller, CLI, gRPC, Test Client) │
└──────────────────┬──────────────────────────┘
│ calls
▼
╔══════════════════════════════════════════════════════════╗
║ DRIVING PORTS (Input Ports) ║
║ (OrderService, UserService interfaces) ║
╠══════════════════════════════════════════════════════════╣
║ ║
║ DOMAIN / APPLICATION CORE ║
║ ║
║ ┌─────────────┐ ┌───────────────────────────┐ ║
║ │ Entities │ │ Use Case / Service │ ║
║ │ (Order, │ │ (PlaceOrderUseCase, │ ║
║ │ User, │◄───│ CancelOrderUseCase) │ ║
║ │ Product) │ └───────────────────────────┘ ║
║ └─────────────┘ ║
║ ║
╠══════════════════════════════════════════════════════════╣
║ DRIVEN PORTS (Output Ports) ║
║ (OrderRepository, PaymentGateway, EmailNotifier) ║
╚══════════════════════════════════════════════════════════╝
│ implemented by
▼
┌─────────────────────────────────────────────┐
│ DRIVEN ADAPTERS │
│ (PostgreSQL Repo, Stripe, SendGrid, Fake) │
└─────────────────────────────────────────────┘
Beginner-Friendly Example
Let's build a product inventory management system. The domain handles adding products, reducing stock, and checking availability. It needs to persist products (driven port: ProductRepository) and send low-stock alerts (driven port: StockNotifier). It exposes a use case interface (driving port: InventoryService) that can be called by a REST controller, a CLI tool, or a test.
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol
# ═══════════════════════════════════════════════════════════════════════════════
# DOMAIN — entities and business rules
# ═══════════════════════════════════════════════════════════════════════════════
@dataclass
class Product:
id: str
name: str
stock: int
low_stock_threshold: int = 5
def reduce_stock(self, qty: int) -> None:
if qty > self.stock:
raise ValueError(f"Insufficient stock for '{self.name}'")
self.stock -= qty
def is_low_stock(self) -> bool:
return self.stock <= self.low_stock_threshold
# ═══════════════════════════════════════════════════════════════════════════════
# DRIVEN PORTS — what the domain needs from infrastructure
# ═══════════════════════════════════════════════════════════════════════════════
class ProductRepository(Protocol):
def find_by_id(self, product_id: str) -> Product | None: ...
def save(self, product: Product) -> None: ...
class StockNotifier(Protocol):
def notify_low_stock(self, product: Product) -> None: ...
# ═══════════════════════════════════════════════════════════════════════════════
# DRIVING PORT — what the domain exposes to callers
# ═══════════════════════════════════════════════════════════════════════════════
class InventoryService(Protocol):
def add_product(self, product_id: str, name: str, stock: int) -> Product: ...
def reduce_stock(self, product_id: str, qty: int) -> Product: ...
def get_product(self, product_id: str) -> Product: ...
# ═══════════════════════════════════════════════════════════════════════════════
# USE CASE — domain logic implementing the driving port
# ═══════════════════════════════════════════════════════════════════════════════
class InventoryServiceImpl:
def __init__(
self,
repository: ProductRepository,
notifier: StockNotifier,
) -> None:
self._repo = repository
self._notifier = notifier
def add_product(self, product_id: str, name: str, stock: int) -> Product:
product = Product(id=product_id, name=name, stock=stock)
self._repo.save(product)
return product
def reduce_stock(self, product_id: str, qty: int) -> Product:
product = self._repo.find_by_id(product_id)
if product is None:
raise ValueError(f"Product '{product_id}' not found")
product.reduce_stock(qty)
self._repo.save(product)
if product.is_low_stock():
self._notifier.notify_low_stock(product)
return product
def get_product(self, product_id: str) -> Product:
product = self._repo.find_by_id(product_id)
if product is None:
raise ValueError(f"Product '{product_id}' not found")
return product
# ═══════════════════════════════════════════════════════════════════════════════
# DRIVEN ADAPTERS — infrastructure implementations of driven ports
# ═══════════════════════════════════════════════════════════════════════════════
class InMemoryProductRepository:
"""Fake adapter — used in tests and local development."""
def __init__(self) -> None:
self._store: dict[str, Product] = {}
def find_by_id(self, product_id: str) -> Product | None:
return self._store.get(product_id)
def save(self, product: Product) -> None:
self._store[product.id] = product
class ConsoleStockNotifier:
"""Simple driven adapter that prints low-stock alerts."""
def notify_low_stock(self, product: Product) -> None:
print(f"[ALERT] Low stock: '{product.name}' has only {product.stock} units left!")
# ═══════════════════════════════════════════════════════════════════════════════
# DRIVING ADAPTER — CLI entry point that calls the domain through the driving port
# ═══════════════════════════════════════════════════════════════════════════════
class InventoryCLI:
def __init__(self, service: InventoryService) -> None:
self._service = service
def run(self) -> None:
self._service.add_product("P001", "Widget", stock=10)
self._service.add_product("P002", "Gadget", stock=3)
product = self._service.reduce_stock("P001", qty=7)
print(f"After reduction: {product.name} has {product.stock} units")
product = self._service.get_product("P002")
print(f"Gadget stock: {product.stock} (low: {product.is_low_stock()})")
# ═══════════════════════════════════════════════════════════════════════════════
# COMPOSITION ROOT — wire everything together
# ═══════════════════════════════════════════════════════════════════════════════
if __name__ == "__main__":
repository = InMemoryProductRepository()
notifier = ConsoleStockNotifier()
service = InventoryServiceImpl(repository, notifier)
cli = InventoryCLI(service)
cli.run()
Production-Ready Example
In a real-world application, Hexagonal Architecture shines when replacing an infrastructure adapter causes zero changes to the domain. The following scenario demonstrates swapping from an in-memory repository (used in tests and development) to a PostgreSQL-backed repository (used in production) with no domain code changes.
Key Production Concerns
- Composition Root: All wiring of adapters to ports happens in one place — the application entry point (
main.py,Program.cs,main.go). The domain never knows which adapter is injected - Testing with fake adapters: Unit tests inject
InMemoryProductRepository— no database, no migrations, no test containers required. Tests run in milliseconds - Integration tests with real adapters: A separate test suite uses
PostgresProductRepositoryagainst a real (or Docker) database to verify the adapter's correctness - Multiple driving adapters simultaneously: The same domain can be driven by an HTTP API, a CLI tool, and a scheduled job — all three call the same use case through the driving port
- Adapter-specific configuration: Adapters receive their configuration (connection strings, API keys) from the environment — the domain receives none because it needs none
Production Folder Structure
src/
├── domain/ # The hexagon — zero infrastructure imports
│ ├── entities/
│ │ └── product.py # Pure domain object
│ ├── ports/
│ │ ├── driven/
│ │ │ ├── product_repository.py # Output port (interface)
│ │ │ └── stock_notifier.py # Output port (interface)
│ │ └── driving/
│ │ └── inventory_service.py # Input port (interface)
│ └── use_cases/
│ └── inventory_service_impl.py # Use case implementing input port
│
├── adapters/ # Outside the hexagon
│ ├── driven/
│ │ ├── postgres_product_repository.py # Real DB adapter
│ │ ├── in_memory_product_repository.py # Fake adapter (tests/dev)
│ │ └── sendgrid_stock_notifier.py # Real email adapter
│ └── driving/
│ ├── http/
│ │ └── inventory_controller.py # REST controller adapter
│ ├── cli/
│ │ └── inventory_cli.py # CLI adapter
│ └── jobs/
│ └── low_stock_scanner.py # Scheduled job adapter
│
└── composition/
└── container.py # Composition root — wires everything
Real-World Use Cases
Hexagonal Architecture is the right choice for any application where the business rules have a longer lifespan than the technology stack.
1. Microservices with Swappable Infrastructure
Each microservice is a standalone hexagon. The domain defines what it needs (a MessagePublisher port, a UserRepository port) without specifying whether the publisher is Kafka or RabbitMQ, or whether the repository is PostgreSQL or DynamoDB. When infrastructure decisions change — migrating from a managed queue to a self-hosted one — only the adapter changes. The domain and all tests are untouched.
2. Multi-Channel Applications
An application that must serve REST API clients, a mobile app via gRPC, an internal CLI tool, and a scheduled batch job has four driving adapters calling the same domain through the same driving port. Hexagonal Architecture makes this natural — each channel is just another adapter. Adding a fifth channel (a WebSocket interface, a message queue consumer) means writing one new adapter, not touching the domain or the other four adapters.
3. Domain-Driven Design (DDD) Bounded Contexts
Each bounded context in DDD becomes a hexagon. The Inventory context and the Billing context each have their own domain, ports, and adapters. They communicate with each other through their driving ports — never by sharing database tables or domain objects directly. This is the architectural foundation of every serious DDD implementation.
4. Legacy System Migration
Hexagonal Architecture is the safest way to migrate a legacy system. The existing legacy code is wrapped in a driven adapter that implements the domain's output port. The new domain logic is tested against a fake adapter. When the real adapter (backed by the new infrastructure) is ready, it replaces the legacy adapter — the domain sees no difference. Migration happens adapter by adapter, not big-bang.
5. Test-Driven Development at Scale
Teams practicing TDD at scale find that Hexagonal Architecture makes TDD sustainable. All business logic tests use fake adapters — they never touch the filesystem, network, or database. The test suite runs in seconds regardless of how many business rules it covers. Infrastructure tests (adapters against real systems) are smaller, focused, and run separately in CI.
6. Flutter Mobile Applications
Flutter apps benefit from Hexagonal Architecture for the same reasons as any application — the domain should not know whether it is talking to a local SQLite database or a remote REST API. A ProductRepository port is implemented by SqliteProductRepository for offline use and RestProductRepository for online use. The domain, and all domain tests, work identically in both cases.
Language-Specific Mistakes and Anti-Patterns
Common Mistakes Across All Languages
- Infrastructure in the domain: Importing an ORM class, an HTTP framework type, or a database driver anywhere inside the domain layer is the most fundamental violation — if the domain has a
from sqlalchemy import Columnimport, it has failed the first rule of Hexagonal Architecture - Anemic domain model: Defining entities as plain data bags (
@dataclassorrecordwith no methods) and putting all business logic in the use case service. The domain entities should contain business rules —product.reduceStock(qty)notif product.stock >= qty: product.stock -= qtyin the service - Port leakage: Defining a port interface in the adapter package rather than the domain package. Ports belong to the domain — the domain defines what it needs; adapters fulfill the need
- Composition root in the wrong place: Wiring adapters to ports inside use cases, repositories, or controllers instead of in one dedicated composition root. The composition root is the only place that knows about all adapters
- Too many ports: Defining a separate port for every method instead of grouping related operations.
ProductRepositoryis one port withfindById,save, anddelete— not three separate ports
Language-Specific Pitfalls
- Python: Using
from sqlalchemy.orm import DeclarativeBasein a domain entity file is a classic violation — SQLAlchemy annotations belong in the adapter. UseProtocolfor port interfaces (structural subtyping — no explicitimplements). Avoid circular imports between domain and adapter packages by keeping domain in its own pure package. - TypeScript: Importing
express.Requestor@nestjs/commondecorators inside domain use case files pollutes the domain with framework concerns. Define ports as pure TypeScript interfaces — no framework decorators. Usetsconfig.jsonpath aliases (@domain/*,@adapters/*) to make wrong-direction imports visually obvious. - Java: Using
@Entity,@Table, or@ColumnJPA annotations on domain entities ties the domain to Hibernate. Use separate JPA entity classes in the adapter layer and map them to domain objects. Avoid@Autowiredfield injection in use cases — use constructor injection so that tests can provide adapters directly. - JavaScript: JavaScript has no interfaces — use JSDoc
@typedefor TypeScript declarations for documentation. Rely on duck typing but enforce port shape in tests. The biggest risk is importing framework modules in the domain file because there's no compile-time check — use ESLint rules to forbid cross-layer imports. - C#: Placing
[ApiController],[HttpGet], orDbContextreferences inside domain classes is the most common mistake. Useinterfacefor all ports and let ASP.NET Core's DI container resolve adapters at startup. Avoid usingdynamicto bypass the port interface — it defeats the compile-time safety that makes the pattern valuable. - PHP: Injecting the Eloquent
Modelclass directly into use cases couples the domain to Laravel's ORM. Define aProductRepositoryinterface in the domain; implement it with Eloquent in the adapter. Use PHP 8readonlyconstructors for immutable domain entities. Avoid putting business logic in LaravelRequestclasses — those are driving adapters, not domain objects. - Go: Go's implicit interfaces make port definition natural — define the interface in the package that uses it (the domain), not the package that implements it. Avoid putting database connection code in the domain package. Use constructor functions (
NewInventoryService(repo ProductRepository, notifier StockNotifier)) to make dependencies explicit and testable. - Rust: Avoid using
sqlxtypes orreqwesttypes in domain structs — those are adapter concerns. Define port traits in the domain crate; implement them in separate adapter crates. Use generic type parameters (InventoryService<R: ProductRepository, N: StockNotifier>) for zero-cost abstraction over adapters at compile time, orBox<dyn Trait>for runtime polymorphism. - Dart: Avoid importing
package:httporpackage:sqflitein domain files. Useabstract interface class(Dart 3) for ports. In Flutter, keep domain classes free ofBuildContext,Widget, or any Flutter SDK imports — domain logic must be testable in pure Dart without a Flutter engine. - Swift: Avoid importing
UIKit,SwiftUI, orCoreDatainside domain types. Useprotocolfor both driving and driven ports. In Swift concurrency, mark protocol methodsasyncif any adapter implementation will be async — this forces callers toawaiteven when the in-memory fake returns synchronously. Useactorfor driven adapters that hold mutable state. - Kotlin: Avoid importing
android.*orandroidx.*packages in domain classes — those are Android framework concerns that belong in adapters. Useinterfacefor all ports. In Spring Boot, annotate adapter classes with@Componentor@Repository, but keep domain classes as plain Kotlin — no Spring annotations in the domain package. Use Hilt or Koin for dependency injection wiring at the composition root.
Frequently Asked Questions
What is the difference between Hexagonal Architecture and Clean Architecture?
They are philosophically identical — both enforce the Dependency Inversion Principle by requiring that all dependencies point inward toward the domain. Clean Architecture (Robert C. Martin, 2012) is a more detailed elaboration of Hexagonal Architecture (Alistair Cockburn, 2005). Clean Architecture adds named concentric rings (Entities, Use Cases, Interface Adapters, Frameworks & Drivers) and explicit rules about which ring can depend on which. Hexagonal Architecture is simpler — it just says domain in the center, ports as interfaces, adapters outside. In practice, teams often say "Clean Architecture" and mean the combination of both.
Do I need Hexagonal Architecture for a simple CRUD app?
No. If your application is a straightforward CRUD service with simple validation and no complex business rules, the overhead of defining ports and adapters for every external dependency adds complexity without proportionate benefit. Hexagonal Architecture pays off when business rules are complex enough to warrant isolation from infrastructure, when the application needs to support multiple entry points or output channels, or when the team practices TDD and needs fast, infrastructure-free tests. For a simple CRUD app, a well-structured layered architecture is sufficient.
How do I handle cross-cutting concerns like logging and authentication?
Cross-cutting concerns are implemented in the adapter layer, not the domain. Authentication is handled in the driving adapter (HTTP middleware, gRPC interceptor) before the use case is called. Logging of infrastructure operations belongs in adapters — the database adapter logs slow queries; the HTTP adapter logs request/response. Domain-level logging (business events like "order placed", "stock depleted") is best modeled as domain events that driving adapters observe and forward to logging infrastructure.
Can I use an ORM with Hexagonal Architecture?
Yes, but the ORM belongs in the adapter layer, not the domain. The domain defines a ProductRepository port with methods like findById and save. The driven adapter implements this port using the ORM — it maps between ORM entities and domain objects. The domain entity Product has no ORM annotations. The adapter's ORM class (ProductEntity or ProductModel) has all the annotations. This mapping is extra code but preserves the domain's purity and testability.
What is the Composition Root and where does it live?
The Composition Root is the single place in the application where all adapters are instantiated and wired to their corresponding ports. It is the only place that knows about both the domain and all adapters. In practice it lives in main.py, Program.cs, main.go, or Application.kt — the application entry point. Frameworks with dependency injection containers (Spring, ASP.NET Core, NestJS) put the composition root in their DI configuration. The key rule: wiring never happens inside domain classes or use cases — only at the composition root.
How do I test a Hexagonal Architecture application?
Testing is one of Hexagonal Architecture's greatest benefits. There are three test layers: domain unit tests (no adapters — test entities and domain logic directly), use case unit tests (inject fake adapters implementing the driven ports — test business rules without any infrastructure), and adapter integration tests (test each adapter against its real external system in isolation — no domain logic). This pyramid means domain tests run in microseconds, use case tests run in milliseconds, and only adapter tests need external infrastructure. End-to-end tests exercise the full stack but are kept minimal.
Key Takeaways
Language-Specific Best Practices
- Python: Use
Protocolfromtypingfor structural port interfaces — no explicitimplementsrequired; organize packages asdomain/,adapters/driven/,adapters/driving/,composition/; usepytestwith fake adapters for domain tests; enforce layer boundaries withimport-linterorpylintcustom rules - TypeScript: Use pure TypeScript
interfacefor all ports — no framework decorators; use barrel files (index.ts) to control what each package exposes; configuretsconfig.jsonpath aliases to make cross-layer imports obvious; usejestwithts-jestand fake adapter classes for all domain tests - Java: Use constructor injection for all use cases — never field injection; keep domain classes as plain Java with no Spring/JPA/framework annotations; use
recordfor immutable value objects; useMockitoor hand-written fakes for driven port testing - JavaScript: Use JSDoc
@interfacecomments to document port contracts; use ESLintimport/no-restricted-pathsto enforce layer boundaries; create fake adapter classes in a__fakes__folder for test use; usejestwith fake adapters exclusively for domain tests - C#: Register all adapters in
Program.csusingbuilder.Services; useinterfaceprefix (IProductRepository) for all ports by convention; userecordfor immutable domain value objects; usexUnitwith hand-written orMoq-based fake adapters for domain tests - PHP: Use PHP 8
interfacefor ports; keep domain classes in aDomain\namespace with zero Laravel imports; usereadonlyconstructor promotion for immutable entities; use PHPUnit with in-memory adapter fakes; use Laravel's service container only inAppServiceProviderfor adapter binding - Go: Define port interfaces in the domain package that uses them; use constructor functions for all use cases; keep the
domainpackage import-free of any third-party packages; usego testwith struct-based fake adapters implementing domain interfaces - Rust: Define port traits in a
domaincrate; implement adapters in separateadapterscrates that depend ondomain; use generic type parameters for zero-cost adapter abstraction; usecargo testwith fake adapter structs for all domain tests - Dart: Use
abstract interface class(Dart 3) for ports; keep domain in a pure Dart package with no Flutter SDK imports; test domain logic withdart testand fake adapters; useget_itorriverpodfor composition root wiring in Flutter apps - Swift: Use
protocolfor all ports; mark stateful driven adapters asactorfor Swift concurrency safety; keep domain types in a pure Swift package (noUIKit/SwiftUIimports); useXCTestwith struct-based or class-based fake adapters - Kotlin: Use
interfacefor all ports; keep domain in a Gradle module with no Android or Spring dependencies; usesuspend funon port methods if any adapter will be async; usekotestorJUnit5with fake adapter implementations for domain tests; use Hilt or Koin only in the composition root module
📋 When to Use Hexagonal Architecture:
- The application has complex business rules that need to be tested without infrastructure overhead
- The application must support multiple entry points (REST, GraphQL, CLI, message queue, scheduled jobs)
- The team anticipates technology changes — database migrations, framework upgrades, third-party API replacements
- The team practices TDD and needs a fast, reliable, infrastructure-free test suite
- The domain is the core competitive differentiator and must outlast any specific technology choice
- The application will grow — new features mean new use cases, not modifications to existing ones
⚠️ When NOT to Use Hexagonal Architecture:
- The application is a simple CRUD service with minimal business logic — the overhead of ports and adapters exceeds the benefit
- The team is small, the codebase is young, and the primary goal is speed of delivery — add the architecture as complexity warrants it
- The technology stack is fixed and will never change — if you will always use PostgreSQL and Express, the abstraction buys nothing
- The team is unfamiliar with dependency inversion and the pattern would be applied cargo-cult style without understanding — a well-structured MVC is better than poorly understood hexagonal
🛠️ Best Practices Across Languages:
- Domain has zero infrastructure imports — if you see a database driver, ORM class, or HTTP framework type in a domain file, it is a violation; fix it immediately
- Ports belong to the domain — the domain package defines the interface; adapters implement it; never define a port in an adapter package
- One composition root — all adapter-to-port wiring happens in one place; use cases never
newtheir own adapters - Fake adapters in the domain test package — keep in-memory fakes next to their corresponding tests; they are first-class test infrastructure
- Name ports by capability, not technology —
ProductRepositorynotPostgresProductRepository;StockNotifiernotSendGridNotifier; the port name must make sense without knowing the adapter - Keep use cases thin — use cases orchestrate; entities contain business logic. If a use case has more than 20 lines, check whether business logic has leaked out of the domain entity
- Test the domain exhaustively, adapters minimally — domain tests are fast, cheap, and cover all business rules; adapter tests only verify the adapter's mapping correctness against its external system
💡 Common Pitfalls:
- Framework bleeding: Using framework-specific types (
Request,Response,@Entity) in domain code — the most common and most damaging violation - Anemic domain: All business logic in use case services, all entities as plain data — this is Transaction Script wearing hexagonal clothing, not true hexagonal architecture
- Port proliferation: A port per method instead of per capability — results in dozens of tiny interfaces that are harder to manage than a few well-designed ones
- Adapter in the wrong direction: Writing a driven adapter that calls the domain (instead of being called by it) — driven adapters only implement domain-defined interfaces, never call use cases
- Missing composition root: Wiring adapters inside use cases (
self.repo = PostgresProductRepository()) defeats the entire purpose — the domain must never know which adapter it receives - Shared database between hexagons: Two bounded contexts accessing the same database tables bypasses the port abstraction — each hexagon owns its data; inter-hexagon communication goes through driving ports
🎁 Advanced Techniques:
- Adapter decorators: Wrap a real adapter with a decorating adapter that adds caching, retry logic, or circuit-breaking —
CachingProductRepositorywrapsPostgresProductRepositoryand both implementProductRepository; the domain sees only the port interface - Event-driven inter-hexagon communication: Hexagons communicate by publishing domain events through a
DomainEventPublisherdriven port; other hexagons consume events through their driving ports (message queue consumers) — loose coupling between bounded contexts with no shared domain objects - Specification pattern: Encapsulate complex query criteria as
Specificationdomain objects passed to theProductRepositoryport — the domain defines what it wants; the adapter translates the specification to SQL, Mongo queries, or in-memory predicates - Anti-corruption layer: When integrating with a legacy system or third-party API that has a different domain model, add a translation layer inside the driven adapter — it maps between the external model and the domain model so domain concepts stay pure
- Hexagonal + CQRS: Split the driving port into
CommandService(write operations, strict domain model) andQueryService(read operations, optimized read models) — commands go through the full domain; queries bypass the domain and read directly from optimized projections via their own driven port - Contract testing for adapters: Use consumer-driven contract tests (Pact) to verify that real adapters conform to the port interface — the domain package defines the contract; adapter integration tests verify fulfillment; both sides can evolve independently
Hexagonal Architecture is not a framework, a library, or a tool — it is a discipline. It requires a team to agree that the domain will never import infrastructure, that ports belong to the domain, and that the composition root is the only place where adapters are wired. Teams that maintain this discipline build systems where the domain can be tested in isolation, where technology choices are genuinely reversible, and where the business logic accumulated over years of development is never held hostage by a framework version or a database vendor. That discipline is what makes large, long-lived systems maintainable.
This guide is part of the Design Patterns in Action series, which covers:
- Visitor Pattern — New operations on a stable hierarchy
- Memento Pattern — Capture and restore object state
- Observer Pattern — Event-driven notifications
- State Pattern — Behavior changes with lifecycle phase
- Strategy Pattern — Interchangeable algorithms
- Facade Pattern — Simplified interface to a subsystem
- MVC Pattern — Model-View-Controller architecture
- Unit of Work Pattern — Coordinate database changes atomically
- Hexagonal Architecture — Ports and Adapters for technology-independent domains
Each guide covers all 11 programming languages with production-ready examples, language-specific best practices, and comprehensive anti-pattern analysis.