Hexagonal architecture: Ports and adapters guide for devs

Фото: Ekaterina Korol (Unsplash) — https://unsplash.com/photos/gray-concrete-building-during-daytime-kgsW2ci4ZqE?utm_source=dev48&utm_medium=referral

Source: Netguru•

Hexagonal architecture: Ports and adapters guide for devs

Hexagonal architecture keeps business rules in a domain core and pushes every database, API and framework out to replaceable adapters. The pattern isn't about hexagons. It's about where you draw the boundary between business rules and everything else, and how disciplined you stay about keeping…

Hexagonal architecture keeps business rules in a domain core and pushes every database, API and framework out to replaceable adapters. The pattern isn't about hexagons. It's about where you draw the boundary between business rules and everything else, and how disciplined you stay about keeping infrastructure on the far side of it.

Get that boundary wrong and you've built a layered architecture with extra ceremony. Below: the mechanics, a working Spring Boot example with tests, and when the pattern is worth its cost.

Hexagonal architecture, also called the ports and adapters pattern, is a way of structuring an application so the domain core never depends on external technology. The core defines interfaces, called ports. Adapters outside the core implement those ports for a specific database, message broker, UI or API.

Alistair Cockburn described the pattern in 2005. His stated intent was to let an application be driven equally by users, programs, automated tests or batch scripts, and to be developed and tested in isolation from its run-time devices and databases.

The dependency inversion principle is what makes this work. Dependencies point inward: adapters know about ports, ports belong to the core, and the core knows nothing about adapters. A driving adapter, such as a REST controller or CLI command, pushes work in. A driven adapter, such as a database repository or payment gateway client, carries work out. The core stays framework-agnostic in both directions.

Why is it called hexagonal architecture?

The name comes from the diagram, not from the number six. Cockburn drew the application as a hexagon so there would be room to place as many ports and adapters around it as a system needs, instead of forcing everything into the top-to-bottom stack of a layered drawing.

A system with three external dependencies is still hexagonal architecture, and so is one with nine. The shape signals symmetry: inbound and outbound integrations sit on the same kind of boundary rather than on a "front" and a "back" of the application.

Treat the hexagon as a teaching aid, not a spec to satisfy. Teams that try to fill six sides end up inventing ports they don't need. The decisions that matter are about the direction of dependencies, not geometry.

Annotated diagram: Domain core, ports, and adapters

Picture the domain core as a hexagon in the center of the diagram, containing nothing but plain objects and business rules. No framework imports, no ORM annotations, no HTTP client. Around its edge sit the ports: interfaces the core defines but never implements.

On the left, driving adapters push work into the system. A REST controller, a message consumer, a CLI command and a scheduled job each call an inbound port, which triggers an application service.

On the right, driven adapters do the opposite. The core calls an outbound port, and a Postgres repository, an S3 client or a Stripe gateway implements it on the other side. The core never imports either kind of adapter directly.

The failure the pattern prevents is business logic tangled into integration code in the same class. When that happens, a database change forces you to rewrite validation rules. With a clean port in place, swapping a driven adapter leaves the core untouched, because the contract it depends on hasn't changed.

Driving adapters vs. driven adapters

A driving adapter calls into the domain core; a driven adapter gets called by it. That direction of the call is the whole test for which side of the hexagon you're looking at, and it decides how you wire dependency injection in a framework like Spring Boot.

On the driving side, a REST controller translates an HTTP request into a command object and hands it to an inbound port. A message consumer does the same for a Kafka event, and a CLI command for a terminal invocation. None of them contain business rules. They adapt the outside world's shape into a call the core understands.

On the driven side, a repository is the clearest example. The core defines an outbound port, OrderRepository, as an interface with methods like findById and save. A Postgres adapter implements it with JPA. A DynamoDB adapter could implement the same interface, and swapping one for the other changes neither the core nor its unit tests.

A common anti-pattern is an adapter that quietly absorbs business logic because it was faster than defining a proper port. If a rule would have to be duplicated when you add a second adapter, it belongs in the core.

Use case example: Placing an order

A request to place an order lands on a REST controller, which maps the JSON body into a PlaceOrderCommand and calls the inbound port, PlaceOrderUseCase. The implementation, PlaceOrderService, builds a new Order aggregate, enforces invariants such as a positive quantity, and marks the order as reserved. Nothing in that class imports Spring Boot, JPA or a payment SDK.

The service then calls the outbound port, OrderRepository, to persist the aggregate. In production, a driven adapter implements that port against Postgres. In tests, an in-memory implementation stands in for it, so assertions run against the aggregate's state directly, with no database and no HTTP.

That test-in-isolation property is exactly what Cockburn designed the pattern for, and it's what the rest of this guide builds on.

Implementing hexagonal architecture in Spring Boot

Spring Boot doesn't force a hexagonal architecture, but constructor injection and flexible package layouts make the ports and adapters split cheap to enforce. The domain and application code carry no Spring annotations at all: no @Service, no @Component. Only adapters and configuration touch the framework.

If you're building on the JVM and want a second pair of hands on this kind of structure, Netguru's Java development services cover it.

A typical layout looks like this:

The outbound port is a plain interface that the application layer owns, not the persistence package:

JpaOrderRepositoryAdapter implements it, maps between Order and a JPA entity, and carries every Spring Data annotation. Neither the domain nor the application package imports jakarta.persistence, so the dependency inversion principle holds: the core defines the contract and the driven adapter satisfies it.

Wiring happens in a configuration class, outside the core:

Spring finds JpaOrderRepositoryAdapter because the adapter itself is annotated with @Repository, and injects it wherever an OrderRepository is required. The container assembles the graph, but every dependency still points inward toward the domain.

The most common way this breaks in Spring Boot codebases is @Transactional and JPA entities creeping into the application service because it's faster in the short term. Once the service imports persistence types, the hexagon is decorative rather than structural. A build rule (for example an ArchUnit test that fails if application or domain packages import org.springframework or jakarta.persistence) keeps the boundary honest.

How hexagonal architecture simplifies testing

The domain core has no dependency on a database driver, message broker or HTTP client. Unit tests exercise business rules directly against plain objects: no Spring context, no Testcontainers, no network calls. Isolation is structural, not a matter of discipline.

For outbound ports, write a test double instead of standing up a real Postgres instance. A hand-rolled in-memory repository that implements the same port as the production adapter is enough to verify domain behavior:

That test needs no Spring Boot context, no schema migration and no Docker daemon. Because the domain module doesn't pull Spring or JDBC onto its test classpath, suites like this stay fast as the codebase grows.

Compare that with domain logic that calls a JPA repository class directly. Every unit test then needs a mocking framework to stub persistence internals, or an integration test with a real database.

Ports push that complexity to the edges. Driving adapters get tests against the inbound port, driven adapters get contract tests that run the same scenarios against the in-memory double and the real implementation, and the domain core stays free of both.

When should you use hexagonal architecture?

Hexagonal architecture earns its complexity when the domain has business rules worth protecting and more than one delivery mechanism or outbound integration that changes on its own schedule. A single CRUD service on one database rarely needs it. A plain layered architecture does the job with less indirection.

A good candidate looks like an order service that takes REST calls from a web app, consumes events from a warehouse system and runs a nightly reconciliation batch against a payment provider. That's three driving adapters and at least two driven ones, all changing on different schedules around the same business rules.

The pattern pairs naturally with domain-driven design. Vaughn Vernon's Implementing Domain-Driven Design presents ports and adapters as a fitting architecture for a bounded context, and in practice each bounded context usually deserves its own hexagon rather than one shared core stretched across contexts. Wrapping ports and adapters around code with no real domain logic adds indirection without buying isolation.

It's also a good target when modernizing a legacy system.

You can carve one capability at a time out of a monolith, give it a clean core with ports, and route traffic to it gradually, which is the approach described in our strangler fig pattern guide. Teams planning that kind of migration as part of a larger product build can look at Netguru's software development services.

Pros and cons of hexagonal architecture

Hexagonal architecture trades short-term speed for long-term flexibility. It isolates the domain core behind ports and adapters, but it adds interfaces, mappers and test doubles that a plain layered service wouldn't need.

The payoff is the dependency inversion principle applied at the architecture level, not just the class level. Business rules never import an adapter directly. They depend on interfaces the core owns, so moving from one persistence technology to another touches only the adapter.

The extra layers and boilerplate also slow down initial development compared with a layered design. For a small application whose infrastructure will never change, that cost may never pay back.

The most common failure is teams drawing ports on a diagram but letting framework types leak into the core anyway. Persistence decisions then sneak into business logic, and the architecture delivers none of the isolation it promised.

Hexagonal architecture vs. clean and onion architecture

Hexagonal architecture, clean architecture and onion architecture all apply the same dependency inversion principle: the domain sits at the center and every dependency points inward. The differences are mostly in vocabulary and in how prescriptive each one is about internal layers. For how they fit alongside layered, event-driven and microservices designs, see our software architecture patterns guide.

Onion architecture, described by Jeffrey Palermo in 2008, draws the application as concentric layers around a domain model, with domain services and application services in their own rings. Clean architecture, published by Robert C. Martin in 2012, uses a similar set of rings (entities, use cases, interface adapters, frameworks and drivers) and a strict rule that source code dependencies only point inward.

Hexagonal architecture, as Cockburn defined it in 2005, is the least prescriptive of the three. It says nothing about layers inside the core. It only separates the inside from the outside and names the two kinds of adapters: a driving adapter calls into the core, and a driven adapter gets called by it.

In practice, many codebases mix them: hexagonal vocabulary at the boundary, clean-architecture use cases inside. A team comfortable with dependency injection can apply any of the three. The boundary decisions matter more than the name.

Hexagonal architecture in microservices design

Hexagonal architecture and microservices fit together when each service owns exactly one bounded context, with its own domain core and ports and adapters guarding its edge. Model the service around a business capability, not a database table, and the boundary lines up almost automatically.

Cross-context integration is where teams stumble. When one service calls another's API, or ingests events from a legacy system with different naming and invariants, the core needs an anti-corruption layer at the boundary. That's a translation adapter behind a port, not just a REST client. It becomes the single point where two bounded contexts actually meet.

The anti-pattern is a set of services with thin domain cores that share database schemas across service boundaries. That gives you the operational overhead of distributed systems without the isolation hexagonal design is supposed to provide.

Adapter swaps also stay cheap at the service level. If the port is clean, moving a driven adapter from one cloud provider's queue to another's is new work at the edge rather than a domain rewrite. The hard part is restraint: knowing where to draw a bounded-context line, not writing another adapter class.

FAQ: Hexagonal architecture questions answered

Is hexagonal architecture DDD?

No, but they work well together. Domain-driven design is a way of modelling a business domain (bounded contexts, aggregates, ubiquitous language). Hexagonal architecture is a way of structuring an application so that model doesn't depend on infrastructure. Many DDD practitioners use a hexagon per bounded context.

What are the disadvantages of using a hexagonal architecture?

It adds interfaces, mapping code and test doubles, which slows down initial development and steepens the learning curve for teams new to dependency inversion. For small CRUD applications with one database and no expected infrastructure changes, that overhead often outweighs the benefits.

Is hexagonal architecture still relevant?

Yes. The same idea underpins clean and onion architecture, and it maps well onto microservices, serverless functions and multi-cloud deployments, where the infrastructure around a piece of business logic changes more often than the logic itself.

How are DTOs used in hexagonal architecture?

DTOs carry data across a port boundary so the domain core never depends on a driving adapter's request format or a driven adapter's persistence schema. A REST controller maps its JSON payload into a command or DTO, and the application layer maps that into domain objects. Skipping that mapping is how framework details leak into business logic.

Is hexagonal architecture the same as microservices?

No. Hexagonal architecture structures the internals of a single application, and microservices split a system into independently deployable services. You can build a monolith or a microservice with hexagonal architecture inside, and many teams do both.

Putting hexagonal architecture to work

Start small. Pick one part of the domain that faces real infrastructure change, such as a payment provider, a database you expect to outgrow or a second delivery channel, and give it a clean core with explicit ports. Enforce the boundary with a build rule, test the core with in-memory adapters, and expand only as the benefits show up.

If you're planning a new system or untangling an existing one and want help drawing those boundaries, tell us about your project.

What this article says