Skip to content

Hexagonal Architecture in Practice: Ports, Adapters and When to Use Them

A
abemon
| | 12 min read | Written by practitioners
Share

You already know the theory

If you are reading this, you probably already know what hexagonal architecture is. Alistair Cockburn proposed it in 2005 with a longer name (Ports and Adapters) and a simple idea: your business logic should know nothing about the outside world. Databases, APIs, web frameworks, the UI: all of these are implementation details that connect to the core through ports (interfaces) and adapters (implementations).

The theory is elegant. Practice is where things get complicated. Because the question theory does not answer is: when is it worth it? And the honest answer is that in many projects, it is not. At least not from the start. (For a broader view on why architecture decisions matter, see our piece on the real cost of technical debt.)

When it is overkill

Let us be direct. Hexagonal architecture adds structural complexity. More files, more interfaces, more indirection. For certain types of projects, that complexity is not justified:

Simple CRUDs. If your application is fundamentally a form connected to a database with basic validation, hexagonal architecture is a cannon to kill a fly. A controller calling a service calling a repository is enough. Adding ports and adapters to a “read this, validate that, save the other” operation is engineering without purpose.

Prototypes and MVPs. When you do not know if the product will survive three months, investing in a decoupled architecture is premature optimization. The MVP’s goal is to validate the hypothesis, not win engineering awards. If it survives, refactor. If not, you have not wasted weeks on abstractions.

Projects with a single adapter per port. If your application uses PostgreSQL and will always use PostgreSQL, creating a RepositoryPort interface and a PostgresAdapter is ceremony without benefit. The interface only makes sense if there is a real possibility that you will need another implementation (testing aside, which we will address later).

So when does it make sense?

When it delivers real value

Complex business logic. When your domain has non-trivial rules (price calculations with tiered discounts, state-dependent validations, workflows with multiple conditions), isolating that logic from the framework and infrastructure lets you test it, understand it, and modify it without risk. We worked on a logistics quoting system (a custom development project that fully justified this approach) where the pricing logic depended on 14 variables (weight, volume, origin, destination, goods type, urgency, route, season, client, agreement…). That logic lives in the domain, fully decoupled. We can test 200 combinations in 3 seconds without starting a server.

Multiple entry channels. If your application is consumed through a REST API, a CLI, a message queue, and a webhook, having a single core with multiple input adapters avoids duplicating logic. Each channel is an adapter that translates its format into the domain’s language.

Changing integrations. If your application integrates with external services that may change (CRM migration, payment provider switch, new partner API), output adapters let you make the change without touching business logic. This is not theoretical: we migrated a client from one ERP to another by swapping an adapter without modifying a single line of domain code. (More on ERP integration patterns in our dedicated executive brief.)

Large teams. When 8 or 10 developers work on the same system, hexagonal architecture establishes clear boundaries. The domain team does not need to know how the Kafka adapter works. The infrastructure team does not need to understand business rules. The interfaces (ports) are the contract between both.

Incremental refactoring

The most common path to hexagonal architecture is not starting from scratch. It is refactoring an existing system that has grown to a point where coupling causes real pain: tests that require a database, API changes that break business logic, or integrations that spread through the application like vines.

The incremental process we have used on real projects:

Step 1: Extract the domain. Identify business logic mixed with infrastructure. Move it to pure classes or modules with no dependencies on framework, database, or external libraries. Do not change the logic. Just move it. This is already useful on its own, because you can start testing it in isolation.

Step 2: Define output ports. For each external dependency your domain uses (database, external API, file system, message queue), create an interface defining what the domain needs. Not what the infrastructure offers, but what the domain needs. The difference is critical. Your domain does not need “a findAll method returning a paginated list with sorting”; it needs “get outstanding invoices for a client.” The port reflects the domain’s language, not the database’s.

Step 3: Implement adapters. Each concrete implementation of a port is an adapter. Your PostgresInvoiceRepository implements InvoiceRepository (the port). Your StripePaymentGateway implements PaymentGateway. Adapters live outside the domain, in an infrastructure layer.

Step 4: Invert dependencies. Now the domain depends only on interfaces it defines. Infrastructure depends on the domain (implements its interfaces), not the other way around. This is Dependency Inversion (the D in SOLID) applied at the architectural level. In practice, you need a dependency injection mechanism. Spring has it natively. In Python, FastAPI has Depends. In Node, you can use a simple container or manual composition.

Step 5: Define input ports. Optional and often unnecessary in the first iteration. Input ports (use cases or application services) define the operations the outside world can invoke. They are useful when you have multiple entry points or want to formally document your domain’s API. If your only entry point is a REST controller, an application service that delegates to the domain is sufficient.

Real project structure

Theory says “hexagonal.” Project directories say something else. Here is a structure we have used in production with Python (FastAPI) that works for teams of 3 to 8:

src/
  domain/
    models/          # Entities and value objects
    ports/           # Output interfaces (repositories, gateways)
    services/        # Pure business logic
    exceptions/      # Domain exceptions
  application/
    use_cases/       # Domain logic orchestration
    dto/             # Data transfer objects
  infrastructure/
    adapters/
      persistence/   # Repository implementations
      external/      # External API clients
      messaging/     # Queue producers and consumers
    web/
      controllers/   # REST endpoints
      middleware/     # Authentication, logging, etc.
    config/          # Dependency injection, configuration

What matters is not the directory names (call them whatever you like) but the dependency rules:

  • domain/ imports nothing from infrastructure/ or application/
  • application/ imports from domain/ but not from infrastructure/
  • infrastructure/ imports from domain/ and application/

If your IDE has dependency analysis (IntelliJ does, there are VS Code plugins), configure these rules and have CI enforce them. A dependency rule violated today is technical debt tomorrow.

Testing: where hexagonal architecture shines

If there is one definitive argument for this architecture, it is testability. Domain logic, having no infrastructure dependencies, can be tested with fast, deterministic unit tests.

Domain tests. Pure, fast, no external dependencies. Test business rules with known input data and verify results. 500 tests in 2 seconds. No database, no server, no network.

Adapter tests. Test that adapters correctly implement ports. The persistence adapter is tested against a test database (or with testcontainers). The external API adapter is tested against a service mock. These tests are slower but necessary to verify integration.

Acceptance tests. Test the complete system, end to end. An HTTP request that traverses controller, use case, domain, adapter, database, and back. Few, slow, and focused on critical business flows.

The classic testing pyramid (many unit, some integration, few e2e) fits naturally with this architecture. Without it, all your tests need infrastructure and are slow. With it, 80% of your tests are pure unit tests that run in milliseconds.

Common mistakes

After implementing (and helping implement) this architecture on a dozen projects, certain mistakes recur:

Over-abstracting. Creating interfaces for everything, including internal domain logic that will never have another implementation. Ports are boundaries with the exterior. Inside the domain, simplicity wins.

Anemic domain. Putting all logic in use cases and leaving entities as plain data containers. If your Invoice entity cannot calculate its own total or validate its own consistency, your domain is anemic and the architecture’s benefits dilute.

Ports that mirror infrastructure. A findAllWithPaginationAndSortBy port is a repository disguised as an interface. The port should speak the domain’s language: getOutstandingInvoicesForClient, findOverduePayments.

Not actually inverting dependencies. Moving code into nicely named folders but keeping direct database imports in the domain. Without real dependency inversion, hexagonal architecture is cosmetic.

Adapters containing business logic. The persistence adapter that calculates discounts before saving an invoice. The external API adapter that decides whether to retry based on business rules. Adapters are translators, not decision-makers. If your adapter has an if statement that depends on a business rule, that logic belongs in the domain.

Hexagonal vs Clean Architecture vs Onion

It is worth clarifying the relationship between these three architectures, because the confusion is frequent. They are variations of the same principle (dependencies pointing toward the domain) with differences in nomenclature more than substance.

Hexagonal (Cockburn, 2005) talks about ports and adapters. The emphasis is that the domain is agnostic to the outside world, and the outside world connects through interfaces.

Clean Architecture (Martin, 2012) adds more layers (entities, use cases, interface adapters, frameworks) and formalizes the “dependency rule”: inner layers do not know about outer layers. In practice, the implementation is very similar to hexagonal with the application layer separated.

Onion Architecture (Palermo, 2008) uses the metaphor of concentric layers. The core is the domain, the outer layers are infrastructure. Same idea, different diagram.

For real projects, the choice between the three is irrelevant. What matters is the principle: the domain does not depend on infrastructure. If your team understands that principle, it does not matter what you call it. If they do not understand it, no name will fix that.

In our practice, we use hexagonal terminology (ports and adapters) because it is the most concrete. “Port” is clearer than “interface adapters layer.” But if your team comes from Clean Architecture and that terminology is familiar, use it. The fight over names is the least productive fight an engineering team can have.

When to start

If you are starting a new project and know the business logic will be complex, start with hexagonal from day one. The additional cost in the initial phase is modest (a few hours of setup) and the long-term benefit is enormous.

If you have an existing project with coupling pain, start with Step 1: extract the domain from a specific module. Do not try to refactor the entire system at once. Pick the module with the most business rules, isolate it, and use it as a model for the rest. Incremental refactoring works. Big-bang refactoring, in our experience, does not.

Hexagonal architecture is not an end in itself. It is a tool for keeping business logic understandable, testable, and modifiable as the system grows. If your system is not going to grow much, you probably do not need it. If it will grow (and most systems that survive do grow), it is the best engineering investment you can make.

About the author

A

abemon engineering

Engineering team

Multidisciplinary engineering, data and AI team headquartered in the Canary Islands. We build, deploy and operate custom software solutions for companies at any scale.