TECH CULTURE
14 minute read • October 12, 2022

Designing Microservices Components using Hexagonal Architecture

Senior software engineer, Ildefonso Garcia, sheds some light on what our software engineers are up to. Learn why sennder is transitioning from a monolith to hexagonal architecture.
ildeheader
authors
IS

Ildefonso Serrano Garcia

In this article, Ildefonso Serrano Garcia (he/him), senior software engineer, explains why and how sennder’s workflow team developed our current tech architecture–including the reasons behind sennder’s decision to move away from monolith architecture, and a deep dive into how hexagonal architecture is being introduced as a best practice in their development process.

About the author: Brining 20 years of experience, Ildefonso joined sennder in May, 2022 as a senior software engineer on the workflow team. Having worked primarily with Java before, he says since joining sennder he has become a Python lover. Ildefonso stays up-to-date regarding new architecture technologies, and enjoys learning from, and sharing knowledge with, his colleagues.

sennder's blueprint for logistics tech

As Europe's leading freight forwarder, sennder’s primary work is to automate the connection of freight supply and demand through digitalization. We do this primarily through several apps that address the needs of both shippers and carriers respectively. These apps make up our tech environment that we call sennOS.

sennOS contains:

  • octopus: An internal platform where sennder employees assist shippers and carriers.

  • orcas: A digital portal, where carriers can manage their fleet of drivers and trucks, and track ongoing, upcoming and completed orders.

  • otters: A digital platform where shippers can find capacity and get a price for their transport.

    Hexagonal architecture

The above figure illustrates sennOS’s basic architecture. The different applications, or front-ends, connect to the core of our system, or monolith, that contains all the business logic of our system.

Let’s focus especially on the core or business logic, our big monolith, and the journey we’ve begun to create more scalable architecture. Originally, the monolith was one data system built to store multiple types of data at once.

A mantra that comes to mind when thinking about monolithic architecture is, “One structure, one piece of code.” While the company was growing, our monolithic architecture allowed us to rapidly develop, and to make quick changes as needed.

Primarily its structure had four layers:

  1. The presentation layer: This layer is responsible for the publication of APIs that are consumed by the different applications.
  2. The application layer: This layer contains all the logic required by the application to meet its functional requirements.
  3. The domain layer : This layer represents the main domain entities.
  4. The infrastructure layer : Also called the persistence layer, this layer is responsible for all the technical stuff like persisting information in a database, producing events and so on.

Where the main rules for this architecture are:

  1. Each subsequent layer depends on the layers beneath it.
  2. All of the dependencies go in one direction.
  3. No logic related to one layer should be placed in another layer.

Hexagonal architecture-sennder

Monolithic architecture development under this approach allowed for simplicity, consistency, separation of concerns, and quick search from a technical perspective.

However, sennder has grown a lot as a company since its monolithic architecture was designed, and it adapted its offering to fit a changing market. As sennder’s services evolved, eventually some of the monolith’s strengths led to other weaknesses. Specifically, the monolithic structure created high coupling between layers. In order to de-couple layers, it became necessary to redefine the way sennOS, and specifically the monolith, was built.

A microservices revolution

Since May 2022, I have worked with teams of sennder engineers to decompose the monolith, and to build up specific services. Put simply, we are converting to a microservices architecture style in order to solve performance issues and improve scalability.

Without getting into the gritty details about our conversion to microservices architecture, our main goal was to create boundaries between sets of data. Within the monolith***,*** data is shared between various platforms, such as otters and orcas, so the interactions are coupled. We needed to decouple these interactions, so that services can function independently.

During this journey we’ve learned and applied domain driven design (DDD) methodologies to define boundaries and domains. As a result, we’ve gained knowledge, governance and control of part of the business logic that currently resides in the monolith.

Essentially we are creating a mesh of tiny services, each accomplishing a specific function. The mantra here is, “One business functionality running in one service.” This enables dedicated teams to develop domain specific services independently.

These services are implemented depending on each case, using software components developed in serverless technologies, or implementing microservices using a broad technology stack.

Hexagonal architecture

Our components (microservices / serverless) needed data which was still provided by the monolith as the source of truth, but the monolith was set to be dissolved completely in the future. So we needed to prepare a simple way to transition our data sources. This gets exceptionally tricky because our data sources are distributed along our architecture across many services and even using different protocols: JSON API, GraphQL and more.

An introduction to Hexagonal Architecture

Our main purpose in the implementation of our services components, serverless or microservices, is to have well-defined, decoupled business boundaries separating the core business logic they implement from its dependencies–putting inputs and outputs at the edge of our business logic. This way we don't depend on how we expose or consume data. This gives us the ability to swap data sources, and keep our core business logic isolated.

There are different models, or architectural styles, for the implementation of the objectives that we are pursuing. A few common models include: hexagonal architecture, onion architecture, and clean architecture. Each of these are conceptually similar and are based on ports and adapters. For sennder’s purposes we’ve decided to stick to hexagonal architecture for now.

As a developer, hexagonal architecture changes our mindset from a conceptual layered model to a model based on the separation of the business logic from its dependencies. These consist of inside and outside parts, where inside parts include the business logic (what we call “domain”) and outside parts, which consist of everything else and can be divided into two blocks for simplifying: interfaces and infrastructure.

While the outside (interfaces / infrastructure) sources change, the data sent to Business Logic is unaffected. The way to achieve this isolation between Business Logic from its dependencies is clearly defining models, repositories, and interactors.

In this case models are domain objects that represent our business, repositories are interfaces to communicate with data sources to create, change or update our business models, and interactors (or services) are responsible for performing business logic actions.

By using interfaces we are able to define business logic decoupled from its dependencies, without any knowledge or care of where the data is kept and how business logic is triggered.

Interfaces here refer to the entry or invocation point of our business logic. It is responsible for sending requests to the business logic to handle a domain specific request. Infrastructure is essentially the adapter to different storage implementations.

The great advantages of this architecture is that we are able to encapsulate data source implementation details and we have a clear separation of components, preventing the leaking of code between them. In the hexagonal architecture all dependencies point inward:

  • **Core Business Logic (Business Layer) does not know anything about the interfaces or infrastructure.
  • The Interface layer knows how to use interactors (Services)
  • The Infrastructure layer knows how to conform to the repository interface.

Hexagonal architecture

Basic principles of Hexagonal Architecture explained

After describing hexagonal architecture we are going to describe in a simple way the principles that define it. I will use this diagram to explain them.

Basic principles of Hexagonal Architecture explained

Separate interfaces, business logic and infrastructure

We have got an explicit and clear separation in our source code into three clear and well defined blocks of code that can be developed independently while keeping in mind the information each layer needed from the other to make it functional.

Separate interfaces, business logic and infrastructure

  • Interfaces are the entry points to our system to interact with. Contains code that allows this interaction.
  • Business Logic is the block of code we want to isolate from the Interfaces and Infrastructure and contains and implements Business Logic.
  • Infrastructure, this is what the business drives to work. It contains code that interacts with databases or maybe publishing events in a broker.

It's important to keep this this clear separation for two reasons:

  1. At any time we can choose to focus on a single problem.
  2. Easy to understand without mixing the constraints of each layer.

Dependencies going inside

Dependencies going insideEverything depends on the Business Logic, but the Business Logic does not depend on anything.

Isolate business logic boundaries with interfaces

Isolate business logic boundaries with interfacesInterface drives the business logic code through an interface and the business code drives the infrastructure code through an interface.

**Interfaces act as insulators between inside and outside, **establishing a contract between layers making them changeables.

How to test hexagonal architecture

When defining Hexagonal Architecture, the main point is its testability–how easy it is to test. Here’s a simple way to test it:

  • Services: Represents the entry point for invoking business logic regardless of the implementation of the persistence mechanism. Use dependency injection, and mocking any kind of repository interaction.

How to test hexagonal architecture

  • Data Sources: Check if they integrate correctly with other services, whether they conform to the repository interface, and check how they behave upon errors.

How to test hexagonal architecture Data Sources

  • Integration Tests: Finally, check that all the pieces of the puzzle are correctly aligned, from our input interfaces layer, through the services, repositories, data sources, and downstream services. These tests check if everything is wired correctly.

How to test hexagonal architecture integration tests

In summary–Why sennder switched from a Monolith to Hexagonal Architecture

Hexagonal architecture is a clean concept which helps us to create clear code without unnecessary technical details. It also helps us achieve flexibility and testability, making software development efficient and simple. As an engineer, it allows me to focus on the problem I am trying to solve, without using unnecessary technologies or frameworks. Furthermore, it's ‘technology agnostic’ so we can migrate from one framework to another without issue.

But like other architectural styles, it has its own limitations and downsides. The main downside may be that it is difficult to implement at first, because it requires a huge code review to isolate the domain.

Since sennder was looking for business logic isolation, and a good maintenance code with a full testable stack and good readability, hexagonal architecture and any evolution of this architecture is a good choice.

authors
IS

Ildefonso Serrano Garcia

Share this article