Every architectural pattern enables the achievement of some quality attributes. On the other hand, achieving a set of a system’s quality attributes makes other quality attributes harder to accomplish. Here at Cardo AI, we solved this opposition using Hexagonal architecture.
Hexagonal architecture is an architectural pattern for building software systems that are testable, decoupled, flexible, and maintainable. Hexagonal architecture is a pattern that helps you shift the main focus to the business domain while spending less time on the technical aspects of it.
Read through to fully understand why we made this choice, along with the benefits and challenges that follow it.
First: what is Software Architecture?
Before diving deeper into Hexagonal Architecture, let’s clarify what Software Architecture is.
There are a lot of famous experts in the software world that have come up with their own definitions of software architecture.
“Architecture represents the significant decisions that shape a system, where significant is measured with the cost of change.”
Ralph Johnson, a member of the Gang of Four, the authors of one of the most influential computer science textbooks: Design Patterns: Elements of Reusable Object-Oriented, explained it this way:
“Architecture is about the important stuff… Whatever that is.”
There are many more descriptions, and so far it seems like we don’t have a consensus around the definition of software architecture amongst experts in the software space.
The Pillars of Software Architecture
While agreeing on an accepted definition of software architecture seems impossible, we can definitely talk about the main ingredients of software architecture.
Neal Ford & Mark Richards, in their brilliant book Fundamentals of Software Architecture: An Engineering Approach, came up with four dimensions that make up software architecture.
I like to call them the “Pillars of Software Architecture.“
|System Structure||Architecture Characteristics||Architecture decisions||Design principles|
|This refers to the style or pattern through which the system is implemented, such as Microservices, Event-Driven, Layered, etc.||Quality attributes, known as “ilities” or non-functional requirements of the system, such as scalability, flexibility, availability, etc. They do not require knowledge of the system’s functionality but are required for the system to function properly. They are a crucial aspect of the architecture because they can drive the system toward a specific structure (pattern)||These are the hard rules for constructing systems. They are constraints that impact the development of the system and are usually about what is not allowed. One example of an architectural decision would be restricting the database access from all the layers except the service layer (in a typical layered architecture)||Contrary to architectural decisions, the design principles are some guidelines about how software should be constructed rather than hard rules.|
Common challenges in building software systems
Building software systems is far from a trivial task. Software systems tend to grow over time and become more and more difficult to maintain and extend.
Some of the most common problems  that we see in software systems nowadays are:
In most cases, almost all of the decisions are made right at the beginning when building the system. This could be a big problem in the future when requirements change. Also, the path that the system is going to take will eventually evolve. As we are all aware, the only guaranteed thing to happen during the software life-cycle is change.
The absence of some hard rules, as well as some guidelines on how the system should be constructed, can lead to spaghetti code.
In spaghetti code, everything is intertangled. You end up having components doing what they’re not supposed to and others not doing what they’re supposed to, and the interaction between them leaves a lot to be desired.
Hard to change
The bigger the system becomes, the harder it is to change it.
Systems become hard to change when lacking good structure and clear boundaries, or when the economic cost of change is too high, which does not justify that particular change.
Built around frameworks
To me, this is one of the biggest challenges we face. Although frameworks are great tools and they do a lot of heavy lifting for us, they tend to force us to write software around the framework’s architectural decisions, leaving us with very few, if any, options for overriding these decisions. They can be decisions like a specific pattern (mostly MVC), the choice of a data model (usually a relational model), etc.
Database as the center of the system
The database plays a crucial role, but often is considered to be the center of the system.
Designing with a database-first approach leads to the tight coupling between the business rules and the data model.
The database is a persistence mechanism and, as such, should be used to store and retrieve data.
With the components directly depending on each other, it becomes very hard to test them in isolation, so very few options are available in that space.
Some of the limited options that exist, in this complex, tightly coupled set of components are end-to-end and integration testing, which usually takes a lot of time to run.
This is because these systems are not designed for testability, the architecture is not decoupled enough, and the dependencies cannot be stubbed out for increasing test runtime.
What is Hexagonal Architecture?
Hexagonal architecture is a general-purpose architectural style that aims to create decoupled software.
As the author himself, Dr. Alistair Cockburn states:
“It Allows an application to be equally driven by users, programs, automated tests, or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.” 
This architectural style is also known as Ports and Adapters, and that’s because Ports and Adapters are the main properties of this pattern.
The key idea is to separate core business logic from external concerns such as databases, web frameworks, message queues, and so on, in a way that the application itself is not dependent on any particular technology.
The general rule of thumb is that nothing comes in, and nothing goes out without going through the ports.
Our implementation of hexagonal architecture consists of:
- The core, which is the central part of the application, contains high-level policies. It has a domain model, which serves as a foundation for the use cases of the system.
- Ports are simply interfaces that the hexagon (application) defines to interact with the outside world. They also allow the outside world to interact with the hexagon.
Ports, as shown in the diagram below, are divided into two kinds of ports. The left-hand side ports are known as driver ports (primary ports), and the right-hand side ports are known as driven ports (secondary ports).
- Driver Ports are the API that the application defines to let the outside world interact with it.
- Driven Ports are the interfaces the application defines to communicate with the external systems/devices.
- Adapters are concrete implementations. They belong outside the core hexagon and are technology-specific implementations for interacting with the hexagon.
Similar to ports, there are two types of adapters: driver adapters and driven adapters.
- Driver Adapters are components that use a port defined as the application API to interact with the core hexagon.
- Driven Adapters are the concrete implementation of the ports defined within the core hexagon, which allows the hexagon to interact with the outside world to complete its tasks.
- Infrastructure is the lowest level of the system. It generally contains very little code. The code that’s required to communicate with the core hexagon and configurations of the system dependencies.
The History of Hexagonal Architecture
Decoupling business rules from peripheral concerns dates back to the early 90s, specifically in Ivar Jacobson’s use-case-driven method.
Initially, Jacobson’s pattern was named EIC (Entity-Interface-Control). Shortly, the term “boundary” replaced “interface” to avoid the potential confusion with object-oriented programming language terminology.
Other patterns similar to Hexagonal Architecture are Onion Architecture by Jeffrey Palermo , and Clean Architecture by Robert C. Martin .
Although they all introduce their own concepts, they also all employ the dependency inversion principle, which states that high-level modules (business rules) should not depend on low-level modules (technology-specific modules); both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.
This principle is proposed by Robert Martin, who describes that by following this principle, source code dependencies can be inverted and not be constrained to align with the control flow.
With the ability to invert source code dependencies, you gain absolute control over the direction of the source code dependencies of the system.
Pros and cons of Hexagonal Architecture
Although the software engineering discipline has made huge advancements, we are yet to experience any development that is a silver bullet.
When designing software systems, we usually focus mainly on the benefits of a particular pattern or technology. However, making a decision based only on the benefits that it provides leads to many problems in the long run.
Rich Hickey once said that programmers know the benefits of everything and the tradeoffs of nothing.
Like every other pattern, Hexagonal Architecture comes with its good and bad parts.
Positive features of Hexagonal Architecture
These are the 5 main benefits of using Hexagonal Architecture as a software design pattern:
It is one of the main benefits of Hexagonal Architecture.
Decoupling the business rules from external concerns such as Database, Framework, UI, and other dependencies allows you to test those business rules independently.
The business rules depend on some abstractions (ports), and the concrete implementation can be easily mocked out. In turn, it facilitates the writing of automated tests and also makes the tests run faster.
This is yet another good part of this pattern. The ability to switch between different technologies is what makes this pattern a plugin-style architecture.
As long as you have a given port, you can simply change the concrete implementations without having to touch anything inside the business rules.
3. Technology agnostic
The parts of the Hexagonal architecture that contain the technology-specific code are adapters, and adapters are the concrete implementations of the ports defined within the core domain.
Having said that, the business rules source code is not affected by changes in a particular technology-specific adapter. Those changes are isolated at the adapter level.
4. Deferring decisions
When we start developing, we can focus just on business logic, deferring decisions about which framework and technology you are going to use. You can choose a technology later, and code an adapter for it.
Deferring decisions is very useful in the sense that the requirements may change, and that decision about a specific technology may not be a good choice anymore.
Even though deferring decisions has great benefits, it is always good to not defer them forever, only defer decisions until the last responsible moment.
5. Focus on the business domain
As we understand that the main goal is decoupling the business rules from technology-specific concerns, we can start focusing immediately on the most crucial aspect of the system, that is the business domain, and defer the technology-specific decisions to the last responsible moment.
The Cons of Hexagonal Architecture
The following are the not-so-good aspects of Hexagonal Architecture:
Applications built using the Hexagonal Architecture pattern can be harder to debug due to not directly using concrete implementations.
Indirection can add complexity. There’s a famous quote by David Weeler about indirection. “All problems in computer science can be solved by another level of indirection”. And it is extended with humor: “except for the problem of too many levels of indirection”.
I think the extension of the quote, even though used with humor, could be considered a drawback in the context of hexagonal architecture because of the misunderstanding of what a port is, and everything that’s used much more than necessary ends up being a bad idea.
When the business domain is modeled independently of a database or another technology, translating between models used for persistence or communication and domain model can be awkward. This problem becomes worse when the models are fundamentally different, both technically and conceptually, from each other.
4. Steep learning curve
Contrary to traditional architectural patterns, usually forced upon developers by frameworks, hexagonal architecture has a steeper learning curve. Indirection, translation, principles, and design patterns applied to enable this architectural pattern can be challenging for relatively new software developers.
Hexagonal at Cardo AI
Here at Cardo AI, we leveraged the hexagonal architecture in some of our services. It allowed us to focus more on the business domain and spend less time on the technical aspects of it.
Decoupling technical concerns from business rules allowed us to delay decisions, especially the technical ones. It also made it possible to switch between different technologies without actually affecting the business rules.
Also, the hexagonal architecture made our services more testable and improved test runtime.
At its core, hexagonal architecture has ports, and levels of indirections added a bit of complexity to our services. Therefore, as we expected, mapping between the domain model and external models used for data storage/retrieval turned out to be a bit of a challenge.
Usage of this architectural pattern offers tremendous benefits for designing software systems that are testable, decoupled, flexible, and maintainable, and last but not least, this pattern helps in building software systems that are resistant in the face of technological advancements and time.
At its core, the hexagonal architecture enables reversibility of the decisions, and as Martin Fowler concluded: One of an architect’s most important tasks is to eliminate irreversibility in software design.
However, the complexity involved in designing and building software products with this pattern, indirection, translation between models, and a steep learning curve for developers in terms of understanding the concepts and principles can be a deal breaker.
Going down the rabbit hole of patterns, in general, is not a good idea.
As mentioned in the introduction, each architectural pattern enables the achievement of some quality attributes, which we, as designers should identify before deciding on that particular pattern.
Identifying the right quality attributes of the system and choosing the least-worst set of trade-offs that your system can tolerate in order to be a success story, are some of the most crucial aspects of software architecture.
Acknowledgments & References
Special thanks to everyone listed below for insightful information and inspiration: