I was looking for ways to organize the structure of my Golang projects when I came across two talks by Kat Zień (on GopherCon 2018 and dotGo 2019) where she described this Hexagonal Architecture or Ports & Adapters Pattern. Unfortunately, I did not understand all the concepts and decided to search for more information about them.
I found several articles and blog posts about this pattern, most of them from a Java perspective. Two of those sources helped me better understand this architecture:
Below are the notes that I have taken while reading them.
The Hexagon = The Application
The hexagon is the application core, it contains the data model, business logic (services/components), and the interfaces to allow interaction with the outside.
These interfaces are APIs (Application Programming Interfaces) to allow external entities to communicate with the application and SPIs (Service Provider Interfaces) to allow the application to communicate with external services it might need.
However, this architecture says nothing about the structure inside the hexagon, which may contain:
- layers
- components by features
- DDD
- a single CRUD
Actors
Actors are any real world “thing” that interacts with the application:
- humans
- other apps
- any hardware/software device
They are of two types:
- Primary Actors or Drivers - they trigger the interactions with the application
- Secondary or Driven Actors - they are the targets of interactions triggered by the application
Usually, when drawing a hexagon diagram, primary actors are on the upper left side of the picture and secondary actors on the bottom right.
Examples of primary actors:
- humans
- test cases
- remote applications
- single page applications
- mobile applications
There are two types of econdary actors:
- repository - the application sends and receives data from it
- recipient - the application just sends data to it
Examples of secondary actors:
- Databases (repository)
- SMTP server (recipient)
- Message Queue (recipient)
Ports
Driver Ports are interfaces (API) that the application offers to the outside world to allow the actors interact with the application itself.
From an architecture standpoint, they are at the boundaries of the application.
Driven ports are interfaces (SPI) to functionalities needed for the application to carry out its business logic.
Adapters
An Adapter is a software component that allows actors interact with a specific port in the hexagon. You can have more than one adapter for a single port, each of which allows an actor interact to the application using a specific technology. For example web and CLI. They convert one interface into another.
There are two type of adapters:
- driver adapters - connect an actor to a port
- driven adapters - connect a port to a service provider
Main Component or Composition Root
To glue all the earlier pieces together the main component does the following sequence of actions at the application startup:
- get all configurations and environment variables required for the application
- for each driven port, it instantiate a corresponding adapter
- instantiate the application passing the driven adapters to its constructor
- for each driver port, it instantiate a corresponding adapter passing the application instance to its constructor
- runs the driver adapter instance
The sequence above depicts the use of Configurable Dependency also known as Dependency Injection or Inversion of Control.
The symmetry of the hexagon becomes clear based on the facts that the application core describes the interfaces it needs to communicate with the outside, and the adapters from both sides implement those interfaces.
However, at the startup, an asymmetry occurs when the main component first injects the driven adapters instances into the core and then injects the core instance into the driver adapters.
Although the hexagon picture seems itself very symmetrical, it is important to understand the asymmetry in the way the drivers and driven adapters connect to the core.
Sequence for writing the application
- write functional scenarios to describe the features
- write API interfaces (drivers - left side of the hexagon) - the entry point to the features
- write failing tests to the API
- write SPI interfaces (driven - right side of the hexagon)
- write the business logic to pass those tests (if needed use mocks to emulate some SPI functionalities since the right side is not implemented yet)
- open the left side and add integration tests for the driver adapter
- open the right side by implementing the SPI for the corresponding feature leveraging the previous integration tests
- repeat the sequence for all other features