DEV Community

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

Posted on • Updated on

Understanding Hexagonal Architecture

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. The implementation of a port is called adapter. Adapters are interchangeable ways of interacting with the application using different technologies.

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.

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"?

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. Because it is not the same the application in terms of HA, the application in terms of software perceived by the actors, or the application in terms of a software project.

The application mentioned so far in this post does not refer to a psrt of a code repository or some independent deployable artifact.

The application in terms of HA refers to the part of the code designed and executed to fulfill a request from a driver actor at a given moment. In terms of code it may have different forms while being the same application from the actor standpoint.

In fact, adapters are outside the application in the context of HA while ports are part of it -ports precisely define that boundary-, although all of them are part of the complete application the actors perceive.

Actually, chances are that the whole thing will be together when it comes to files, folders, builds, or deployments.

Furthermore, the application in terms of HA cannot be useful by itself. Which is kinda obvious since adapters are not part of it. For it to be useful it needs a "way to run".

This is what is called a walking skeleton: the code required to, among many other things, run and wire together the right adapters for the ports required to fulfill a request from a driver actor.

HA examples simplified for learning purposes may only need a small script as walking skeleton. In real world systems, this walking skeleton usually manifests as the framework used in the application (application in the context of a software project). Think of .NET, Django, Laravel, Ruby on Rails, etc.

The framework will take care of identifying if the context of an incoming request is an HTTP or command line, or to load and configure queues and database connections.

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 patterns 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:

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.

It is common to find applications claiming to follow Hexagonal Architecture (well, not the applications but we the developers) which use a slightly different approach: 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. Thr adapter will pass the request contract to the application. This approach offers the same benefits as the driver-port-as-interface, delegating the driver adapters' implementation to the built-in tools of the framework:

# 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 (0)