GO TO:
Ports and adapters, aka hexagonal architecture
Ports and adapters architecture, also known as hexagonal architecture, is a pattern that imposes the way an application’s code is built and organized. The main assumption of this pattern is to create code in such a way as to maximally separate the implementation of business logic from any external dependencies, such as frameworks, databases, external services, etc. As a result, we gain greater control over the code and independence from external libraries and the changes made to them. Let’s take a closer look at the pattern:
Also read: Microservices architecture pattern
Ports and adapters pattern
The pattern of ports and adapters divides the code structure into two areas:
- internal (application / domain)
- external (infrastructure)
The internal area focuses on the solution and implementation of the main problem (use case). This area does not make any reference to or draw from frameworks, databases, and other services or external libraries. The external area, in turn, includes the implementation of ports in the form of adapters and classes related to frameworks, such as Spring, Hibernate, etc. Communication between the internal and external areas is via ports and their implementation, that is, adapters (hence the name of the pattern).
Some basic assumptions of hexagonal architecture:
- The internal area, that is, the proper implementation of logic, “does not know anything” about the external area. There is no relationship between this area and frameworks (like Spring, Hibernate, etc.).
- The problem solving logic is closed in the internal area. We have access to it only through the class of the domain facade.
Implementation of this type of software architecture
The main goal of the example is to present the implementation of classes and package organization based on the concept of ports & adapters architecture. An example is the domain area responsible for registering new users in the system. Two use cases have been implemented here:
- Registration of a new user – includes functionalities related to generating an account activation code and sending an activation e-mail.
- Activation of the user account – verifies the generated code, activates the account and sends a message to the external system.
Packages
- Package: user
This is the main package of the domain within which we will operate – namely user domains. This package contains two subpackages: crud and registration.
2. Package: user.crud
This package holds the logic associated with simple data retrieval operations.
It can also include other operations that do not have complex business logic and only serve simple operations such as CRUD (create, read, update and delete).
3. °Package:°user. Registration°° user.registration°Package:°user. Registration°° user.registration
This package contains all classes related to the logic of registering new users in the application (implementation and handling of the aforementioned use cases).
4. Package: user.registration.domain
The package contains classes with the main implementation of business logic and ports’ definitions (Java interfaces) through which communication with other system components takes place. The core.user.registration.domain package contains the vanilla Java code, or code that is not dependent on frameworks and other external libraries. The only access point to the implemented logic in this package is the façade class. Bear in mind: the choice of libraries to use (e.g. commons-lang, Dozer, Orika) inside the package is up to the team and depends on the general arrangements in the project.
5. Package: user.registration.infrastructure
This package includes the implementation of adapters that are used through the ports defined in the code. The package includes the implementation and configuration of components using the functions and methods of the frameworks. Libraries of other solution providers are often used here. These are responsible, for example, for generating PDF files, sending e-mails or communicating with external systems, such as Kafka, SOAP, etc.
6. Package: user.registration.infrastructure
This package should include a configuration related to creating and operating framework components.
7. Package: user.registration.infrastructure
This package includes controller classes and DTO classes, through which other systems or modules can use the functions of our domain. The DTO classes describe the input and output structures for the REST API.
8. Package: user.registration.infrastructure
A package of classes related to the persistence layer, meaning classes responsible for retrieving and saving data in the database. These are the definitions of the data model (ORM), the DAO classes. This package includes, among other things, the implementation of the data access adapter class. In more complex cases, we may have more model classes or refer to model classes (ORM) of other packages.
Hexagonal architectural pattern – description of an implementation of classes
- Classes from user.registration.domain:
- UserRegistration – the main class of implementation of business logic. Access to the class is limited at the package level.
- UserRegistrationDataProvider – port for I/ O operations related to database access.
- UserRegistrationNotifier – the output port for sending messages to the queue.
- ConfirmationMailSender – the output port for sending e-mail message.
The abovementioned ports are public interfaces of Java classes, the implementation of which is done at the level of infrastructure packages.
UserRegistrationFacade – domain facade. The class through which we can access methods (functions) of the domain. In the illustrated example, the facade class performs the calling function for the UserRegistration class – it acts as the wrapper function. In more complex cases, we may have more classes containing business logic. Then the facade is the access point to the calls of all these domain functions. Sometimes we may need to add some framework-related validations at this stage. This may be due to the fact that some other domain areas will need to use logic from another domain. The validation function can then be used at the facade level. Of course, as in the aforementioned cases, everything depends on the general arrangements and project assumptions in the team.
2. Classes from the user.registration.infrastructure package
Implementation of adapters:
- ConfirmationMailSenderAdapter
- UserRegistrationNotifierAdapter
- UserRegistrationDataProviderAdapte
3. The UserRegistrationService class is responsible for the transactionality of operations. This class calls the business logic code through the UserRegistrationFacade. For more complex operations at the level of this class, facades from other areas can be injected.
4. Classes from the user.registration.entrypoint package.
5. The UserRegistrationController class issues a REST API. It uses the UserRegistrationService class to execute transactions. In the event that operations do not require transactions, we can directly use the facade class here.
In the example implementation of the domain package, you can see many classes – this is one of the attributes of this pattern. Thanks to this, we gain the hermeticity of implementing business logic and structures that are related to it. The access point to the domain function is the facade class. By default, domain classes should not use framework classes, and therefore a larger number of data-related POJO (plain old Java object) classes. In the case of complex use cases, you can create more classes with business logic implementation; for example, you can separate account activation into a separate class
Ports and adapters – FAQ
-
Why do we need to restrict access to certain classes at the package level?
Using class access modifiers and methods, we hide the details of the implementation – that is, we use the so-called “class encapsulation”. We thus ensure control over the access, condition and behavior of the object. In our example, in the domain package, the facade class and the classes representing the input and output data have public access. The entire implementation of the logic is closed at the package level and access to the operation is only possible through the methods defined in the facade class.
-
How about libraries such as Lombok, JSR 303 (validation) or loggers in the domain package?
According to the main concept of hexagonal architecture, we should not use external libraries. However, in practice, this is not quite the case. The libraries we use inside the domain depends on the team, because each member of the team has different experiences and ideas. However, it is worth applying some restrictions when it comes to these tools. In the projects I participated in, we were mainly limited to libraries like Java commons, mapper libraries and validation libraries.
-
Ports and adapters – where to set up transactions?
One of the most important elements of the implementation of operations is their transactionality. Often the required operation will need to use a function from another code domain or we will want to call the domain function in the transaction. Then we need to ensure transactionality for these function calls. In this case, you can build a dedicated service that will contain classes of domain facades.
In the example, the service was implemented in the UserRegistrationService class.
-
Why shouldn’t we set up transactions in the facade class?
The lack of transactions in the facade class results from the assumption that the code in the domain package should remain free of all frameworks.
-
Can I use Domain-Driven Design with the hexagonal architecture?
DDD (Domain-Driven Design) focuses on the domain and its business logic. It has nothing to do with frameworks and transactions. In the example, the DDD part was implemented in the domain package. The ports & adapters pattern helps us organize the code and separate access to the implemented logic. We can say that this pattern is more responsible for the application layer of the solution, because it is accountable, among other things, for providing configuration, transaction or persistence mechanisms. The DDD approach and the ports & adapters pattern work together in code structures. DDD solves a logical problem, and P&A provides access to technical mechanisms and libraries.
-
Should we always use the ports & adapters pattern?
If we are wondering when to use hexagonal architecture, the aspect of business logic will be decisive. The ports & adapters pattern is ideal for complex business logic, when we want to focus on solving the problems that have been issued there. The code is independent of all frameworks, making it easy to manage and test. Tests are run quickly and written easily. We write fewer integration tests – a large part of the logic is covered by unit tests. In integration tests, we verify critical paths.
This approach requires the creation of more classes in order to separate them from the rest of the code outside the package, which at first glance seems excessive. However, thanks to this, it is easier to separate logic into other independent modules (in microservice architectures, there is often a need to separate logic into another service).
In the case of applications that have little or no business logic (CRUD applications), it is better to build a code structure in a multi-layered form (Controller – Service – Repository). In the example, a CRUD package was separated, in which we implemented a simple operation for downloading the list of users.
Ports and adapters design pattern – summary
I hope that I have been able to present the main ideas of coding in the hexagonal architecture, and answer some of the questions that may arise during implementation. Of course, everything may differ depending on the project.
It is entirely up to you. The team creates the software, so organization and code structure should be readable for everyone and make work more convenient. In fact, how the pattern will be implemented depends on the team and their decision.