In Short…
The Ports And Adapters architecture, also known as “Hexagonal Architecture”, is a software architecture that aims to separate the business logic of an application from external entities and infrastructure that interact with it. The Ports and Adapters architecture does this by isolating the business logic and domain model of an application from it’s runtime dependencies using a layer of interfaces. This article explains the Ports and Adapters architecture, demonstrates how this architecture is implemented in code, and explores how the Ports and Adapters architecture makes software easy to evolve over time.
In More Detail…
Ports and Adapters in the Physical World: USB Devices
To better understand the concepts behind the hexagonal architecture, it’s useful to examine an analogy to the physical world with laptop power cords.
A laptop power cord has three components
The electrical “forks” that plug in to an outlet
The power brick, which converts incoming voltage to an appropriate level for the laptop
the power cord that plugs in to the laptop itself on the laptop’s power port
The electrical forks expose an interface for which external things like a power outlet in a house can interact with the power cord. The power brick receives this input and transforms the power to an appropriate type and voltage for the device on the other end. The power cord takes the resulting output from the power brick and feeds it in to the laptop connected on the other end.
The power of this setup is that the parts providing input to the power brick or receiving the output of the power brick are interchangeable. The same laptop power cord can charge any device that implements the interface allowing the device to be connected to the receiving end of the cord. Likewise, the power brick can receive power from anything that is capable of interacting with the electrical forks, such as a wall socket of a house, an outlet in a car, or a portable power generator.
The concept of interchangeable parts is the same philosophy behind the Ports and Adapters architecture for decoupling runtime dependencies from an application’s logic.
Ports and Adapters Architecture
The ports and adapters architecture is, unsurprisingly, defined in terms of ports and adapters. Ports define interfaces between the application and it’s dependencies. Adapters are runtime dependencies that connect to the application by communicating through ports.
The application’s logic is completely agnostic to the details of what is connected to the ports, other than knowing about the port’s definition. An application can have many adapters connected to many ports, with no limitations on the number of ports exposed or adapters connected to the ports.
Ports and adapters are classified as either “primary” or “secondary”. It’s helpful to think of “primary” as “driving” and “secondary” as “driven”. Primary ports define the interface for how external things can “drive” the application. Secondary ports define the interface for how external things will be “driven” by the application. Primary adapters connect external things to the application and secondary adapters connect the application to external things.
In the power analogy, the wall socket is the primary adapter that interacts with the power brick through the electrical forks. The electrical forks are the primary port interface to the power brick. The power brick’s circuitry is the application logic, which sends output through the secondary port. The laptop is the secondary adapter that exposes an interface for the power brick to “drive” by sending the laptop power.
Ports and Adapters Architecture Example: To-Do List
Imagine you’re tasked with creating a To-Do List application. The application has some functional requirements, including creating to-do lists, tasks, and updating the status of tasks as they’re completed. We’ll start off simple by implementing the logic and running it through some integration tests and a command-line user interface. We anticipate that our simple To-Do List will grow far beyond the original proof of concept, so we want to implement our application in a way that lets us build the logic of our To-Do List application in a way where we can easily swap out runtime dependencies and infrastructure.
Let’s take our To-Do List application and see how it can be implemented using the Ports and Adapters architecture to evolve over time. This can be done in any language, but we’ll be using Typescript for this example. All the sample code can be accessed here.

We’ll start by defining the domain model and interfaces for primary and secondary ports. Then, we’ll implement the application logic by implementing primary port interfaces. Next, we’ll create the initial secondary adapters required by the application logic by implementing secondary interfaces. Finally, we’ll create primary adapters that drive the application logic through the primary port interfaces.
Lets define a simple domain model for a To-Do List
Moving on to the ports, we need to define primary ports and secondary ports as interfaces. For our application logic, we’ll create an interface TodoService
which contains the basic CRUD operations for our To-Do List application. This interface is a primary port.
In order for our application to be useful, it will need to be able to store and retrieve information about existing To-Do lists. We'll create a secondary port for a TodoRepository
that will let us plug in adapters that know how to persist our application data.
Our ports use the domain model as a communication layer between the adapters and the domain logic. This is fine, since our domain model is still entirely agnostic to any runtime dependencies.
Moving on, time to implement our application logic. We’ll keep things simple for demo purposes, creating a TodoFacade
that implements our TodoService
. A facade is simply an object that hides complex behavior behind a simpler interface. This facade is the boundary of the application logic, exposing ports with which the primary adapters can interact.
With our application logic implemented, we can now see how the ports and adapters interact with the application logic.
the application logic knows about the
TodoRepository
secondary port, and uses it for the business logic implementation. We can plug in any adapter that implements the secondary interface, so we can have different implementations depending on the context. Our logic is the same regardless of whether theTodoRepository
we’re given at runtime persists data in memory, writes to a document database, publishes messages to a message queue, or just keeps the data in memory.the application logic implements the primary port interface. Similarly to how we can “plug in” secondary adapters to our application logic, our application logic can be “plugged in” to primary adapters that know how to use the primary port interface.
Our application logic is fully implemented without any adapters. We only need the primary port interface to make sure our application logic can process interactions from primary adapters. We only need knowledge of the secondary port interface to know how our application can drive whatever adapter eventually gets “plugged in” to the secondary port.
Time to implement a secondary adapter for our TodoRepository
. For now, we’ll implement a TodoRepository
that just keeps track of data in memory.
Next, we will implement our primary adapter that we’ll use to drive the application. This could be anything from a HTTP server implementing a REST API, a test suite that validates behavior of the application, a subscriber that updates the application receives a notification, or something else.
For now, we’ll keep it simple and implement a primary adapter that acts as a unit test, just creating, updating, and fetching a hardcoded To-Do list.
We are less strict about “layers” compared to an “N-layer” architecture. The primary adapter is the context in which the application is being used, so it’s natural that we would also connect the secondary adapter here. It’s common, but not required, to have a framework like Spring or Django handle creating and wiring up secondary adapters and our application logic.
Observations and Analysis
Evolving the application
With the Ports and Adapters architecture, we can simply swap out adapters without needing to change the application logic at all. To scale this from a single command-line interface to a highly scalable web and mobile application, we may need to do something like
implement a secondary adapter
SqsTodoRepository
that implements theTodoRepository
interface by publishing a message to a message queue instead of keeping things in memory.implement a secondary adapter
DynamoDbTodoRepository
that implements theTodoRepository
by persisting data to a DynamoDB tableimplement a primary adapter that listens to a message queue for incoming requests, and invokes the application logic with the
DynamoDbTodoRepository
implementation plugged in to the secondary port to persist the application data.implement a primary adapter that exposes a REST API to web and mobile user interfaces, plugging in the
SqsTodoRepository
to the application logic’s secondary port. This primary adapter would listen for incoming requests, and invoke the corresponding application logic.
None of these things involve changing anything in the application logic.
Primary vs Secondary Ports and Adapters
What if our “secondary adapter” in one application is a primary adapter to another application? A primary adapter just knows how to interact with an application through it’s interface (the primary ports). So by that definition, one system’s “application logic” could be another system’s primary adapter. From this perspective, we can simply do away with the notion of primary and secondary ports and adapters and just think of things in terms of “ports” and “adapters”. A port defines the interface with which adapters can interact, and adapters communicate with applications over these interfaces.
Compared to N-layer architecture
Personally, I’ve encountered and used the traditional N-layered architecture much more often in my career than the Ports and Adapters architecture. On paper, these are two different architectures. In practice, the lines get blurred quickly when implementing an N-layer architecture.
The N-layer architecture I see in practice commonly follow this setup
A controller layer, exposing some API over a network connection
A logic layer, implementing the service’s logic
A domain layer, implementing the service’s data model
A persistence layer, which reads/writes data to external sources
Early on in the development of these types of systems, engineers rightfully decide to add a layer of abstraction between the layers by sticking interfaces between each layer and using dependency injection. This is a smart approach, making things easy to mock for unit tests and swapping out implementations as needed. When using interfaces and dependency injection in this manner, an N-layer architecture becomes very similar to a Ports and Adapters architecture. Both approaches achieve the important goal of interchangeable runtime dependencies.
Closing Thoughts
Regardless of the architecture style, the biggest value comes from interchangeable run time dependencies. Personally, I prefer the more flexible mental model of Ports and Adapters for thinking about the architecture of a system decoupled from runtime dependencies. If the mental model of N-layer architecture with interfaces and dependency injection is more familiar, that’s a perfectly acceptable way to architect your application.