szpak szpak

Ports and Adapters: Hexagonal Architecture Explained

Ports and Adapters keeps domain logic isolated from frameworks, APIs, and databases, making systems easier to test and change.


You change one business rule. It should be a small job.

Instead, you end up editing an HTTP controller, a validation layer, a repository, a framework-specific service, and a database query hidden in a place nobody remembers. By the time the feature works, you are no longer confident about what you changed or why.

That is exactly the kind of mess Ports and Adapters tries to prevent.

The Problem This Architecture Solves

Most business applications do roughly the same three things:

  • receive input
  • execute business rules
  • store or send data somewhere else

The trouble starts when all three concerns are welded together.

If your rules depend directly on a web framework, ORM, queue client, or external API SDK, every technical decision leaks into the business core. The result is predictable: tests get harder, upgrades get riskier, and simple changes stop being simple.

Ports and Adapters cuts that dependency path on purpose. The domain says what it needs. The outer layers decide how to provide it.

What Ports And Adapters Actually Means

The name is more useful than the nickname hexagonal architecture, because it tells you where to look.

A port is a contract. It describes how the domain or application layer wants to talk to the outside world.

An adapter is a concrete implementation of that contract. It could be an HTTP controller, a PostgreSQL repository, a message publisher, a CLI command, or an API client.

The important part is not the class diagram. The important part is direction.

Business rules should depend on abstractions they own. Infrastructure should depend on those abstractions too, but from the outside. That keeps the core focused on policy instead of plumbing.

Two Directions Of Ports

You will usually see two kinds of ports.

Inbound Ports

Inbound ports are the ways the system is driven.

They receive a request to do something and pass it into the application or domain logic. That request might come from an HTTP endpoint, a background job, a CLI command, or an event consumer.

In other words, inbound ports answer this question:

How does work enter the system?

Outbound Ports

Outbound ports are the dependencies the logic needs in order to finish the job.

Repositories, external APIs, message brokers, caches, file storage, payment gateways, all of them fit here. The core does not need to know whether the adapter uses PostgreSQL, Redis, Kafka, or a sleepy third-party REST API from 2014. It only needs the capability.

That question is different:

What must the system call in order to complete the use case?

Where DDD Fits In

Ports and Adapters works very well with Domain-Driven Design, but it does not require a full DDD ceremony to be useful.

If you are modeling behavior around an Aggregate, the fit is especially natural. The domain can define ports for persistence through a Repository, create objects through a Factory, and keep business rules inside entities or services without dragging framework code through every decision.

That said, you do not need to start speaking in strategic patterns before breakfast to benefit from this architecture. If the domain has meaningful rules and the infrastructure changes more often than you would like, the pattern already has a job to do.

Why Teams Keep Coming Back To It

The first benefit is testability.

When business logic talks to ports instead of concrete tools, you can test rules without booting the whole world. You do not need a real database just to check whether a user can reset a password or reserve a slot. A focused fake or stub is usually enough.

The second benefit is that technical decisions can move later.

That matters more than it sounds. Early in a project, people love making confident infrastructure choices with very weak evidence. Then reality arrives and politely ruins the plan. If your storage or integration details live behind outbound ports, changing course is painful, but still possible. If those details leaked into the core, the migration becomes a group exercise in regret.

The third benefit is maintainability.

Systems built this way tend to age better because the blast radius of change stays smaller. A framework upgrade remains an adapter problem. A new transport remains an adapter problem. A business rule remains a business rule problem. That is the separation you want.

Why People Call It Hexagonal

The hexagon is just the drawing. It exists to show that the core can have many entry points and many outbound dependencies without pretending everything must sit in a strict left-to-right layered stack.

Useful picture. Slightly overhyped name.

Ports and Adapters tells the story more clearly.

Frameworks Belong At The Edge

Using a framework is not a sin. Using it as your application’s personality usually is.

Frameworks are good at delivery concerns: routing, serialization, middleware, dependency wiring, retries, scheduled jobs, and all the other glue work. Let them do that. Just do not let them become the place where your business rules go to disappear.

That is one of the reasons I like this pattern more than vague calls to “keep it clean.” It gives you an explicit answer to a practical question: where does this code belong?

If it expresses policy, it belongs in the core. If it speaks a protocol, a database, or a framework, it belongs in an adapter.

The moment those lines blur, maintenance gets expensive.

Final Thought

Ports and Adapters is not architecture theater. It is a way to stop infrastructure choices from colonizing business logic.

If a simple rule change forces you to touch five unrelated layers, the architecture is already telling you something. You should listen.