To ensure that our projects are solid, scalable and able to adapt to future changes, the application of a good design architecture is essential. The hexagonal architecture helps us to achieve this goal.
Software architecture: definition and significance
In the field of development, we are confronted with increasingly complex systems that need a solid structure to facilitate their creation, maintenance and future growth. For this reason, the concept of architecture is becoming increasingly important in the field of software.
The software architecture establishes a set of defined and clear frameworks for interaction with the source code and defines in an abstract way the set of components, their interfaces and the communication between them.
This architecture is created based on various goals, not only functional, but also other goals such as maintenance, testability, reusability, flexibility (in terms of changes) and independence from other systems.
Examples of architectures are: Model-View-Controller, Client-Server, Service Oriented (SOA), Event Driven, Layered Architecture or Hexagonal Architecture, to name a few.
Introduction to hexagonal architecture
In 2005, Alistair Cockburn published an article in which he described that the intention of the hexagonal architecture was that an application could be used by users, programs, automated tests and scripts in the same way and that it could be developed and tested both in isolation from its eventual devices and databases during execution.
This architecture, also called ports and adapters architecture, proposes to divide our application into different layers or domains, each with its own responsibilities, so that they can develop in isolation and each of them is testable and independent of the others.
To achieve this layer independence, the concept of ports and adapters is used. A port is nothing more than a logical concept that defines an entry and exit point of the application. The function of the adapter is to implement the connection to this port and other external services. In this way, we can have multiple adapters for the same port. For example, our framework will adapt one SQL port for each number of different database servers that our application can use.
There are two types of connectors and adapters: primary and secondary. The difference between them is which one triggers the call or which one is responsible for it.
In the case of primary ports and adapters, it is the user who makes a request to the application via the user interface. For example, a user can request an entry via an HTTP request. These ports and adapters can be seen on the left side of the hex diagram.
For secondary ports and adapters, on the other hand, the action is triggered by the application. For example, a database persistence request could come from an action of a primary adapter. These cases are shown on the right side of the hexagon.
The shape of the hexagon has nothing to do with the number of sides, but rather with the form of presentation, as each side represents a port that either goes into or out of the application.
The hexagonal architecture proposes to describe the application in several layers. The reason for this is to achieve a conceptual division of the different areas of the application. The code of each layer would describe how to communicate with the others via interfaces (ports) and implementations (adapters).
In this way we will have an application that will be as follows:
- Independent of frameworks: The project will never be dependent on an external framework, as there will always be a layer that abstracts the logic and allows frameworks to be changed without affecting the application.
- Testable: The business processes can be tested independently of the interface and external agents.
- UI-independent: The system is not dependent on the graphical interface and this can be changed without affecting the business processes of the application.
- Database independent: The application domain does not know how the information is structured and stored in a repository.
- Independent of external agents: The business processes have no knowledge of the existence of an external agent. They only need to know what these agents need to fulfil their tasks.
The layers into which we can divide our system using this architecture are:
1. The domain layer
It is the central layer of the hexagon and contains the business rules. In it we find the data models and their constraints.
This layer does not know how the repository information is structured, stored and retrieved. It simply exposes a set of interfaces (ports) that are adapted in the infrastructure layer for each specific case of implementing this persistence.
2. The application layer
Directly above the domain layer is the application layer, where the different use cases are defined. When defining the use cases, we think of the interfaces that are available in the application’s hexagon and not of any of the available technologies that we can use.
In this layer, the various requests, that the application receives from the infrastructure layer, are also adapted. For example, a use case accepts input data, coming from the infrastructure layer and executes the necessary actions to return the output data to that layer.
3. The infrastructure layer
This is the outermost layer of the hexagon and corresponds to the implementations or adaptations of the interfaces or ports of the other layers.
Normally this layer corresponds to the framework, but it also contains third-party libraries, SDKs or any other code that is external to the application.
The infrastructure layer implements the services defined in the application layer (secondary adapters). For example, if we have defined a service for sending e-mails or SMS in it, we implement this service in this layer according to the requirements of the provider or an external library.
In addition, this layer contains everything that has to do with the interaction with the user (primary adapter). It receives some input data that is used to query the corresponding use case in the application and returns some output data. HTTP drivers or command line scripts are found here, among other things.
4. Communication between the layers
As we have already mentioned, each layer must define a set of ports that are adapted for each concrete implementation. These ports are the class interfaces that define how each external layer can communicate with the current layer.
To achieve this, we use dependency injection, i.e. we inject the dependencies into the class instead of instantiating them within the class. In this way, we have decoupled the classes of the other layers so that they depend on an interface instead of a concrete implementation.
So, what we are achieving is to reverse the control of the application, preventing our program from being dependent on a particular technology, allowing the technology to adapt to the requirements of the application.
At WATA Factory we have used this architectural concept in some of our biggest projects. It allows us to achieve isolation of each of the layers, gives us flexibility when we make a change to the infrastructure or an external service, easy testing and most importantly it helps us apply SOLID to get cleaner and more maintainable code over time.