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.
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 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 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 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
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.