In the past few years, many blog posts and articles have been written that present the Clean Architecture, as it has been presented by Robert C. Martin (Uncle Bob) in his blog post and (in more details) in his fantastic book “Clean Architecture: A Craftsman’s Guide to Software Structure and Design.”
In this post, we present an example of a REST service that uses Clean Architecture and is written in Kotlin.
The source code can be found in this repo:
The project consists of 4 modules
This module contains the domain entities. There are no dependencies to frameworks and/or libraries.
This module contains the business rules that are essential for our application. The only dependency of this module is to
core. In this module, gateways for the repositories are being defined. Each use case defines the interface of the gateway that is required following the ISP. These gateways, operate on the domain entities defined in
UseCase is an interface similar to the
java.util.Function. It just gets a request and transforms it into a response.
UseCaseExecutor handles the execution of a
UseCase. To do so, it has an
invoke method that takes the following arguments:
UseCasethat will be executed
Requestobject (the input of the use case)
Responseobject (the output of the use case) of the
UseCaseexecution to a
There are three more overloaded versions of the
invoke method, which omit the input and/or the output of the
UseCaseExecutor implementation (
UseCaseExecutorImp) is using
java.util.concurrent.CompletionStage for the execution abstraction. These abstractions are convenient as they can perform asynchronous executions and also have out of the box compatibility with most frameworks.
This module contains the implementation of the gateways defined in the
usecases module. This module depends on the framework that facilitates the data access. In our example, we use JPA and Spring Data. The
Jpa*Repository classes are the actual implementation of the gateways defined in the
These repositories, make use of the Spring Data
JpaRepository. Here is an example
DBProductRepository is a subclass of the a Spring Data
The entities in this module, are JPA entities, so mapper functions are required to make the translation between these entities and domain entities. In the previous snippet, we demonstrated how these mapper functions are used in the
JpaProductRepository. An example of an entity is
This module contains all the details of the delivery mechanism that we use along with the wiring of the app and the configurations. In our example, we use rest services built with Spring Boot. Similarly, to the JPA entities of the
dataproviders module, the DTOs have mappers to convert from and to the domain entities.
A rest controller gets the
RequestDto and forwards it to the related use case through the
UseCaseExecutor. The response of the use case (which is a
ResponseDto) is the response of the controller's method that implements the endpoint. An example of such usage is
In this pull request, we present how easy is to change the data layer without having to touch the business logic (
usecasesmodules). The flexibility is provided by the clean boundaries that we have between each layer. Also, due to the dependency rule that outer layers depend on the in inner layers, the
usecasesmodules are unaware of everything that we have changed in the outer modules. Hence, we don't even have to recompile these modules to deploy the application with the new data layer.
Moreover, it is much simpler for someone who joins the project to understand the domain of the application since the
usecases modules contain only the business objects and the way they interact. Someone might say, that it is possible for a new team member to start writing new functionality without getting familiar enough first with the frameworks and/or the libraries that are used (of course there are some exceptions). The core of our application doesn't contain any magic framework that might make the learning curve steeper.
Due to the separation discussed in the previous section, it is straightforward to test the business logic of the application without having to setup any framework or library. We can write simple unit tests to test the complex business logic in our application without having to deal with the frameworks. Also, we don’t have to change these tests when we change something in a framework. Of course, we should have integration tests that ensure that everything is wired up correctly, but these costly tests will only be a few and will not increase when the complexity in our business logic is increased.
It is easy to notice that we have written way too much for the simple functionality that out application offers, even in a concise language like Kotlin. Clean Architecture requires some abstractions that might be considered as over-engineering if the application we are building is relatively simple. It might not feel natural to indirect the invocations to the data and the delivery layer in the beginning, but as we add new functionality to our system, our velocity is increased, since we separate pure domain code with frameworks and configurations.
Clean Architecture, like every software architecture, comes with a cost, so it is up to the developers/architects to decide if they can benefit from it or not.