# Ports and Adapters

Ports are interfaces the core defines for what it needs; adapters are the outside implementations that satisfy them.

**Leads to:** Hexagonal Architecture

**Rests on nothing.** A place to start.

*Software Engineering · Software Architecture*


## Why learn this

This unlocks the ability to design AI systems that outlive their infrastructure. When you build a model or an agent that talks to a database today and a different database tomorrow, the mechanism at this boundary is what lets you swap the database without rewriting the model logic. It is the foundation for Hexagonal Architecture, Dependency Inversion, and the qualities (maintainability, testability, scalability) that make real AI services deployable. Without it, every model you write is welded to one specific tool — and the moment that tool changes, the model does not work anymore.


## The intuition

Think of a power outlet in your wall. The outlet does not care whether you plug in a lamp, a phone charger, or a fan — it only cares that the plug fits the socket. The socket is a port: it defines what the wall will provide (electricity at a certain voltage and shape), and any device that follows that shape can connect. The device itself is an adapter: it takes the raw electricity and translates it into whatever the device needs — light, charge, or air movement. In software, a port is an interface the core (the heart of your AI system) declares: 'I need to store this data,' or 'I need to fetch this user profile.' An adapter is the concrete code that actually does it, for example talking to a specific database like MySQL or Postgres. The core never talks to the database directly; it talks to the port. That way, if you change the database, you only write a new adapter — the core stays untouched.


## An analogy

Imagine you run a restaurant. The kitchen (the core) needs ingredients. It does not care who delivers them, as long as they arrive at the back door in the right containers. The delivery person is an adapter — they take produce from a local farm, a warehouse, or a supermarket and put it into the kitchen's standard crates. If the farm goes out of business, you find a new supplier and give them the same standard crates — the kitchen never changes. The port is the specification of the crates and the delivery schedule; the adapters are the different suppliers. Now where does this analogy break down? In the restaurant, the kitchen also cooks — it produces something new. In a pure port-adapter system, the core only defines what it needs, and the adapter is a passive translator — it does not add business logic. Also, in a real system there can be many ports (the kitchen needs vegetables, meat, and spices, each with its own crate), and many adapters per port (two suppliers for vegetables). The analogy also misses the idea that the port is defined by the core, not by the outside — in the restaurant, the back door standard is set by the kitchen, not by the suppliers.


## Definition

A port is an interface that the core defines for what it needs from the outside world; an adapter is a concrete implementation of that interface that translates between the core's language and a specific external tool, framework, or service.


## How it connects

This concept is the practical expression of Dependency Inversion, which you have notes on: the principle where the interface belongs to the consumer (the core), not the provider (the outside tool). It is also the same idea as Layered Architecture's dependency rule — outer layers may know inner layers, never the reverse — and it is the building block of Hexagonal Architecture, where the core sits at the centre and all external interaction passes through ports and adapters. It directly supports Cohesion and Coupling: high cohesion inside the core, loose coupling between the core and the outside. If you change a database, you write a new adapter — you do not touch the core, which is exactly what Separation of Concerns and Single Responsibility are designed to achieve.


## The mathematics

This concept does not use mathematics beyond arithmetic — it is a structural idea, not a quantitative one. The branch of mathematics it faintly touches is set theory, in the sense that a port is a specification of a set of allowed behaviours (what operations the outside world must support), and an adapter is an element of that set. But there is no formula to derive here. The only 'operation' is a mapping: the adapter maps the core's request (say 'get_user(id)') to a specific external call (a SQL query). This mapping is like a function in programming, but you do not need to think about it numerically. To be concrete: imagine the core says 'give me the temperature at this location'. A port says 'a function that takes a location and returns a temperature'. An adapter could be an actual call to a weather API that returns 32.5 degrees; another adapter could read from a sensor file and return 33.1. The core does not know which adapter is plugged in — it just gets a number. There are no formulas to memorize; the only arithmetic is the bookkeeping of which adapter is currently connected, which you can track like a simple table: 'If adapter = API, temperature = 32.5; if adapter = sensor, temperature = 33.1.'

There is no mathematical formula for ports and adapters. The only symbolic notation is a mapping arrow: core_request → adapter_implementation → external_call. The pattern is about software structure, not numeric relationships.


## A worked example

```
Imagine the core needs the current exchange rate from US dollars to Indian rupees. The port defines: a function that returns a number (the rate). Adapter A is an internet API; Adapter B is a local file. The core calls the port with no arguments, expecting a single number.

Step 1: Adapter A (API): response = 83.45   # The core does not see the HTTP request; it only sees the returned number 83.45.
Step 2: Adapter B (file): rate = 83.50   # A different adapter reads a file and returns 83.50. The core cannot tell the difference.
Step 3: Core uses the number: cost_in_INR = amount_in_USD × rate   # The core performs a simple multiplication — arithmetic only — using whatever rate the port gives it.
Step 4: Example: 100 USD × 83.45 = 8345 INR   # That is the full extent of the mathematics. The port-adapter pattern does not require any more.

Result: The core computes the same kind of result regardless of which adapter is plugged in, because the port abstracts away the source. The arithmetic is trivial; the structure is what matters.
```


## From scratch, in Python

```python
# Step 1: Define the port (an interface) using Python's abc module.
# This says: any adapter must provide a method called 'get_rate' that takes no arguments and returns a number.

from abc import ABC, abstractmethod

class ExchangeRatePort(ABC):
    @abstractmethod
    def get_rate(self) -> float:
        pass

# Step 2: Implement two adapters that follow the port.
# Each adapter is a class that inherits from the port and provides the method.

class APIAdapter(ExchangeRatePort):
    def get_rate(self) -> float:
        # In real life this would call an HTTP API.
        return 83.45  # Simulated value

class FileAdapter(ExchangeRatePort):
    def get_rate(self) -> float:
        # In real life this would read a file.
        return 83.50  # Simulated value

# Step 3: The core only knows the port, not the specific adapter.

def convert_currency(amount_usd: float, port: ExchangeRatePort) -> float:
    rate = port.get_rate()
    return amount_usd * rate

# Step 4: Use the core with different adapters.

api = APIAdapter()
file = FileAdapter()

print(convert_currency(100, api))    # 8345.0
print(convert_currency(100, file))   # 8350.0
```


## What the code does

Line by line, starting from the top. First, we import `ABC` and `abstractmethod` from the `abc` module — these are Python's way of saying 'this is a contract that other classes must follow'. The `ExchangeRatePort` class inherits from `ABC`, and inside it we define a method with `@abstractmethod` — that decorator means any class that inherits from this port must define a `get_rate` method, or Python will refuse to create an instance of that class. Then we create two adapter classes: `APIAdapter` and `FileAdapter`, both inheriting from the port. Each one defines its own `get_rate` method — the API adapter returns 83.45, the file adapter returns 83.50. These are hardcoded numbers for demonstration; in a real system they would fetch live data. Next, we define `convert_currency`, which is the core logic. Notice it accepts a parameter named `port` of type `ExchangeRatePort` — it does not know which adapter it will receive. It just calls `port.get_rate()` and multiplies by the amount. That is the whole trick: the core depends only on the interface, not on any specific implementation. Finally, we create an instance of each adapter, pass it to `convert_currency`, and print the results. Changing the adapter is as simple as swapping which object we pass in. If we added a third adapter tomorrow, we would not touch the core at all.


## Common misconceptions

**Ports are the same as APIs** — A port is just an API, and an adapter is a library.

An API is a public interface exposed by a service. A port is defined by the consumer (the core), not the provider. An adapter is a translator that implements that port on top of a provider's API. The same provider can be wrapped in many different adapters, each exposing a different port for different consumers. In fact: A port is a specification the core declares: 'I need this operation.' An adapter is an implementation of that specification for a specific tool. The tool does not know the port exists; the adapter does.

**The adapter should contain business logic** — Since the adapter is the code that talks to the outside, it is a good place to put extra processing like formatting or validation.

Adapters are expected to be dumb translators. They convert data formats and handle I/O, but all business rules — what data is valid, how to combine fields, what decisions to make — belong in the core. Putting logic in an adapter makes it non-reusable and hard to test, and it breaks the dependency rule. In fact: Adapters only translate between the core's language and the external world. Business logic stays in the core, so it can be tested and reused across all adapters.

**A port is just an abstract class** — A port is the abstract class or interface definition in the code.

The interface is the code embodiment of the port, but the port is the conceptual contract — the operations and data types the core requires. Interfaces can be defined in many ways (abstract class, Protocol, duck typing). The port is the meaning; the interface is the syntax. In fact: The port is the abstract contract; the interface is one way to express it in code. The concept is about the dependency direction, not the specific programming construct.

**Adapters are only for external services** — Adapters are only needed for external services like databases, APIs, or file systems. Internal classes do not need ports.

Ports and adapters are useful inside a codebase too. If the core uses a utility class that could be replaced (e.g., a logger, a cache), defining a port for it and an adapter lets you swap implementations or mock them in tests without changing the core. In fact: Ports and adapters can be used at any boundary where the core needs to be independent from the implementation — external or internal.


## In short

A port is an interface the core defines for what it needs from the outside world. An adapter is a concrete implementation of that interface for a particular tool, service, or framework. By depending on ports instead of concrete implementations, the core becomes independent of external technologies, is easy to test, and can swap adapters without modification. This is the practical essence of Dependency Inversion and the foundation of Hexagonal Architecture.


## Where this is used

Where does this matter in building AI systems? In LLM applications. When you build a chatbot, the core is the prompt logic and the conversation flow. It needs to retrieve documents from a vector database, call a language model, and maybe send an email. Each of those is a port. A vector database port might define 'search(query, top_k) -> list of texts'. The adapter for Pinecone implements that by making a network call; the adapter for Chroma implements it by searching locally. The core does not know which one is running — you can develop with a local database and deploy with a managed one, changing only a single line that chooses the adapter. For small models on devices — say a keyword-spotting model on your phone — the port might define 'run_inference(audio_chunk) -> label'. The adapter for a phone CPU uses TensorFlow Lite; the adapter for a server uses PyTorch. The same core logic works in both. In retrieval-augmented generation (RAG), the retrieval is a port. The core asks for 'relevant passages for this query' — the adapter could be a dense-vector search or a keyword search. In agentic systems, tools are ports. An agent defines 'call_tool(name, arguments) -> result'. Each tool — a web search, a calculator, a calendar API — is an adapter. The agent core just loops: it decides which tool to call, calls it through the port, reads the result, and continues. The adapters are what actually talk to the outside world. In everyday applied work, ports and adapters are what make models production-ready. A model needs to load data, train, and serve predictions. The port might be 'load_training_data() -> DataFrame'. Adapters could query SQL, read CSV, or pull from a cloud bucket. A developer using this pattern writes the core once, writes a new adapter for each data source, and never changes the training loop. They reach for a framework like FastAPI for the web layer, SQLAlchemy for the database, and use abstract base classes or Protocol types to define ports. What breaks when done badly? If you skip the port and let the core call the database directly, then changing the database means rewriting parts of the core, which tests every edge case again. It also makes unit testing hard — you cannot mock the database without spinning it up. But with ports, you can write a 'FakeAdapter' that returns canned data, and test the core in milliseconds without any infrastructure.


---

Read this on the web: https://basavapurushottam.com/learn-ai/ports-and-adapters
