Hexagonal Architecture Explained: Part 1

As software engineers, we aspire to build great products in the most efficient way possible, often using new, improved and advanced technologies. Unfortunately, we frequently come up against a common roadblock: Complex codebases that are difficult to decouple.

Complex codebases make it exceedingly tricky to integrate with new frameworks, libraries, and languages, leading to a number of issues, such as:

  • Huge cognitive load due to front-end changes being made while relying on production servers to populate the UI

  • Limited ability to work offline due to development servers needing to be deployed and paid for

  • Scattered business logic that exists both in the front and back-end of your application, turning a trivial task, such as a simple UI change, into an arduous one

  • Steep learning curve before new engineers can understand and navigate the codebase

The Cytora engineering team had grand visions of constructing a decoupled, pluggable architecture that could be built for production and run efficiently at scale. We found one of our solutions in hexagonal architecture - an incredibly simple form of application architecture that creates a clear separation between the application's core logic and the services it uses.

In this blog post, the first in a series of three, we will outline our working methodology of hexagonal architecture, and aim to demystify some of the concepts surrounding its implementation.

What does our ideal solution look like?

A truly decoupled architecture allows you to unplug heavy machinery in favour of a local, lightweight mock development “plug”. This greatly reduces the cognitive load in development, allowing engineers to focus on the task at hand instead of setting up a complex development environment.

Following the simple principles of hexagonal architecture, engineers are able to build in a much more dynamic and agile way.

The first thing to note about hexagonal architecture is that it is not a framework or library that you can install, it is simply a set of rules to be followed.

An overview of hexagonal architecture. 

An overview of hexagonal architecture. 

Business logic

Business logic is the linchpin of hexagonal architecture. The business logic is depicted as the centre of the universe of your application.

This is where the rules of your application lie, acting as a source of truth. This means that instead of having the rules that control the flow of your application split across the front-end and back-end, you encapsulate all of the rules in one place, creating a central point from which all logic is controlled.

Plugs

Plugs are often described as ‘ports’. For the sake of simplicity, we will describe any component that interacts with the business logic, as either a consumer or a dependency, as a ‘plug’. Examples of consumer plugs include UIs and  REST APIs, dependency plugs can be databases, a payment system on the back-end etc.

Relationships

Relationships describe the interactions between plugs and the business logic. Usually, these will take the form of methods that have to pass through the business logic to be handled.

Interfaces

Interfaces are a key component of hexagonal architecture. Interfaces describe the points of contact between plugs and business logic. They should be designed in a uniform fashion to work effortlessly across multiple plugs. It is essential that these terms are clearly defined and agreed upon within your team. Well-defined and documented interfaces allow for more flexibility and make it much simpler when building and interacting with plugs.

How is this different from Object-oriented programming (OOP)?

This is a common question with a fairly simple answer;

  • Hexagonal architecture focuses on the reconfiguration of the components of an application

  • It provides you with one source of truth in the form of your business logic, as opposed to a more scattered scenario you may encounter with OOP

  • The two concepts are not mutually exclusive - hexagonal architecture can exist within OOP

Example of a to-do list application built with hexagonal architecture

When a plug makes a request, the business logic returns an error message before any other action is taken. The laws for the application live within.

When a plug makes a request, the business logic returns an error message before any other action is taken. The laws for the application live within.

When a plug requests an action which may impact another plug, this transaction is handled by the business logic, which “routes” the request to the appropriate third party plug, and then returns the success/error call.

When a plug requests an action which may impact another plug, this transaction is handled by the business logic, which “routes” the request to the appropriate third party plug, and then returns the success/error call.

When there is a plug level error such as a database being unavailable, this error message is also handled via the business logic through to the relevant plug, in this case, a UI.

When there is a plug level error such as a database being unavailable, this error message is also handled via the business logic through to the relevant plug, in this case, a UI.

As we continue to add complicated tasks, such as creating a current list of tasks from our task store, the business logic handles all of the transactions in the process. We ensure that plugs do not violate the rules of hexagonal architecture by bypassing business logic.

As we continue to add complicated tasks, such as creating a current list of tasks from our task store, the business logic handles all of the transactions in the process. We ensure that plugs do not violate the rules of hexagonal architecture by bypassing business logic.

By putting all of the previous steps together, we end up with a complex system of plugs connected to our business logic. Due to uniform interfaces, the app process-flow is easier to understand and reinforces the importance of having our business logic as the source of truth for our application.

By putting all of the previous steps together, we end up with a complex system of plugs connected to our business logic. Due to uniform interfaces, the app process-flow is easier to understand and reinforces the importance of having our business logic as the source of truth for our application.

In a perfect world, teams would work with complex codebases in the same manner that teams collaborate on the construction of a commercial airliner; no single engineer will have a holistic and granular overview of the entire design. One team is concerned with the interior, while another team is concerned with the engine. This separation allows for high degrees of specialisation and modularity.

Because of their separation of concerns, when teams do need to work in a cross-functional manner, issues become very apparent and easy to resolve. With the number of technologies and teams working on building complex codebases today, we chose to explore a similar model; hexagonal architecture.

In part two of this blog series, we’ll provide you with the code to execute the theories discussed above and expose the methodology we use for hexagonal architecture at Cytora.