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:
thecodegang/clean-architecture-example_clean-architecture-example - A simple clean architecture example in Kotlin and Spring Boot 2.0_github.com
The project consists of 4 modules core
, usecases
, dataproviders
, and delivery
.
core
moduleThis module contains the domain entities. There are no dependencies to frameworks and/or libraries.
usecases
moduleThis 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 core
.
In this module, [UseCase](https://github.com/thecodegang/clean-architecture-example/blob/master/usecases/src/main/kotlin/com/github/aantoniadis/delivery/usecases/core/UseCase.kt)
and [UseCaseExecutor](https://github.com/thecodegang/clean-architecture-example/blob/master/usecases/src/main/kotlin/com/github/aantoniadis/delivery/usecases/core/UseCase.kt)
are also defined.
The UseCase
is an interface similar to the java.util.Function
. It just gets a request and transforms it into a response.
The UseCaseExecutor
handles the execution of a UseCase
. To do so, it has an invoke
method that takes the following arguments:
UseCase
that will be executedRequestDto
RequestDto
to a Request
object (the input of the use case)Response
object (the output of the use case) of the UseCase
execution to a ResponseDto
There are three more overloaded versions of the invoke
method, which omit the input and/or the output of the UseCaseExecutor
.
Currently, the UseCaseExecutor
implementation ([UseCaseExecutorImp](https://github.com/thecodegang/clean-architecture-example/blob/master/usecases/src/main/kotlin/com/github/aantoniadis/delivery/usecases/core/UseCase.kt)
) is using java.util.concurrent.CompletableFuture
and 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.
dataproviders
moduleThis 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 usecases
module.
These repositories, make use of the Spring Data JpaRepository
. Here is an example [JpaProductRepository.kt](https://github.com/thecodegang/clean-architecture-example/blob/master/dataproviders/src/main/kotlin/com/github/aantoniadis/dataproviders/db/jpa/repositories/JpaProductRepository.kt)
:
DBProductRepository
is a subclass of the a Spring Data JpaRepository
.
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[ProductEntity.kt](https://github.com/thecodegang/clean-architecture-example/blob/master/dataproviders/src/main/kotlin/com/github/aantoniadis/dataproviders/db/jpa/entities/ProductEntity.kt)
:
delivery
moduleThis 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[ProductResourceImp.kt](https://github.com/thecodegang/clean-architecture-example/blob/master/delivery/src/main/kotlin/com/github/aantoniadis/delivery/rest/imp/ProductResourceImp.kt)
.
The exceptions are handled by GlobalExceptionHandler.kt, and they are converted to [ErrorDto](https://github.com/thecodegang/clean-architecture-example/blob/master/delivery/src/main/kotlin/com/github/aantoniadis/delivery/rest/api/ErrorDto.kt)
.
In this pull request, we present how easy is to change the data layer without having to touch the business logic (core
and usecases
modules). 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 core
and usecases
modules 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 core
andusecases
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.