Özer Cem KELAHMETKELAHMET
Tools · Architecture

Hexagonal architecture

Hexagonal architecture keeps business logic independent of databases, APIs and frameworks by connecting them through ports and adapters. Learn the pattern.

Overview

Hexagonal architecture, also called ports and adapters, is a way of structuring an application so that its business logic does not depend on databases, web frameworks, message brokers or external APIs. The core of the application defines what it needs and what it offers through interfaces called ports. Technology-specific code, called adapters, plugs into those ports from the outside. The core can then be run and tested with a web interface, a command line, a test harness or a batch job, and it can store data in PostgreSQL, a file or memory, without changing.

An everyday analogy is a travel adapter. Your laptop has one kind of plug. In different countries, the wall sockets differ. You do not rewire the laptop for each country; you carry adapters that convert the local socket to the plug the laptop expects. In hexagonal architecture, the laptop is your business logic, its plug is a port, and each adapter connects it to a particular technology.

The pattern matters for any system expected to live for years. Frameworks, databases and model providers change faster than business rules. For AI applications it is especially useful: a service that calls a language model, a vector store and a feature store can define ports such as Embedder or Retriever, and swap Qdrant for pgvector, or one model provider for another, by changing an adapter rather than the core logic.

What it is

The pattern has a small vocabulary.

Part What it is Example
Application core Business logic and use cases, free of framework and infrastructure code ApproveLoan use case and loan rules
Port An interface owned by the core describing an interaction LoanRepository, CreditScoreProvider, ApproveLoanUseCase
Driving (primary) port What the outside world can ask the application to do ApproveLoanUseCase.approve(id)
Driven (secondary) port What the application needs from the outside world CreditScoreProvider.scoreFor(customer)
Driving adapter Code that receives external input and calls a driving port REST controller, CLI command, message consumer, test
Driven adapter Code that implements a driven port with real technology SQL repository, HTTP client to a credit bureau, in-memory fake
Configurator The place where adapters are wired to ports at startup A main function or dependency injection setup

The hexagon shape is only a drawing convention. It leaves room to draw several ports on different sides; the number six has no meaning. The essential rule is about the direction of dependencies: adapters depend on the core, and the core depends on nothing outside itself.

Where it came from

Alistair Cockburn, known for his work on use cases and agile methods, developed the idea from the mid-1990s and published it as "Hexagonal Architecture" in an article in 2005, later preferring the name "Ports and Adapters". His aim 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 eventual devices and databases. In 2024 Cockburn and Juan Manuel Garrido de Paz published a book, "Hexagonal Architecture Explained", setting out the pattern in detail.

Related ideas followed. Jeffrey Palermo described onion architecture in 2008, and Robert C. Martin described clean architecture in a 2012 article and a 2017 book. All three share the same core principle: dependencies point inwards towards the business logic. Hexagonal architecture became common in domain-driven design and microservices circles in the 2010s.

How it works

1. Model the use cases

Start from what the application does: approve a loan, place an order, answer a question. Express each as a driving port, an interface such as PlaceOrder, implemented by an application service in the core.

2. Identify what the core needs

List what the use cases require from the outside: store orders, charge a card, send an email, get an embedding. Express each as a driven port in the core's own terms, such as PaymentGateway.charge(amount, card), not in terms of a vendor's SDK.

3. Implement the core

Write business rules against the ports only. The core imports no web framework, ORM or cloud SDK. This makes it plain code that runs quickly in unit tests.

4. Write adapters

For each driving technology, write an adapter that converts input into a port call: an HTTP controller parses JSON and calls PlaceOrder. For each driven port, write one or more adapters: a SQL repository, a payment provider client, an in-memory fake for tests.

5. Wire at startup

A configurator creates adapters and passes them into the core. Production wiring uses real adapters; test wiring uses fakes. Changing a database means writing a new adapter and changing the wiring.

6. Test from the outside in

Tests drive the core through its driving ports with fake driven adapters, checking behaviour without infrastructure. Separate contract tests check each real adapter against its port.

A short Python sketch shows the dependency direction:

from typing import Protocol

class CreditScoreProvider(Protocol):          # driven port, owned by the core
    def score_for(self, customer_id: str) -> int: ...

class ApproveLoan:                            # use case in the core
    def __init__(self, scores: CreditScoreProvider):
        self.scores = scores
    def approve(self, customer_id: str, amount: int) -> bool:
        return self.scores.score_for(customer_id) >= 650 and amount <= 50_000

class FakeScores:                             # driven adapter for tests
    def score_for(self, customer_id: str) -> int:
        return 700

Key concepts and terms

Term Meaning
Ports and adapters The original alternative name for the pattern.
Port An interface defined by the core for an interaction.
Adapter Technology-specific code connecting a port to the outside world.
Driving side Actors that start interactions with the application, such as users or tests.
Driven side Systems the application uses, such as databases or APIs.
Dependency inversion High-level code depends on abstractions it owns, not on low-level details.
Use case One thing the application does for an actor.
Configurator Startup code that connects adapters to ports.
Test double A fake, stub or mock used in place of a real adapter.
Contract test A test that checks an adapter honours its port's expectations.

A worked example

This scenario is illustrative. A team builds an internal assistant that answers policy questions. Version one is a single script: a web handler embeds the question with one provider's SDK, queries a hosted vector database, builds a prompt and calls a language model. Tests require network access and API keys, take 40 seconds and fail randomly.

They restructure it. The core defines one driving port, AnswerQuestion, and three driven ports: Embedder, Retriever and LanguageModel, each described in the assistant's own terms. The use case retrieves the top 5 passages, refuses to answer if the best relevance score is below 0.3, and otherwise asks the model to answer with citations. Adapters wrap the real SDKs. Tests use fakes: a fixed embedder, an in-memory retriever with ten passages and a scripted model.

The test suite now runs 120 cases in under 2 seconds without network access. Three months later the company decides to move retrieval into its existing PostgreSQL with pgvector. The team writes one new Retriever adapter, runs the shared contract tests against it, and changes one line of wiring. The core and its tests are untouched. A command-line driving adapter added later lets analysts run batch evaluations through the same use case.

Strengths and limitations

Strengths:

  • Business logic is isolated, readable and fast to test.
  • Infrastructure can change with limited, local impact.
  • The same core can be driven by APIs, CLIs, queues and tests.
  • Encourages clear thinking about what the application needs.
  • Fits naturally with domain-driven design and microservices.

Limitations:

  • More interfaces and files than a simple layered application.
  • Overkill for thin CRUD services with little logic.
  • Port design is hard; ports that mirror one vendor's API defeat the purpose.
  • Mapping data between adapter and core models adds code.
  • Teams may disagree about what belongs in the core versus adapters.

How it compares with alternatives

Style Key idea When it fits better
Layered (n-tier) architecture Presentation, business and data layers stacked, often with business depending on data access Simple applications and teams familiar with layers
Onion architecture Concentric rings with the domain model at the centre Same goals; preferred by some DDD teams
Clean architecture Entities, use cases, interface adapters, frameworks in rings Larger codebases wanting explicit layer rules
Framework-centric design Business logic lives inside framework controllers and models Prototypes and short-lived applications
Microservices Deployment-level decomposition Complementary: each service can be hexagonal inside

Practical guidance and best practices

  1. Define ports in the language of the domain, not in terms of a specific vendor's API.
  2. Keep framework annotations, ORM classes and SDK types out of the core.
  3. Write an in-memory adapter for each driven port and use it in most tests.
  4. Add contract tests that run the same checks against fakes and real adapters.
  5. Keep adapters thin: translation and technical concerns only, no business rules.
  6. Do the wiring in one place so the architecture is visible.
  7. Apply the pattern where logic is substantial; keep thin services simple.
  8. For AI components, wrap model providers, vector stores and tools behind ports so evaluations can run offline.

Common mistakes

  • Creating ports that are one-to-one copies of a vendor SDK, so swapping is still impossible.
  • Leaking database entities or HTTP request objects into the core.
  • Putting business rules inside adapters, such as validation in controllers.
  • Building a port and adapter for everything, including trivial utilities.
  • Mocking everything in tests instead of using simple, realistic fakes.

Frequently asked questions

Why is it called hexagonal?

Because Cockburn drew the application as a hexagon to leave space for several ports around the edge. The number of sides has no technical meaning, which is why he also uses the name ports and adapters.

Is hexagonal architecture the same as clean architecture?

They share the principle that dependencies point towards the business logic. Clean architecture prescribes more named layers; hexagonal architecture focuses on the inside-outside split and on ports.

Does it require dependency injection frameworks?

No. Passing adapters into constructors from a main function is enough. Frameworks can help with large applications but are optional.

Is it suitable for microservices?

Yes. Each microservice can be structured hexagonally, so its business logic stays independent of its transport and storage choices.

How does it help with AI systems?

Model providers, vector databases and tools change frequently. Placing them behind ports lets teams swap them, run deterministic tests with fakes and evaluate behaviour without network calls.

How many ports should I have?

As many as the application has distinct conversations with the outside world, usually a handful per bounded context. If you have dozens of tiny ports, they are probably too fine-grained.

Glossary

  • Hexagon: the drawing used to show the application core and its ports.
  • Port: an interface owned by the application.
  • Adapter: an implementation connecting a port to technology.
  • Primary adapter: a driving adapter that calls into the application.
  • Secondary adapter: a driven adapter that the application calls.
  • Core: the business logic at the centre.
  • Dependency rule: outer code may depend on inner code, not the reverse.
  • Fake: a simple working implementation used in tests.
  • Wiring: connecting chosen adapters to ports at startup.

Summary

Hexagonal architecture, published by Alistair Cockburn in 2005 as ports and adapters, keeps business logic at the centre and connects it to the outside world through interfaces the core owns. Adapters translate between those ports and specific technologies such as web frameworks, databases and model providers. The result is a core that is easy to test and that survives technology changes. Use it where logic is substantial, design ports in domain terms, keep adapters thin and test both the core with fakes and the adapters with contract tests.

It belongs to Architecture in my stack, under MLOps and AI infrastructure. Others in the same group:

Type to search.
    ↑↓ navigate↵ open