DEV Community

Cover image for Understanding Hexagonal Architecture
Carlos Gándara
Carlos Gándara

Posted on • Edited on

Understanding Hexagonal Architecture

Alistair Cockburn was kind enough to give his opinion on this post. Based on this, I have adjusted it a bit, highlighting the parts that changed.

The goal of the Hexagonal Architecture (aka ports and adapters, or HA in this post) pattern is, as originally formulated by its author Mr. Alistair Cockburn:

Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.

In a previous post we summarized the way it works as:

Hexagonal Architecture promotes the separation of an application internals from its interactions with the external world.

We have our application, represented as a hexagon. Outside the hexagon there are the external actors: users interacting with a graphical interface, messaging systems, databases, vendor APIs, etc.

The application defines the ways for it to interact with external actors. Each of these contracts is a port. Ports are expressed in application language, like parts of a use case. Adapters are the connections of the external actors with the application using a port. They are interchangeable ways of interacting with the application using different technologies.

Edited on 2024-08-02: The above paragraph was incorrectly stating that adapters are implementations of the ports, which is only half true. It's fixed now.

In this post we will dig deeper in each of the parts of the pattern and link them to code examples.

Driving and being driven

Before jumping into more definitions, let's first consider how our applications work from the perspective of the interactions with the external actors.

Some of these actors use our application (e.g. a call to our REST API). Other actors are used by our application instead (e.g. a database).

We call the former driver actors and the latter driven actors. How they interact with our application happens will determine the way we define the contracts the application sets for them.

Driver and driven actors in Hexagonal Architecture

Analogously, this distinction applies to ports, adapters, and sides of the application.

The ports

Ports are the contracts the application settles for the interaction with the external actors. Depending on the type of actor we are dealing with, driver of driven, this contract will look different.

Driver ports define how driver actors interact with the application. Therefore, they specify the input that must be provided in order to ask the application to perform the action the actor is interested in.

Driven ports define how the application will interact with the driven actor. Therefore, they specify what they want from the actor.

Ports in Hexagonal Architecture

Since ports are defined by the application they are written in pure "application language", removing coupling to any detail of concrete technologies. Our application will not care about any technologies, because ports are isolating it from them.

Examples!

  • To create a user we need their email and password. The driver port for this use case defines this data as the contract the actor must adhere to in order to use the application. Whoever you are, if you want me to create a user, you must give me this.
  • Users are modeled in our application with the User entity. The driven port for storing users defines that there must be a way to store User entities.

The adapters

Actors interact with the application. Ports define how these interactions happen. Adapters fill the space between the actors and the application, enabling the conversation between them.

Adapters in Hexagonal Architecture

Adapters adapt (didn't see that coming) the details of different technologies into what the application has defined beforehand through ports. In the process, those details do not permeate inside our application, keeping it clean. If any response is expected by the actor, it's the adapter responsibility to produce it.

Following our previous examples, our port to create users specifies the application requires an email and a password. We may have:

  • For the "user interacting with a graphical interface" actor, an adapter for an HTTP REST API that takes the values from the HTTP request. It also returns the 202 response to the client.

  • For the "company CRM system" actor, an adapter for a message broker that gets the values from the message payload. The adapter takes care of sending the ACK of the message as well.

  • For some weird integration with an "old system" actor, an adapter reading the values from a CSV file this system is putting in our FTP server each night.

Wiring everything together: the Configurator

Edited on 2024-08-02: The first version of the post was not mentioning the Configurator.

On top of our application, the ports it defines, and the different adapters that connect with the ports, we need something that takes care of using the right elements in the right moment. This is what the pattern refers to as the Configurator.

Most of the times we use a framework when building software applications: Ruby on Rails, Spring, .NET, Django, etc. The framework usually takes care of the wiring of the different moving pieces, for instance by providing a service container. In an application built applying ports and adapters, that would be the Configurator.

It is something we may take for granted, but we must be aware that when not using a framework that provides this type of functionality, we must implement ourselves a mechanism to provide the hexagon with the right adapters for each request.

The benefits

With what have covered so far we have achieved significant benefits.

First, our application may be technology-agnostic. Its communication with the outside is defined via ports, with the adapters taking care of converting each technology quirks to what the port defines, which is what the application understands.

In practice, the application does not know about the database structure, that's for the database adapter to handle. Or if we get input data from an HTTP request or a CLI command, in JSON or protobuff, from Rabbit or from Kafka.

Second, with each port acting as entry point (or exit, depending on the port being driver or driven) we can use any number of different adapters. Therefore, our application can be extended just plugging in a new adapter. Pretty much as the strategy pattern.

And there is a third massive benefit: testing.

To test our application behavior without the need to have some real technology in place, we can -and certainly must- implement a test adapter that acts as a replacement.

For the driver ports, we don't need an actual message broker, or to mimic an HTTP request. A test adapter will just call the application sticking to the port contract.

For the driven ports, we don't need an actual database up and running. We can use faster and simpler in-memory implementations.

Test adapters in Hexagonal Architecture

In fact, the port + test adapter combo allow to move on with the application implementation without worrying about with which technology the integrations will actually happen.

These decisions can be deferred until the last reasonable moment avoiding attaching ourselves to a certain technology too early, something that could influence further design for the wrong reason

For instance, we can use in memory persistence adapters without the need to pick a concrete database early on. We design our application without being influenced by the constraints the chosen database has, and once we have further developed our system we can pick the best database for it.

Naming ports and adapters

There is no recipe for naming in HA. Alistair Cockburn is a firm proponent of use cases, and his explanations of the pattern point to name ports and adapters in a use case oriented way. However, doing so or not does not change what the pattern solves. And patterns are about solving recurrent problems, not about doing it with a certain wording.

Alistair would propose to call ports following the naming pattern For[Doing][Something]. Examples for driver ports: ForPlacingOrders, ForConfiguringSettings, etc. For the driven side: ForStoringUsers, ForNotifyingAlerts, etc. Which is pretty cool, but sadly not something I've seen in the wild.

It's more common to find driver ports as PlaceOrder or ConfigureSettings and driven as UserRepository and AlertNotifier.

In the adapter side the trick is to reference the technology we are adapting. We can have CliCommandForPlacingOrders or MysqlDatabaseForStoringUsers. If not following Cockburn's naming pattern we could go with PlaceOrderApiController or RabbitMqAlertNotifier. There is also this pattern of adding the Using[Technology] suffix (credits to Frank de Jonge for this one), resulting in PlaceOrderUsingRestApi or UserRepositoryUsingPostgreSql.

Find the convention you are comfortable with and be consistent with it. Just mind that the most the naming express the architecture, the better.

What is "the application"?

Edited on 2024-08-02: edited to make it more concise.

One aspect not usually covered in the literature about HA is what the application or the hexagon actually is, which can be frustrating for newcomers.

The HA application is one element of the pattern. Ports are at its boundary. Therefore, adapters are outside of the application. HA is about isolating the application with the boundaries.

The application perceived by the external world is the whole system. Including the HA application, the adapters, the configurator, and any other code or files needed for it to run.

For a system to run we need some tooling that is outside the HA pattern. This is what is called a walking skeleton: the minimum code in our system that allows to run it. When using a framework, we can consider it our walking skeleton even when it comes with way more built-in features than the minimum needed to run. HA examples simplified for learning purposes may only need a small script as walking skeleton.

For what is worth, the concept of walking skeleton was coined by... Alistair Cockburn. The software industry indeed owns a lot to him and his work. For deeper reading on the concept I cannot less than recommend the book Growing Object-Oriented Software, Guided by Tests, which has a chapter dedicated to it.

Request lifecycle in Hexagonal Architecture

Now that we know all the moving parts we can foresee how systems built applying HA work.

Scenario: the company CRM has created a new client. The business policy when this happens is that we create a user for the client, send an email to notify her, and propagate the user was created so other systems can react to this action. Our application takes care of user creation and exposes a REST API, which is called by the CRM.

A request lifecycle in Hexagonal Architecture

Pretty much a standard request for any system, no special complexity here.

The CRM is the driver external actor. Our application defines the CreateUser driver port, which has an adapter for the REST API: CreateUserUsingRestAPI. There is also a driven port UserRepository for user storage and another one UserCreatedNotifier. The adapters for them are UserRepositoryUsingPostgres, UserCreatedNotifierUsingEmail and UserCreatedNotifierUsingRabbitMQ.

Our application, which is built using our framework of choice, receives the request and detects it's arriving via HTTP. It wires together the adapters inside whatever service we have defined for handling this request and executes it.

When doing so, the application is not polluted with HTTP request payloads, content types, or headers. The adapter transforms all of that into what the port defines.

In the driver side, the application knows nothing about the database tables involved in persisting an user, or how the payload of the RabbitMQ messages should be. The ports define they will persist a user and will notify about it being created, and the adapters take care of transforming that application-defined instructions into what each technology requires.

Code examples

Before jumping into code, an aspect that cannot be emphasized enough: HA is just about ports and adapters.

It dictates nothing about how the application should be organized internally. There is no recipe for the number of layers our application should have, not even if it should have layers at all.

Command buses and handlers, DDD patterns, onions and upper case Clean Architectures, big balls of mud, etc. are all valid ways of implementing the internals of an application which applies Hexagonal Architecture. But there is no requirement or guideline dictated by the pattern on how it should be done.

Therefore, the following examples should be taken as ways of illustrating how ports and adapters work together. Not as the right way™️ to be hexagonal.

Now, the ports for our "create user" use case could look like:

Since the writing of the original post I've come to realize that this first code example is not accurate. Driver adapters use the application and this example proposes the application to use them. Although we would get the same benefits, the second code example is the one actually applying Hexagonal Architecture.

interface CreateUser {
    buildRequest(): CreateUser;
}

interface UserRepository {
    add(User user): void;
}

interface UserCreatedNotifier {
    notify(UserCreated event): void;
}
Enter fullscreen mode Exit fullscreen mode

The adapters would be implementations for those interfaces using the different technologies suggested by their name.

Our application (in terms of HA) has a service to create the users, which gets as dependencies the ports involved in the use case:

class CreateUserHandler {
    private CreateUser createUser;
    private UserRepository userRepository; 
    private array<UserCreatedNotifier> notifiers;

    fn handle(): void
    {
        var request createUser.buildRequest();
        var user = new User(request.email(), request.password());
        userRepository.add(user);
        notifiers.each(x => x.notify(new UserCreated(user.email())));
    }
}
Enter fullscreen mode Exit fullscreen mode

Our framework is configured to inject the right implementations -the adapters- for the current request and to call the handle method.

We can notice how using test adapters we could test the business logic of our handler without dependencies to actual infrastructure services: a TestCreateUser could provide baked test values, a InMemoryUserRepository would not need any underlying database system running, etc.

A different approach is when driver ports are not defined as interfaces, but as data structures the adapters must provide to the application.

Frameworks are good identifying the technologies used by driver actors, and we delegate to them invoking the right adapter. The adapter will pass the request contract to the application. This approach offers the same benefits as the driver-port-as-interface, delegating to the built-in tools of the framework the driver adapters' implementation:

# replaces the interface
class CreateUser {
    public readonly string email;
    public readonly string password;
}

# the adapter is a framework controller
class CreateUserController {

    private CreateUserHandler handler;

    fn createUser(HttpRequest request) {
        handler.handle(
            new CreateUser(
                request.payload.get('email'),
                request.payload.get('password')
            )
        );
    }
}

# the application no longer expects the port as dependency, but as a parameter
class CreateUserHandler {
    private UserRepository userRepository; 
    private array<UserCreatedNotifier> notifiers;

    fn handle(CreateUser request): void
    {
        var user = new User(request.email(), request.password());
        userRepository.add(user);
        notifiers.each(x => x.notify(new UserCreated(user.email())));
    }
}
Enter fullscreen mode Exit fullscreen mode

When and when not

Patterns define well proved ways of solving common problems. The problem HA tackles is the difficulty to maintain, test, and evolve applications when they are too coupled to concrete technologies. But "too coupled" is a very contextual and subjective measure. What is valid for one team or project might not be for others.

Furthermore, context changes over time. What is a reasonable decision today may not be so tomorrow.

My personal take is that HA is actually so simple and the overload it implies so small, that it is the default approach I would use for any greenfield project. It just feels natural to abstract away the technical details using ports. Remember: HA is just ports and adapters, it does not necessarily imply DDD, buses, presenters, etc.

However, there are contextual factors that might justify not going with this pattern. The effort to move existing applications to HA may not be worth if the application is not expected to be extended and already has good test coverage. In a more social aspect of it, for teams of hardcore-framework developers (which is a legit standpoint) the switch to an architecture that pushes the framework away from the application core could cause a lot of friction, which might not compensate the expected benefits.

As always, no magic recipes. The advice would be to try it, explore it enough to understand what it gives and takes, and choose it when it fits.

Concluding

Hopefully this post has been useful to get to know the Hexagonal Architecture pattern. Maybe there is a degree of frustration because how high level it actually is. We devs (humans!) tend to look for guided solutions to apply without much thinking, and HA is quite lax in those terms. It's more a set of guidelines to avoid creepy scenarios than any guided way of building applications.

For less abstract guidelines there are other patterns that will put us more "on rails". HA just don't mind if you use these other patters or not. It only cares of protecting the boundaries of your application.

In following posts we will cover possible ways of designing the internals of our application while sticking to HA. With a bit of luck they won't take as long as this one.

Credits

For the diagrams I've used Excalidraw, borrowing libraries from Youri Tjang, Drwn.io, David Luzar and Ana Clara Cavalcante

The cover picture is from Penny Richards, distributed under the Creative Commons license. It can be found here.

Top comments (6)

Collapse
 
pierodibello profile image
Pietro Di Bello

Thanks @xoubaman for this article. It’s rare to find good articles like yours that explain in a clear and simple way what Hexagonal Architecture looks like, as this architectural style can indeed be many different things to different people. :D

I found a small misinterpretation of what a “walking skeleton” really is.
To me, a walking skeleton (much like the tracer bullet in “The Pragmatic Programmer” book) is a minimal implementation of the application that includes just enough code to run and wire together the right adapters for the ports required to fulfill a request from a driver actor.
WDYT?

Collapse
 
xoubaman profile image
Carlos Gándara

Looks to me that the definition you comment is pretty aligned with the post:

This is what is called a walking skeleton: the minimum code in our system that allows to run it.

A nuance to it is that usually we build systems that at high level are known problems: enterprise web applications, HTTP APIs, etc. Since the basic structure of these systems is the same, I consider it's ok to stretch the term "a bit" :D and call walking skeleton a fullstack web framework, even knowing it is far from being minimal.

I would not go with something actually minimal for a Saas web system, because it's a well known type of system we know up front will benefit from using a framework.

Actually, it's not only the framework but also the configuration needed and the first ports and adapters so we can verify, as you say, the basics are correctly wired.

For small systems as the ones normally used to illustrate the pattern, the walking skeleton would be really minimal.

I hope this clarifies my point of view, which indeed is not that detailed in the post.

Collapse
 
pierodibello profile image
Pietro Di Bello

Thank you for the clarification! I now have a better understanding of what you were referring to when you were mentioning the walking skeleton 👍

Thread Thread
 
xoubaman profile image
Carlos Gándara

Thank you for your comment. I'm just a guy on the internet with an opinion, it's always cool to hear others and get something out of it

Collapse
 
pierodibello profile image
Pietro Di Bello • Edited

@xoubaman I have another question for you: in past experiences, I also happened to work on applications using Hexagonal Architecture where the adapter code (the code written to implement the driver adapters and the driven adapters) was way larger than the application code (the code for what is “inside the hexagon”).
The answer I give to myself is that often adapter code is quite verbose/complex because it has to deal with frameworks, infrastructure details, error handling, etc.
Does this match also your experience? In your past experiences, what was the ratio between adapter code and "inside-the-hexagon" code?

Collapse
 
xoubaman profile image
Carlos Gándara

I have never measure this to be honest. Although it makes sense. As you say, by moving technical details into adapters the domain code is only about domain logic, with no interference.

This is good IMO. It is the code that we want to be easy to change.