When you hear stories about the most gigantic projects having a microservice architecture, you are tempted to introduce dozens of tiny applications that would work for you, like house elves, invisible and undemanding. However, system architectures lie on a spectrum.
What we imagine is the extreme end of that spectrum: tiny applications exchanging many messages. At the other end of the spectrum you imagine a giant monolith that stands alone to do too many things. In reality, there are many service-oriented architectures lying somewhere between those two extremes.
In a nutshell, a microservice architecture means that each application, or microservice’s code and resources are its very own and will not be shared with any other app. When two applications need to communicate, they use an application programming interface (API) — a controlled set of rules that both programs can handle. Developers can make many changes to each application as long as it plays well with the API.
This idea comes in many flavors, with different shares of the monolith architecture. In this post, we are going to discuss one of such variations of microservice architecture, known as Hexagonal Architecture.
The first key concept of this architecture is to keep all the business models and logic in a single place, and the second concept — each hexagon should be independent.
What is Hexagonal Architecture?
Invented by Alistair Cockburn in 2005, Hexagonal Architecture, or to call it properly, Ports and Adapters, is driven by the idea that the application is central to your system. All inputs and outputs reach or leave the core of the application through a port that isolates the application from external technologies, tools and delivery mechanics.
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. Alistair Cockburn
Hexagonal Architecture draws a thick line between the software’s inside and outside parts, decoupling the business logic from the persistence and the service layer. The inside part makes up the use cases and the domain model it’s built upon. The outside part includes UI, database, etc. The connection between them is realized via ports and their implementation counterparts are called adapters. In this way, Hexagonal Architecture ensures encapsulation of logic in different layers, which ensures higher testability and control over the code.
Each side of the hexagon represents an input — port that uses an adapter for the specific type. Hence, ports and adapters form two major components of Hexagon Architecture:
A port is a gateway, provided by the core logic. It allows the entry or exiting of data to and from the application. The simplest implementation of a Port is an API layer. Ports exist in 2 types: inbound and outbound.
An inbound port is the only part of the core exposed to the world that defines how the Core Business Logic can be used.
An outbound port is an interface the core needs to communicate with the outside world
An adapter transforms one interface into another, creating a bridge between the application and the service that it needs. In hexagonal architecture all communication between the primary (which use system to achieve a particular goal) and secondary actors (which system uses to achieve primary actor’s goals) and application ports is done with the help of adapters. Therefore, adapters can also be of two types:
The primary or Driving Adapters represents the UI. It is a piece of code between the user and the core logic. They are called driving adapters because they drive the application, and start actions in the core application. Examples of a primary adapters are API controllers, Web controllers or views.
The secondary or Driven Adapters represent the connection to back-end databases, external libraries, mail API’s, etc. It is an implementation of the secondary port, which is an interface. These adapters react to actions initiated by the primary adapters.
The third component of the architecture is the domain model, a conceptual model that represents meaningful concepts to the domain that need to be modelled in software. The concepts include the data involved in the business and rules the business uses in relation to that data.
Benefits of Hexagonal Architecture
- High Maintainability, since changes in one area of an application doesn’t affect others.
- Ports and Adapters are replaceable with different implementations that conform to the same interface.
- The application is agnostic to the outside world, so it can be driven by any number of different controls.
- The application is independent from external services, so you can develop the inner core before building external services, such as databases.
- Easier to test in isolation, since the code is decoupled from the implementation details of the outside world.
Our implementation of Hexagonal Architecture
One of Sciforce’s projects required to separate often changing external elements from internal ones that might lessen the impact of change and simplify the testing process.
The standard architectural template is based on the Spring set of frameworks:
The Core contains objects that represent the business logic of the service. Typically for Hexagonal Architecture, the core knows nothing about the outside world, including the network and the file system. All communication with the outside world is handled by Inbound and Outbound gateway layers.
In our application, a Port is a Groovy interface used to access either a Core or an Outbound object and an Adapter is an implementation of a Port interface that understands how to transform to and from external representations into the Core’s internal data model.
You can swap out an Adapter in gateway layer: for example, you can enable accepting messages from RabbitMQ instead of HTTP clients with no impact on the Core.We can also substitute Service Stubs for outbound gateways increasing the speed and reliability of integration tests.
Example: Simple application
In the example, we show a scheme of a simple application that uses the Spring Framework to accept a REST request with some text, converts the text to lowercase and saves it to MongoDB.
At the first step, when the client sends an HTTP request, the RestController is responsible for handling it. In terms of Hexagonal Architecture, this controller serves as an Adapter that communicates the request from the HTTP protocol to the internal domain model (a simple string in this case). It is also responsible for calling into the Core via the Port and sending the results back over the client.
The ConversionService is the object that the inbound Adapter (RestController) invokes via the ConversionPort interface. The service itself doesn’t access anything outside of the process: all it needs to do its job is to access the in-memory objects. After performing all the necessary processing (converting the text to lowercase), it delegates the task of storing the results to the outbound PersistencePort.
The MongoDBGateway is the implementation of the PersistencePort. It knows how to adapt the Core’s internal model, which is plain text, into a form that MongoDB can handle. Though the example is basic, in more sophisticated systems, it might implement exception, logging and retry logic in the code.
This example shows only 3 objects but in an actual application, you would have multiple objects in play. For example, a single REST controller would respond to different URLs by calling different services which then call different outbound gateways.
To sum things up, the main idea of Hexagonal Architecture is decoupling the application logic from the inputs and outputs. It helps to free your most important code of unnecessary technical details and to achieve flexibility, testability and other important advantages that make your working process more efficient.
However, like other architectures, hexagonal architecture has its limitations and downsides. For instance, it will effectively duplicate the number of classes on your boundary.
When it is the best choice? As it facilitates the detachment of your external dependencies, it will help you with the classes that you anticipate will be swapped out in production in future and the classes that you intend to fake in tests.