Architecture foundations

3 min read ·

Hexagonal Architecture: Keep Business Rules Out of Infrastructure

A payment-provider change should not rewrite discount rules. When ports and adapters help separate business decisions from technology.

The company changes payment providers. The team expects to replace an integration, but discovers that the external API handler also contains discount calculations, order confirmation rules and pieces of refund logic. A technical replacement has become a product risk.

Hexagonal Architecture, or Ports & Adapters, is one way to break that dependency. Business rules stay in the centre; databases, HTTP endpoints, queues and external partners connect through explicit boundaries.

A port describes what the application needs

A port is a contract expressed in the application's own terms. A business flow needs to “take payment” or “store an order”; it should not need to know a provider URL, database table or SDK detail.

An adapter implements that contract for a particular technology. One day it may connect to provider A, another day to provider B. An incoming adapter might be an HTTP API, a scheduled job or a message handler, while the rules for confirming an order remain the same.

Dependency direction is the crucial point. Technical details depend on contracts defined inside the application, rather than domain rules depending on infrastructure accidents.

Where flexibility becomes real

In a long-lived product with multiple integrations, the benefit can be substantial. Teams can test pricing without a live payment provider, introduce a new order channel or change a persistence adapter without dragging technical details through the core.

But a port does not guarantee effortless vendor replacement. Providers may have different refund semantics, failure modes or settlement models. The adapter has to cope with those differences, and sometimes the business agreement itself must change.

How many interfaces are enough?

It is tempting to create a port for every method. Soon the codebase has dozens of one-use abstractions forwarding the same arguments. The team is paying for independence it may never need.

I would introduce ports at meaningful external dependencies or boundaries with genuinely different reasons for change. Simple internal calculations do not need an adapter merely because the architecture is drawn as a hexagon.

A practical test before adopting it

Ask the team to demonstrate two things: can the domain rule be tested without external infrastructure, and how much core code changes when an integration is replaced? If both are already easy, a major rewrite may be unnecessary.

When boundaries are unclear, start with business operations and module ownership rather than generating new interfaces. Layering can help, but layers alone do not guarantee that dependencies point the right way.

Ports & Adapters earn their cost when business rules need to outlive today's infrastructure. Build those boundaries deliberately, not as abstractions against every imaginary future change.