Spring Boot application example with BDD tests
Nowadays fast-paced development is widely followed in software projects. The endless technological progress and the large adoption of Agile methodologies are boosting the development. Microservices came to light to enable a more distributed architecture to develop and deploy the solution in more parallel and gradual fashion. Although it is always welcome to speed up the development process it does not come without cost. The requirement’s expression tends to be more frequently changing. It is now more than a necessity to have the development process fast reacting to the requirement change. Centralizing the business logic and producing a good quality code and relying on well designed and well tested software are good options to achieve well managed and maintainable software. In this context the hexagonal architecture comes to the light adding a flexible and change tolerant design aligned with the DDD (Domain Driven Design) principles that reduces the cost of code evolving and the related technical debts.
Applications are composed of two parts: the business logic directly addressing the business need and the underlying necessary technical part (non-functional requirement). The hexagonal architecture aims to cleanly separate these two parts to keep focusing on the business part in the first place. Thus, reducing the requirement change cost and keeping the project code as manageable as possible. For that it defines three parts:
- Domain: the core business logic
- Application: the layer encapsulating the Domain and making the go between the inside business logic and the outside related infrastructure
- Infrastructure (also called framework): the technical code to adapt the outside technical implementations (third party vendor specific code) or the external client interactions (outside operations) to the application
These three parts are designed in a way that the Domain is the central part. The Application is encapsulating the Domain and the Infrastructure is surrounding the Application. It is to note that there is only a one-way dependency direction. The infrastructure depends on the Application and the Application depends on the Domain. The Domain is kept free of any technical code.
The domain contains the entities and the value objects that are directly connected to the business requirement. It also contains the related business rules that implement the business logic. The domain part is not depending on any technical code or third-party libraries except of the use of lightweight libraries such as Lombok (Project Lombok) or Mapstruct. The domain should be evolved and tested without any impact on the other parts.
The Domain is a good candidate to be built through a BDD (Behavior Driven Development) methodology as its first and main purpose is to implement the behavior beyond any technical dependencies.
The application part encapsulates the Domain hiding the business logic and exposing the various project use cases thus serving as an API for the project. It contains the different use cases exposed as interfaces and the related input and output ports.
Also called the framework part It contains the various adapters to both feed the application with inputs and to adapt the application to external interactions.
Let’s assume that we are building an application to handle payments. For simplicity reasons we assume our Payment business object as the following:
- payment identifier
- payment related account identifier
- payment amount
- payment status: in time, late…
Let’s suppose that we should implement the following requirements:
- fetch and filter the payments
- aggregate the payments by account
Below the related package structure:
Our payment business object can be translated into the following entity:
We add a value object (VO) to represent the payment status as an enum:
Then we start implementing our business logic by implementing the required business rules. For that we can follow a behavior driven development using Cucumber for example. Starting by defining the acceptance tests features.
First, we start with the fetch payments feature. We can define the feature as follows:
And we add the fetch payments test:
Then we complete the related test steps:
Finally, we implement the fetch business rule to address the payment fetching requirement:
We run the BDD fetch test to validate this fetch rule. The tests and the related business rules should be evolved as much as required until we meet the business requirement.
Once the target requirement is satisfied, we should have a successful test:
The same we add the acceptance test for the second aggregate business requirement:
And the related aggregate payments test:
Then we complete with the related test steps:
Finally, we implement the target aggregate payments business rule:
Once done we should reach a successful test:
Inside the application part we add the fetch and aggregate use cases expressing respectively the payment fetching and the payment aggregation capabilities provided by our business logic and implemented in the Domain part. These use cases are serving as an API to the business provided by the application.
To wire the application to the Domain we use input ports. As implemented in the following fetch and aggregate input ports:
It is to note that the fetch payment input port is using a payment loading output port. By analogy to the input port that is wiring the application to the Domain the output port is used to consume the outside operations. The following payment loading output port is used to retrieve the payments from the outside world:
No matter how this output port is implemented. Its concrete implementation is an infrastructure responsibility that is not part of the business requirement. Whether the payments are loaded from the database or retrieved by an external service call or even from a bare source file the business logic in the Domain part should be kept intact.
The main infrastructure goal is to adapt the outside to the application. It provides the input adapters using the application use cases to wire inputs to the application use cases and the output adapters that are implementing the output ports to consume the external operations.
Below an example of file load adapter that allows us to load payments from a source file:
A second adapter here to adapt the application to external usage call:
In the infrastructure side we can add any required technical dependency such as Spring for example. The Hexagonal architecture used in conjunction with Spring can rely on the inversion of control provided by Spring to inject the dependencies. In the current example we are using Spring Boot. Next, we define the following Spring configuration:
Then, we add the source file containing the payments examples:
Finally, we add the main Spring application:
And we can start the application process:
To better isolate the Domain and enforce the hexagonal architecture design we can rely on Maven multi modules to subdivide the application into multiple modules as the following structure:
The demo-domain should not depend on any extra artifact other than the Lombok like libraries.
The Hexagonal architecture is a good option to adopt to have a change tolerant design that isolates the business logic providing a well-managed structure and a good guidance to achieve flexible and well maintainable application. It is aligned with DDD principles and can be applied in the context of an application developed with microservices. The Hexagonal architecture design is flexible enough to be slightly changed that’s why we observe multiple hexagonal architecture variations in practice. Although this architecture provides a valuable design it is not always compatible with all scenarios such as developing a technical library for example. Moreover, for some cases extra caution should be undertaken to deal with some technical requirement such as supporting transaction operations for example.
Vous pouvez également consulter l’article de Gnaoui sur l’Architecture Hexagonale.