# Hexagonal Architecture

Placing domain logic at the centre with all external interaction through ports implemented by adapters, so the core depends on nothing outside itself.

**Rests on:** Dependency Inversion, Ports and Adapters

*Software Engineering · Software Architecture*


## Why learn this

This helps you untangle a messy request — one that comes in through a web page, gets logged, saves data, and sends an email — from a tangled blob into a clear core with four doors. It prepares you for the notes on Ports and Adapters, Dependency Inversion, and Separation of Concerns. It also makes later topics like Domain-Driven Design (structuring software around business rules), Distributed Systems (multiple programs working together across networks), and API Design (standardizing how programs talk to each other) easier to grasp. Without this view, you see only a knot of code. With it, you see the central logic separately from the tools used to reach it.


## The intuition

Imagine you run a small spice shop. You have a counter for customers, a storeroom at the back, and a phone line to suppliers. If you decide to sell online, you do not rebuild the storeroom or change how you talk to suppliers. You add a website. The website talks to the same rules: check the storeroom, take payment, arrange delivery. The storeroom does not care if the request comes from a person at the counter or a web form; it only cares that someone asked for a jar of turmeric. 

In software, these core rules are the *domain logic*. The way people reach them — the counter, the website, the phone app — are just different doorways. Each doorway is an *adapter*. The adapter translates the outside world into a format the core understands. Imagine the core is a bank manager reviewing loans. The manager’s rules for approval do not change based on whether the application comes on paper, by phone, or via an app. The manager has a standard form: name, income, debt, job tenure. Any channel that can fill this form works. In code, this standard form is called a *port* — an interface defined by the core that says, 'This is what I need.' The actual paper forms or screen layouts are the *adapters*. 

The core never talks to the database or sends emails directly. It asks for these services via ports. The outside world provides adapters to satisfy those requests. Draw the core in the center with connections radiating out, and you have the hexagonal diagram. The point is that you can change any door (database, email service, user interface) without touching the core rules. You can even test the core in a room with no real hardware or network.


## An analogy

Think of a car’s engine and its standard shaft output. The engine produces power through this shaft regardless of whether the car uses a manual gearbox, automatic, or electric motor. The engine does not care about the rest of the car; it only cares about turning the shaft. Similarly, the core defines what it needs (the port), and the outside world provides whatever fits (the adapter). Where the analogy ends: in software, you can update the core code while the system runs, and swap adapters without stopping the engine. Also, a car’s mechanical parts are fixed once built, but software ports are abstract definitions that can evolve as requirements change. The key rule remains: the core defines what is needed; the outside world must obey that definition.


## Definition

Hexagonal architecture structures software by placing business rules at the center and forcing all external interactions — such as databases, web interfaces, or messaging systems — to go through defined contracts (ports) implemented by interchangeable components (adapters).


## How it connects

This builds on Dependency Inversion (the core defines the interface, so it does not depend on concrete implementations like specific database drivers) and Separation of Concerns (keeping business logic distinct from technical details like HTTP or SQL). It refines layered architecture by using a center-periphery model rather than strict vertical layers. This structure supports maintainability (swapping infrastructure is easy), testability (testing the core without real hardware), and scalability. Neighboring topics include REST and GraphQL (which define specific types of API adapters) and Microservices (where one hexagon might interact with many others across a network).


## The mathematics

This concept involves no equations, but it uses a kind of logic that is closer to arithmetic than algebra: the rule that dependencies point inward. Think of it as a one-way street. The core is a building with a single door. A visitor from outside — say, a database driver — walks in through the door. The core can talk to the visitor, because the visitor has agreed to speak the core's language. But the core never leaves the building. It never walks out to the database's office. It never calls the database driver by name. It only knows the shape of the door, which is the port. The mathematics here is the same as the law 'if A depends on B, and B depends on C, then a change in C can affect A, but a change in A cannot affect C.' You can draw this as arrows on a diagram, and the rule is: arrows point inward only. There are no numbers to calculate, but the reasoning is rigorous: if you trace every arrow in a well-architected hexagon, you will find that from the core there are no arrows outward — only inward. That is a logical guarantee. The branch of mathematics that studies such guarantees is graph theory, which deals with networks of points (nodes) and arrows (edges) between them. This concept uses just one idea from graph theory: a directed graph has a direction on each edge, and a cycle is a path that starts and ends at the same point following the arrows. Hexagonal architecture forbids cycles: if the core points to the database, and the database points back to the core, that is a cycle, and it breaks the rule.

The dependency rule: for any two components A and B, if A depends on B, then B must not depend on A (directly or transitively). In graph terms: the dependency graph must be a directed acyclic graph (DAG). Symbolically: if dependency(A) = B, then dependency(B) ≠ A.


## A worked example

```
Consider a tiny system with three components: Core, WebAPI, and Database. We will draw arrows showing 'depends on'. Draw a circle labelled Core, a circle labelled WebAPI, and a circle labelled Database. Now add arrows: WebAPI depends on Core, so draw an arrow from WebAPI to Core. Database does not depend on Core; Core depends on Database's port, but our rule says Core depends on an interface, not the concrete Database. To show that, draw an arrow from Core to a small box labelled 'DatabasePort', and an arrow from Database to the same box, because Database implements the port. Now trace the arrows. From Core, you can only reach DatabasePort, not Database. From DatabasePort, you can reach nothing (it is a terminal). From WebAPI, you reach Core, then DatabasePort. There is no path from any component back to WebAPI. The rule 'no cycles' holds. If instead Core directly used Database, you would draw an arrow from Core to Database. Then from Core you reach Database, and if Database has a way to call Core (say, it notifies Core of changes), you draw an arrow from Database to Core, forming a cycle. This cycle is allowed to exist in reality, but it violates the pattern, and it is this violation that makes testing hard: to test Core you would need Database present.

Step 1: Step 1: Draw three nodes: Core, WebAPI, Database. Arrows: WebAPI → Core, Core → DatabasePort, Database → DatabasePort.   # WebAPI is the adapter; it calls into the core. The core only sees the port, not the concrete database.
Step 2: Step 2: Trace all paths starting from Core: Core → DatabasePort. That is the only path. No path leads back to Core.   # This shows there is no cycle involving the core.
Step 3: Step 3: If we violate the pattern by adding an arrow Database → Core (e.g., for a callback), then trace: Core → DatabasePort, but DatabasePort is not the same as Core. To have a cycle, Core must reach Database directly, but it cannot unless Core has a concrete reference.   # The pattern enforces no direct reference from core to concrete infrastructure, so a cycle is impossible by construction.

Result: A directed graph with no cycles is called a directed acyclic graph (DAG). Hexagonal architecture ensures the dependency graph of your whole system is a DAG, with the core as the sink — every path ends at the core or its ports.
```


## From scratch, in Python

```python
import json
import sqlite3
from abc import ABC, abstractmethod
from datetime import datetime

# --- The domain: a simple order system ---
class Order:
    def __init__(self, order_id, customer_name, amount):
        self.id = order_id
        self.customer_name = customer_name
        self.amount = amount
        self.status = 'created'

    def approve(self):
        self.status = 'approved'

    def reject(self):
        self.status = 'rejected'

# --- The ports: interfaces the core defines ---
class OrderSaver(ABC):
    @abstractmethod
    def save(self, order):
        """Save the order to whatever storage the adapter uses."""
        pass

class OrderNotifier(ABC):
    @abstractmethod
    def send(self, message):
        """Send a notification to the customer."""
        pass

# --- The core application service ---
class OrderService:
    def __init__(self, saver: OrderSaver, notifier: OrderNotifier):
        self.saver = saver
        self.notifier = notifier

    def submit(self, customer_name, amount):
        # generate a simple id based on timestamp
        order_id = int(datetime.now().timestamp() * 1000)
        order = Order(order_id, customer_name, amount)
        order.approve()  # hypothetical business rule: approve all small amounts
        self.saver.save(order)
        self.notifier.send(f"Order {order.id} approved for {order.customer_name}")
        return order.id

# --- Adapters: concrete implementations of the ports ---
class FileSaver(OrderSaver):
    def __init__(self, filename):
        self.filename = filename

    def save(self, order):
        with open(self.filename, 'a') as f:
            f.write(json.dumps(order.__dict__) + '\n')

class SQLiteSaver(OrderSaver):
    def __init__(self, db_path):
        self.conn = sqlite3.connect(db_path)
        self.conn.execute('CREATE TABLE IF NOT EXISTS orders (id INTEGER, customer TEXT, amount REAL, status TEXT)')

    def save(self, order):
        self.conn.execute('INSERT INTO orders VALUES (?,?,?,?)',
                          (order.id, order.customer_name, order.amount, order.status))
        self.conn.commit()

class EmailNotifier(OrderNotifier):
    def send(self, message):
        print(f"EMAIL: {message}")

class SMSNotifier(OrderNotifier):
    def send(self, message):
        print(f"SMS: {message}")

# --- Assembling the system: pick adapters at composition root ---
def choose_deps():
    # decide which storage to use (e.g., based on config)
    if False:  # switch to True to use SQLite
        saver = SQLiteSaver('orders.db')
    else:
        saver = FileSaver('orders.txt')
    notifier = EmailNotifier()
    return saver, notifier

saver, notifier = choose_deps()
service = OrderService(saver, notifier)

# --- Use it ---
order_id = service.submit('Priya', 2500)
print(f'Order submitted with id {order_id}')
```


## The same thing, with a framework

In a real project, you would reach for a dependency injection container (like Spring in Java, or a simple module in Python) to manage wiring, and you would use web frameworks (FastAPI, Express, etc.) as adapters on the input side. The concept is language- and framework-agnostic. In Python, you might use `fastapi` to define an HTTP adapter that calls the core service. But the core itself remains framework-free — it only imports the domain and port classes. The framework adapter would look like:

```python
# adapter_http.py
from fastapi import FastAPI, Request
from order_service import OrderService
from saver_adapters import SQLiteSaver
from notifier_adapters import EmailNotifier
import config

app = FastAPI()

@app.post("/orders")
async def submit(request: Request):
    body = await request.json()
    saver = SQLiteSaver(config.DB_PATH)
    notifier = EmailNotifier()
    service = OrderService(saver, notifier)
    order_id = service.submit(body['customer'], body['amount'])
    return {"order_id": order_id}
```

Underneath, `fastapi` is just a web server that parses HTTP requests and lets you call your code. The hexagon pattern plugs into it at the boundary: the HTTP request becomes an adapter call into the core. The core never imports `fastapi`.


## What the code does

This code is deliberately plain Python with no frameworks. It runs as is. The structure mirrors the architecture: core classes in the middle, port classes defining what the core needs, adapters implementing those ports, and a composition line at the bottom that wires them together. 

- `Order` is the domain object. It has attributes and business methods like `approve` and `reject`. It knows nothing about databases, files, email, or SMS.
- `OrderSaver` and `OrderNotifier` are the ports. They are abstract base classes (ABC) — the `@abstractmethod` decorator means you cannot create an instance of them directly. They only declare `save` and `send` methods, without any implementation. This is the 'airlock' the core relies on.
- `OrderService` is the core application service. It holds references to the ports (saver and notifier) but not to concrete adapters. Its `submit` method does the business logic — creating the order, approving it (a rule) — and then calls the saver and notifier. Notice it never mentions FileSaver or EmailNotifier.
- The adapters `FileSaver`, `SQLiteSaver`, `EmailNotifier`, and `SMSNotifier` each implement a port. They contain the actual details: writing to a file, inserting into SQLite, printing an email, etc. They live outside the core; they translate between the core's port interface and the real world's mechanics.
- The `choose_deps` function and the lines below it are the composition root — the single place where you decide which adapter to use. By changing the `if False` to `if True`, you switch from saving to a file to saving in SQLite, with zero change to the core.

If you want to test the core without any infrastructure, you can create a fake saver that just writes to a list and a fake notifier that records messages, insert them into `OrderService`, and run assertions. That is the testability win.


## Common misconceptions

**The hexagon must have six sides** — The diagram has to be literally a hexagon with exactly six entry points.

The six sides are an arbitrary choice by the pattern's inventor, Alistair Cockburn. The core can have four ports or ten. The important property is the shape — a centre and a periphery — not the number of edges. In fact: The hexagon is a mnemonic. It reminds you the core is surrounded by adapters. The number of ports is whatever the core needs to do its job.

**Adapters are just for I/O** — Adapters only handle user input and database output; everything else stays in the core.

Adapters can be any external system: message queues, file storage, time servers, notification services, even other internal components like caching layers. In fact: Any interaction with anything outside the core — hardware, other services, even a clock — should go through a port and adapter.

**Core means it doesn't change** — The core is immutable; once built, it stays the same forever.

The core changes when business rules change, which is exactly the point — the rules that matter evolve. Hexagon architecture makes those changes isolated. In fact: The core changes, but the change is contained to the domain layer, and the outer adapters can often stay untouched.

**Ports are an unnecessary extra layer** — Why define a port when you only have one database? Just use the database class directly.

If the core depends on a concrete class, swapping to a different database or a fake during testing requires changing the core. That violates the dependency rule and harms testability. In fact: The port is a seam. It costs a small extra step now but pays back in testability and the option to swap later without touching the core.

**Ports and adapters are the same as layers** — Hexagonal architecture is just layered architecture with different names.

Layers are a top-to-bottom stack (presentation, application, domain, infrastructure) with dependencies pointing downward. Hexagonal has a central core and a periphery; there is no notion of up and down, only in and out. In fact: Hexagonal is a different mental model: a target with the core in the middle and adapters arranged around it. It's not a stack.

**Hexagonal only works with OOP** — Language other than Java or C# can't do ports and adapters.

Ports and adapters map to any language. In Python it's often duck typing — core code accepts any object with the right methods. In Go it uses interfaces; in functional languages, it's often higher-order functions. In fact: The pattern is language-agnostic. The essence is that the core depends on contracts (types or interfaces) not on specific implementations.


## In short

Hexagonal architecture puts the business rules in the centre, defines ports — interfaces the core expects — and lets adapters outside translate between those ports and the real world. The rule is that dependencies point inward: the core never knows about databases, HTTP, or the UI. Everything it needs is behind a port. This makes the core testable without infrastructure, lets you swap databases or transport by replacing an adapter, and keeps the parts of the system that change frequently separate from the parts that are the heart of the business. Remember the analogy: the engine, the shaft, and the gearbox — the core is the engine, the port is the shaft, and the adapter is the gearbox. If you take nothing else away: draw the arrows inward, never out, and test the core as if the outside world were a friendly actor that obeys the port contract.


## Where this is used

**Large language models.** Imagine you are building a chat assistant powered by a huge LLM like GPT-4. The LLM itself is a model, not the whole system. Around it you have infrastructure: a prompt builder that reads conversation history, a vector database for retrieval, a safety filter, a telemetry service, and a UI. The core domain logic is 'how to answer the user' — the policies about which sources to ground on, how to refuse harmful requests, when to ask for clarification. That core should not know whether the UI is a web chat, a mobile app, or a Slack bot. It defines ports: `ConversationRetriever` (give me relevant documents given a query), `ResponseFilter` (is this response safe?), `UserProfileService` (what does this user prefer?). Input adapters implement these with actual vector databases, classifier models, and profile stores. Swapping the vector DB from Pinecone to FAISS is an adapter change. The core refusals logic is tested without any infrastructure present: you feed it a fake retriever that returns canned documents and assert it refuses correctly. When the safety team updates the filter, they change one adapter.

**Small language models on devices.** A model running on a phone or a laptop has the same architecture but with stricter constraints. The core is still the reasoning — say, a keyword spotter that decides whether the user said 'wake up'. The adapters are the audio capture (microphone driver), the inference engine (TensorFlow Lite or ONNX), and the response speaker. The core defines a port `SpeechRecognizer` and `CommandExecutor`. The audio adapter preprocesses audio frames into features; the inference adapter runs the model and returns a probability; the command executor turns a 'wake up' decision into a speaker beep. If you switch from a cloud-based model to a local one, only the inference adapter changes. The core logic about hysteresis (don't wake up on a one-off sound) stays the same and is testable by feeding synthetic audio signatures.

**Retrieval-augmented generation.** RAG is about augmenting an LLM with external documents. The core is the 'generate an answer grounded in these documents' algorithm. Ports: `DocumentRetriever` (given a query, return top-k text chunks), `Embedder` (convert text to vectors), `Cache` (store previous answers). Adapters implement retrieval with a vector database, embedding with a model like BGE, and caching with Redis. The RAG core does not care which vector DB — that is the adapter. If the embedding model is upgraded, the adapter changes and the cache must be invalidated (a subtle adapter concern). Testing the core means mocking the retriever: you assert that if the retriever returns certain chunks, the final answer doesn't quote other chunks. Without hexagon, the RAG service would be littered with `db.query(...)` calls, making it untestable and hard to swap.

**Agentic systems that plan and call tools.** An agent that plans actions and calls tools (web search, calculators, database queries) is a perfect hexagon candidate. The core is the planning loop — decides which tool to call next based on the goal and the observations so far. Ports: `ToolExecutor` (run this tool with these arguments), `ModelGateway` (send a prompt to an LLM and get a response), `Memory` (store past steps). Adapters implement with actual HTTP calls for tools, OpenAI/Claude/Mistral client libraries, and a vector store for memory. The agent's reasoning is independent of whether the tools are REST APIs, Python functions, or shell commands — those are all adapters. During development, tool adapters can be faked to return predetermined results so you can test the planning loop's decisions. In production, the real tools are called. If you add a new tool, you write a new adapter, not touch the planning core.

**Everyday applied machine learning.** Consider a recommendation system at an e-commerce site. The core decides which products to show given a user and context — the logic mixing collaborative filtering scores, popularity boost, and business rules like 'don't show out-of-stock'. Ports: `ProductCatalog` (get product metadata and stock status), `UserHistory` (get user's past interactions), `ScoringModel` (compute a raw score), `Tracking` (log what was shown). Adapters implement with a SQL database, an event stream, the ML model served by a separate service, and a telemetry pipeline. The core logic is pure, so you can unit test it with mocked adapters: feed it a user with history X and a catalog where item 7 is out of stock, assert it does not recommend item 7. When the ML team replace the scoring model, the adapter changes; the ranking rules (the core) remain. For fraud detection, the core decides whether to flag a transaction. Ports: `TransactionDB`, `RiskModel`, `NotificationService`. Adapters implement with a bank's ledger, a gradient boosting model, and an SMS gateway. The core's threshold rules are business policy and are tested without a database. If the fraud team moves from batch to real-time, the core stays; the adapter that queries the DB changes.

**What building with it looks like in practice.** A developer typically starts by identifying the core use cases. They write the domain entities and the application services, defining ports as interfaces for anything external. Then they write adapters, often in separate modules, and wire them at the composition root (often in `main.py` or a DI container). The frameworks they reach for are web frameworks (FastAPI, Spring Boot, Express) for input adapters, database drivers and ORMs for output adapters, and dependency injection tools to manage the wiring. The pain points are: too many ports (over-abstraction), where everything is an interface and you can't tell what the system does; lost domain logic leaking into adapters (a subtle violation); and the 'inner core' being polluted with framework types (a sign you haven't separated). What breaks when done badly: a change to the database requires touching multiple files that also handle HTTP and UI; the core needs a running database to test; swapping from Postgres to Mongo is a weeks-long project instead of a two-hour adapter change; the team argues about where to put a new feature because the boundaries are blurry.


---

Read this on the web: https://basavapurushottam.com/learn-ai/hexagonal-architecture
