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.
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:
- The presentation layer: This layer is responsible for the publication of APIs that are consumed by the different applications.
- The application layer: This layer contains all the logic required by the application to meet its functional requirements.
- The domain layer : This layer represents the main domain entities.
- 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:
- Each subsequent layer depends on the layers beneath it.
- All of the dependencies go in one direction.
- No logic related to one layer should be placed in another layer.
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.
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.
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.
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.
- 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:
- At any time we can choose to focus on a single problem.
- Easy to understand without mixing the constraints of each layer.
Dependencies going inside
Everything depends on the Business Logic, but the Business Logic does not depend on anything.
Isolate business logic boundaries with interfaces
Interface 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.
- Data Sources: Check if they integrate correctly with other services, whether they conform to the repository interface, and check how they behave upon errors.
- 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.
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.