Hexagonal Architecture
First, let’s remember the concept of “layered architecture”. Many software developers use layered architecture in enterprise applications. Although this architectural approach is frequently used, it brings some problems as the complexity within the application increases. In this architecture, modules communicate with the same level or lower-level modules. As complexity increases, the level of coupling rises while cohesion unintentionally decreases. After a while, adding a new feature to the application becomes very challenging. This situation is also referred to as the “lasagna anti-pattern”.
Hexagonal Architecture
This architectural approach was proposed by Alistair Cockburn in 2005. The main goal of this approach is to separate the domain and infrastructure layers. By doing this, we prevent the domain and infrastructure code from mixing, resulting in an application that is easier to add new features to, more flexible, and easier to test. Essentially, in hexagonal architecture, we separate frequently changing parts from less frequently changing parts. The domain layer changes frequently with the addition of new features or modifications to existing features, while the infrastructure layer changes less frequently.
- Domain
We can describe the domain layer as the heart of the application. It contains all the business rules and logical decisions. This layer should not be dependent on any structure (such as frameworks). However, some dependencies can be added to this layer, such as Apache Commons’ StringUtils, CollectionUtils, or unit test dependencies. It is beneficial to create the domain layer as a separate application. When the domain and infrastructure layers are in the same application, their codes may unintentionally mix, resulting in a dirty codebase. When developing an application with hexagonal architecture, the starting point can be the domain. This allows us to write the core functionality of our application without being distracted by the technologies to be used (outside-in vs. inside-out). This approach is entirely at the discretion of the developer. Having only one entry point to the domain layer helps avoid confusion in the infrastructure layer about which entry to use. The infrastructure layer should not use domain models as controller request models; instead, there should be request DTO objects. Using domain models can inadvertently affect the infrastructure layer if changes are made to the model. Therefore, it is healthier for these to be separate objects. The connection between the infrastructure and domain layers should always be made through an abstract class. Direct access to a domain class should not be allowed.
- Infrastructure
This layer handles persistence (e.g., DB, Redis, Elasticsearch), event publishing, or REST calls. Additionally, this layer can be considered the application’s gateway to the outside world. A framework can be used in the infrastructure layer, or dependencies of the technologies to be used within the application can be added to this layer. As mentioned earlier, it is beneficial to create the infrastructure layer as a separate application. If the domain and infrastructure layers are separate applications, the domain application should be added as a dependency to the infrastructure application.
- Port
Ports can be thought of as gateways used to access the infrastructure layer from the domain layer or vice versa. A port is defined as an interface and resides in the domain layer. Ports used to go from the domain layer to the infrastructure layer are implemented in the infrastructure layer. The class implementing the port is essentially the adapter. This structure allows the domain layer to perform the tasks it wants in the infrastructure layer.
- Adapter
Primary/Driving Adapter: This is the entry point of the application. It includes structures triggered by REST/SOAP calls, listeners, schedulers, or CLI (e.g., controller classes, event consumer classes).
Secondary/Driven Adapter: This is where the ports created in the domain layer are implemented in the infrastructure layer. Examples include persistence operations, third-party application calls, and event publishing structures.
- Dependency Inversion Principle
The Dependency Inversion Principle, one of the SOLID principles, is highlighted and fully utilized with hexagonal architecture. To briefly recall, high-level modules should not depend on low-level modules directly; instead, they should depend on abstractions. In hexagonal architecture, we actually apply this principle with the concepts of ports and adapters.
Domain ve infrastructure ayrı bir uygulama yaptığımız kurguda, bunu bir nevi zorunlu bir hale getirmiş oluyoruz. Bu sayede alt seviye modüllerde yapılan değişiliklerden üst seviye modüller etkilenmiyor ya da minimum şekilde etkileniyor.
By creating the domain and infrastructure as separate applications, we make this principle almost mandatory. This ensures that changes in low-level modules do not affect high-level modules or affect them minimally.
In hexagonal architecture, the domain is defined as the high-level module, while the infrastructure is defined as the low-level module. Dependencies should be directed from the outside inwards, and the core of the application should be the domain.