Skip to main content
Home
HexagonalGuru
Software Architecture

Hexagonal Architecture: Ports and Adapters Explained with Real Examples

HexagonalGuru's avatar

HexagonalGuru

Hexagonal Architecture: Ports and Adapters Explained with Real Examples

The problem everyone recognizes

Every team that has maintained software for a few years has lived the same story: the project starts out clean, but over time business logic gets tangled up with the framework, the database, controllers, queues and external integrations. One day, changing any piece — migrating the database, switching the email provider, upgrading the framework — means touching half the project, and the fear of breaking something paralyzes technical decisions.

Hexagonal architecture exists precisely to avoid that ending. It is not a fad or an academic ritual: it is a concrete way of organizing code so that what matters (your business) doesn't depend on what is incidental (the technology).

What hexagonal architecture is

Hexagonal architecture — also known as Ports & Adapters — was described by Alistair Cockburn in 2005. Its core idea is simple: place the domain (the business rules, what makes your application unique) at the center of the system, completely isolated from the outside, and connect it to the real world through two kinds of pieces:

  • Ports: interfaces defined by the domain itself. They declare what the business needs ("store an invoice", "send a notification") without saying how it gets done.
  • Adapters: concrete implementations of those ports. An adapter knows how to talk to MySQL, to Redis, to a third-party API or to the console. The domain doesn't know — and doesn't care — which one is plugged in.

The hexagon is just a visual metaphor: each "side" represents a connection point with the outside. What matters is not the shape but the dependency rule: dependencies always point inwards. Infrastructure depends on the domain, never the other way around.

The three layers, in practice

  • Domain: entities, value objects and pure business rules. Zero framework imports, zero SQL, zero HTTP. It is code you could still run in twenty years on different technology.
  • Application: the system's use cases (create a quote, publish an article). It orchestrates the domain and defines the ports it needs. Patterns like command handlers and CQRS fit here.
  • Infrastructure: everything technical — HTTP controllers, Doctrine repositories, API clients, queue workers — implementing the ports that domain and application have defined.

A minimal example

Imagine a "publish an article" use case. The port is defined by the application:

interface ArticleRepositoryInterface
{
    public function save(Article $article): void;
}

The handler only knows that interface:

final readonly class PublishArticleHandler
{
    public function __construct(
        private ArticleRepositoryInterface $repository
    ) {}

    public function __invoke(PublishArticleCommand $command): void
    {
        $article = $this->repository->findById($command->id);
        $article->publish(); // the business rule lives in the domain
        $this->repository->save($article);
    }
}

And in infrastructure lives the adapter that talks to MySQL via Doctrine. Want to switch databases tomorrow or add a cache? You write a new adapter and plug it in. The domain and the use case never notice.

The real benefits it delivers

After years of applying it on all kinds of projects, these are the benefits our clients actually feel:

  • Radical testability: the domain is tested without booting a framework, a database or containers. Business tests are fast and stable, which means fewer regressions and safer releases.
  • Replaceable technology: changing the database, the cloud provider or the framework stops being a project in itself. You rewrite an adapter, not the application.
  • Technical debt under control: the structure forces business rules to live in a single, explicit place. New code can't "hide" in controllers.
  • Teams that scale: with clear boundaries between layers, several people can work in parallel without stepping on each other, and new hires understand the structure in days, not months.
  • Postponable decisions: you can start simple (a local adapter) and switch to the serious option (S3, a queue, an external provider) when the business asks for it, without rewriting anything.

When we do NOT recommend it

Honesty is also part of expertise. Hexagonal architecture adds structure, and structure costs: more interfaces, more classes, more discipline. There are cases where it doesn't pay off:

  • Disposable prototypes and MVPs: if the goal is to validate an idea in two weeks, building layers slows you down. That said: if the prototype sticks, refactor soon, because that "temporary" code always ends up in production.
  • Trivial CRUDs with no business rules: a small maintenance backoffice with four forms doesn't need ports or adapters.
  • One-off scripts and tools.

For everything else — products that will live for years, systems that integrate third parties, platforms with multiple teams — the investment pays for itself many times over starting from the first big change.

How we apply it

This is not theory: it is our standard way of working. Our entire platform is built with hexagonal architecture, domain-driven design and bounded contexts deployed as independent microservices. Combined with CQRS and domain events, it enables things that would be unthinkable with a traditional architecture: replacing a service's file storage with object storage without touching a line of business code, or shipping a new context to production without redeploying the others.

In fact, the blog you are reading is served from one of those microservices, built with exactly the principles we just described.

Does it make sense for your project?

If your application has years of development behind it, every change costs more than the last one, and the team has stopped proposing improvements "because it's too risky", the problem is almost never the team: it's the architecture. The good news is you don't need to rewrite from scratch — you can migrate gradually, starting with the modules that hurt the most.

If you want an honest assessment of your case, talk to our team: we'll review the state of your architecture, tell you what would improve with a migration and — just as importantly — what doesn't need to be touched.

  • #ddd
  • #microservices
  • #hexagonal-architecture
Related articles
Shall we start?

Ready to build something that grows with your business?

Tell us your goals and together we will map the design, development and technology route that takes you from vision to measurable results.