Hexagonal Architecture in Go

Matías Varela
8 min readMar 14, 2020

--

Before writing the first line of code, one may ask: How should I organize the project? or, Which component should I write first?. If these questions are difficult to answer is because you are not following a software architecture pattern.

In this article, I want to share my knowledge and my point of view of an hexagonal architecture (or Ports & Adapter pattern) proposed by Alistair Cockburn in 2005.

Hexagonal Architecture

Core

In this architecture, everything is surrounding the core of the application. It is a technology agnostic component that contains all the business logic. The existence of other things in the outside should be completely ignored. In other words, the core shouldn’t be aware of how the application is served or where the data is actually hold.

The core could be viewed as a “box” (represented as a hexagon) capable of resolve all the business logic independently of the infrastructure in which the application is mounted. This approach allow us to test the core in isolation and give us the ability to easily change infrastructure components.

Having a clear definition of the main component let us talk about the interactions with “things” that exists in the outside of the core. Those are what we call Actors.

Actors

Actors are real world things that want to interact with the core. These things could be humans, databases or even other applications. Actors can be categorized into two groups, depending on who triggers the interaction:

  • Drivers (or primary) actors, are those who trigger the communication with the core. They do so to invoke a specific service on the core. A human or a CLI (command line interface) are perfect examples of drivers actors.
  • Driven (or secondary) actors, are those who are expecting the core to be the one who trigger the communication. In this case, is the core who needs something that the actor provides, so it sends a request to the actor and invoke a specific action on it. For example, if the core needs to save data into a MySQL database, then the core trigger the communication to execute an INSERT query on the MySQL client.

Notice that the actors and the core “speak” different languages. An external application sends a request over http to perform a core service call (which does not understand what http means). Another example is when the core (which is technology agnostic) wants to save data into a mysql database (which speaks SQL).

Then, there must be “something” that can help us to make such translations. Here is where the Ports & Adapters come to play.

Ports

In one hand, we have the ports which are interfaces that define how the communication between an actor and the core has to be done. Depending on the actor, the ports has different nature:

  • Ports for driver actors, define the set of actions that the core provides and expose to the outside. Each action generally correspond with a specific case of use.
  • Ports for driven actors, define the set of actions that the actor has to implement.

Notice that the ports belongs to the core. It is important, due to the core is the one who define which interactions are needed to achieve the business logic goals.

In black, the ports for driver actors. In gray, the ports for driven actors.

Adapters

In the other hand, we have the adapters that are responsible of the transformation between a request from the actor to the core, and vice versa. This is necessary, because as we said earlier the actors and the core “speaks” different languages.

An adapter for a driver port, transforms a specific technology request into a call on a core service.

An adapter for a driven port, transforms a technology agnostic request from the core into an a specific technology request on the actor.

Actors connected to the core ports through adapters

Dependency Injection

After the implementation is done, then it is necessary to connect, somehow, the adapters to the corresponding ports. This could be done when the application starts and it allow us to decide which adapter has to be connected in each port, this is what we call “Dependency injection”. For example, if we want to save data into a mysql database, then we just have to plug an adapter for a mysql database into the corresponding port or if we want to save data in memory (for testing) we need to plug an “in memory database” adapter into that port.

This image illustrate how easy is switching between infrastructure. Just change the dependency.

Case of study: MinesWeeper API

Now that we have seen the main components of an hexagonal architecture, let us put all together in a sample application. We are going to build an API for the popular game called Minesweeper. To make it simpler, we are going to implement just a few features, however, it is enough to use it as an example.

Functional requirements

  • Get a game: given an id, it should return the game associated.
  • Create a new game: given a name, a size (N) and the number of bombs (B), it should create and return a new game where its board has NxN cells and B bombs in random positions.
  • Reveal a cell: given a game id and cell position, it should reveal the cell and return the resulted game. If a bomb is found or the last empty cell is revealed then the game is over.

As we mention earlier, in this architecture everything is surrounding the core of the application, therefore, it is important to start by building the business logic. At this point, just forget for a moment where the data actually will be hold or how the application will be served. Just put all your energy implementing and testing the core.

Let us begin, step by step.

Note: the examples that are about to be shown are simplified. Checkout the github.com repository to see the complete project. Link

Application structure

The following, is the directory structure.

├── cmd
├── pkg
└── internal
├── core
│ ├── domain
│ │ ├── game.go
│ │ └── board.go
│ ├── ports
│ │ ├── repositories.go
│ │ └── services.go
│ └── services
│ └── gamesrv
│ └── service.go
├── handlers
└── repositories

Core

All the core components (services, domain and ports) will be placed in the directory ./internal/core.

Domain

All the domain models will be placed in the directory ./internal/core/domain. It contains the go struct definition of each entity that is part of the domain problem and can be used across the application.

Note: not every go struct is a domain model. Just the structs that are involved in the business logic.

Ports

The ports will be placed in the directory ./internal/core/ports. It contains the interfaces definition used to communicate with actors.

Services

The services are our entry points to the core and each one of them implements the corresponding port . They will be placed in packages inside the directory ./internal/core/services.

Let us implement our first case of use: “Get a game”

We know that somehow the game is saved in a storage. Although we are not able to determine, at this moment, which specific actor is responsible for such task (mysql, aws dynamodb or a simple file) we know that the interaction will be done through a port, so let us delay that decision and just use the port instead. We name that port GamesRepository.

Notice that the dependency is injected at the moment the service is created

Let us do something more interesting, the implementation of the second case of use: “Create a new game”.

Notice that the service includes another dependency: “uidgen.UIDGen”. In this case, it is not a port because it only delegate the id generation to another package but it is not used to interact with actors.

Checkout the implementation of the last case of use: “Reveal a cell” in the github repository. Also checkout how the core is being tested with the help of GoMock, which I highly recommend.

Adapters

At this point, we have all the business logic implemented and tested, therefore, we know that our application fulfill the functional requirements.

Now, it’s time to implement the adapters so the application can interact with the actors. Having all the components decoupled from each other give us the advantage to implement and test them in isolation or we can easily parallelize the work with the help of other members of the team.

Driver adapter

All the driver adapters will be placed in packages inside the directory ./internal/handlers.

The game is going to be served via http. Therefore, this driver adapter must be capable of transform an http request into a service call.

Driven adapter

All the driven adapters will be placed in packages inside the directory ./internal/repositories.

The game models are going to be saved in an “in memory” key value store. This driven adapter must satisfy the ports.GamesRepository interface.

Serve the application

For the final step, we need to serve the application. We can provide one or several ways for serving this app. Each one of them, must be implemented in its own package inside the directory ./internal/cmd

Advantages

  • Separation of concerns: each component (core, adapters, ports, etc) has a well-defined purpose and there is no doubt of their responsibilities.
  • Focus on the business logic: delaying the technical details allows you to focus on what matters at the end, the business logic.
  • Parallelization of work: once the ports are defined, it is easy to parallelize the work across mates. Having several members of the team working in different well-defined and decouple components can reduce the development time considerably.
  • Tests in isolation: each component can be tested in isolation, and more important is that the core is self-tested.
  • Easily change infrastructure: it is really easy to change the infrastructure. You can move from a mysql to an elastic-search database without an impact on the business logic.
  • Self-guided process: the architecture itself guides you on how the development process steps should be taken. Starts from the core, continue with the ports and adapters and finally serve the application.

Disadvantages

  • Too complex for small or short-term projects: it is important to analyze if this architecture is appropriate for the desired project. For example, if the micro-service has only one specific task it could be overkill. Or if it is short-term project sometimes is better to keep it simple.This architecture is recommended for applications with real business domain problems.
  • Performance overhead: adding extra components trigger extra calls to functions, therefore, in each of them we will be adding a very small overhead. This could be a disadvantage if our service has to be extremely performant.

--

--