The Clean Architecture Approach to backends using Kotlin

Image result for clean architecture

This doubles as a review/guide of the starter project I currently use for developing back end integrations and can be found here: https://github.com/gilokimu/Backend-Starter-Project

Description of the architecture

The project consists of 4 modules 

  1. core
  2. usecases
  3. dataproviders
  4. delivery.

Core module

This module contains the domain entities. There are no dependencies to frameworks and/or libraries.

data class Product(
    val code: ProductCode,
    val description: String,
    val price: BigDecimal,
    val createdAt: LocalDateTime?
)

data class ProductCode(val value: String)

data class ProductData(val code: ProductCode, val product: Product)

Product.kt : The product entity class

Usecases module

This module contains the business rules that are essential for our application (Application business rules). 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 and UseCaseExecutor are also defined. The UseCase is an interface similar to thejava.util.Function. It just gets a request and returns a response.

interface UseCase<in Request, out Response> {
    fun execute(request: Request): Response
}

The use case class gets a request and returns a response. Request and Response are generic types

The UseCaseExecutor handles the execution of a UseCase. To do so, it has an invoke method that takes the following arguments:

  1. the use case that is to be executed
  2. the RequestDto
  3. a function that converts the RequestDto to a Request object (the input of the use case)
  4. a function that converts the Response object (the output of the use case) of the use case execution to a ResponseDto

There are 3 more overloaded versions of the invoke method, which omit the input and/or the output of the UseCaseExecutor.

interface UseCaseExecutor {
    operator fun  invoke(
        useCase: UseCase,
        requestDto: RequestDto,
        requestConverter: (RequestDto) -> Request,
        responseConverter: (Response) -> ResponseDto
    ): CompletionStage

    operator fun  invoke(
        useCase: UseCase,
        requestDto: RequestDto,
        requestConverter: (RequestDto) -> Request
    ) =
        invoke(useCase, requestDto, requestConverter, {})

    operator fun invoke(useCase: UseCase) =
        invoke(useCase, Unit, { })

    operator fun  invoke(
        useCase: UseCase,
        responseConverter: (Response) -> ResponseDto
    ) =
        invoke(useCase, Unit, { }, responseConverter)
}

The UseCaseExecutor with the 4 overloaded invoke functions based on whether there is a response or request

Currently, the UseCaseExecutor implementation (UseCaseExecutorImp) 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.

class UseCaseExecutorImp : UseCaseExecutor {

    override operator fun  invoke(
        useCase: UseCase,
        requestDto: RequestDto,
        requestConverter: (RequestDto) -> Request,
        responseConverter: (Response) -> ResponseDto
    ): CompletionStage =
        CompletableFuture
            .supplyAsync { requestConverter(requestDto) }
            .thenApplyAsync { useCase.execute(it) }
            .thenApplyAsync { responseConverter(it) }
}

The UseCaseExecutor implementation

class CreateProductUseCase(
        private val productRepository: ProductRepository
    ) : UseCase<Product, Unit> {

    override fun execute(product: Product) {
        if (product.price < BigDecimal.ZERO)
            throw ValidationException("Product price should not be negative")
        productRepository.save(product.copy(createdAt = LocalDateTime.now()))
    }

    interface ProductRepository {
        fun existsProductCode(productCode: ProductCode): Boolean
        fun save(product: Product)
    }
}

An example for the create product use case, implements use case and will be invoked by the use case executor above

Dataproviders module

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 JpaRepository classes are the actual implementation of the gateways defined in the usecases module.

These repositories, use the Spring Data JpaRepository as dependencies. For more, check MongoProductRepository.kt. The entities in this module, are JPA entities, so mappings to and from these and the domain entities are needed. CheckProductEntity.kt for more.

@Entity
@Table(name = "products")
data class ProductEntity(
    @Id
    val code: String,
    val description: String,
    val price: BigDecimal,
    val createdAt: Long
)

// Mappers
fun ProductEntity.toProduct() =
    Product(
        code = ProductCode(this.code),
        description = this.description,
        price = this.price,
        createdAt = LocalDateTime.ofInstant(Instant.ofEpochMilli(this.createdAt), ZoneId.systemDefault())
    )

fun Product.toProductEntity() =
    ProductEntity(
        code = this.code.value,
        description = this.description,
        price = this.price,
        createdAt = this.createdAt!!.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
    )

ProductEntity.kt – Entity object that interacts with JPA and defines the mapping across our core entity and data providers entities

open class JpaProductRepository(private val dbProductRepository: DBProductRepository) :
    ProductRepository {

    @Transactional
    override fun update(code: ProductCode, product: Product): Product? {
        dbProductRepository.save(product.toProductEntity())
        return getByProductCode(code)
    }

    override fun delete(code: ProductCode) {
        dbProductRepository.delete(getByProductCode(code)!!.toProductEntity())
    }

    override fun existsProductCode(productCode: ProductCode) = dbProductRepository.existsById(productCode.value)

    override fun getByProductCode(productCode: ProductCode) =
        dbProductRepository.findById(productCode.value).unwrap(ProductEntity::toProduct)

    override fun getProducts() = dbProductRepository.findAll().map { productEntity -> productEntity.toProduct()}

    @Transactional
    override fun save(product: Product) {
        dbProductRepository.save(product.toProductEntity())
    }

}

JPA Product Repository class

fun <T> Optional<T>.unwrap(): T? = orElse(null)

fun <T, E> Optional<T>.unwrap(conv: (T) -> (E)): E? = unwrap()?.let { conv(it) }

fun <T, E> MutableList<T>.unwrap(conv: (MutableList<T>) -> (E)): E? = let { conv(it) }

Helpers.kt : Extension functions for mapping across entities

Delivery module

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 thedataproviders module, the DTOs have mappers, to convert from and to the domain entities.

data class ProductDto(
        var code: String,
        val description: String,
        val price: String,
        val createdAt: String? = null
)

// Mappers
fun Product.toProductDto() =
    ProductDto(
        code = this.code.value,
        description = this.description,
        price = this.price.toString(),
        createdAt = this.createdAt?.format(DateTimeFormatter.ISO_DATE_TIME)
    )

fun ProductDto.toProduct() =
    Product(
        code = ProductCode(this.code),
        description = this.description,
        price = BigDecimal(this.price),
        createdAt = if (this.createdAt == null) null else LocalDateTime.from(DateTimeFormatter.ISO_DATE_TIME.parse(this.createdAt))
    )

ProductDto.kt : The product data class that will serve content to the view, included as well is the mapping similar to the data provider’s entity above

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. The exceptions are handled by GlobalExceptionHandler.kt, and they are converted to ErrorDto.

@RestController
@RequestMapping("/products")
interface ProductsResource {
    @GetMapping("/{code}")
    fun getProductByCode(@PathVariable("code") code: String): CompletionStage<ProductDto>

    @GetMapping("/")
    fun getProducts(): CompletionStage<List<ProductDto>>

    @PostMapping("/create")
    fun createProduct(@Valid @RequestBody productDto: ProductDto): CompletionStage<ResponseEntity<Unit>>

    @PutMapping("/{code}")
    fun updateProduct(
            @PathVariable("code") code: String,
            @Valid @RequestBody productDto: ProductDto): CompletionStage<ProductDto>

    @DeleteMapping("/{code}")
    fun deleteProduct(@PathVariable("code") code: String): CompletionStage<ResponseEntity<Unit>>
}

Spring boot rest controller class

class ProductResourceImp(
    private val useCaseExecutor: UseCaseExecutor,
    private val getProductByIdUseCase: GetProductByIdUseCase,
    private val createProductUseCase: CreateProductUseCase,
    private val getProductsUseCase: GetProductsUseCase,
    private val updateProductUseCase: UpdateProductUseCase,
    private val deleteProductUseCase: DeleteProductUseCase
) : ProductsResource {
    override fun updateProduct(code: String, productDto: ProductDto): CompletionStage<ProductDto> {
        productDto.code = code
        return useCaseExecutor(
                useCase = updateProductUseCase,
                requestDto = productDto,
                requestConverter = { it.toProduct() },
                responseConverter = { it.toProductDto() }
        )

    }

    override fun deleteProduct(@PathVariable("code") code: String): CompletionStage<ResponseEntity<Unit>>  =
            useCaseExecutor(
                    useCase = deleteProductUseCase,
                    requestDto = code,
                    requestConverter = {ProductCode(it)},
                    responseConverter = { ResponseEntity<Unit>(HttpStatus.OK) })

    override fun getProductByCode(@PathVariable("code") code: String) =
        useCaseExecutor(
            useCase = getProductByIdUseCase,
            requestDto = code,
            requestConverter = { ProductCode(it) },
            responseConverter = { it.toProductDto() })

    override fun createProduct(@RequestBody productDto: ProductDto) =
        useCaseExecutor(
            useCase = createProductUseCase,
            requestDto = productDto,
            requestConverter = { it.toProduct() },
            responseConverter = { ResponseEntity<Unit>(HttpStatus.CREATED) })

    override fun getProducts() =
            useCaseExecutor(
                    useCase = getProductsUseCase,
                    requestDto = null,
                    requestConverter = {  },
                    responseConverter = { productList -> productList.map { product -> product.toProductDto() } })

}

Implementation of the view class : usecase is injected into this class. This class receives a request dto from the rest controller, uses the use case executor to convert the request and invoke the use case to get a response

@ControllerAdvice
@RestController
class GlobalExceptionHandler : ResponseEntityExceptionHandler() {
    @ExceptionHandler(NotFoundException::class)
    fun notFound(ex: NotFoundException) =
        ResponseEntity(ErrorDto(ErrorCodeDto.NOT_FOUND, "Resource not found"), HttpStatus.NOT_FOUND)

    @ExceptionHandler(ValidationException::class)
    fun alreadyExists(ex: ValidationException) =
        ResponseEntity(ErrorDto(ErrorCodeDto.VALIDATION_ERROR, ex.message), HttpStatus.BAD_REQUEST)
}

The rest controller exception handling class

data class ErrorDto(
    val errorCode: ErrorCodeDto?,
    val message: String?
)

enum class ErrorCodeDto {
    NOT_FOUND,
    VALIDATION_ERROR
}

ErrorDto.kt : the error data class

@Configuration
class Module {
    @Bean
    fun productsResourceImp(
            useCaseExecutor: UseCaseExecutor,
            getProductByIdUseCase: GetProductByIdUseCase,
            createProductUseCase: CreateProductUseCase,
            getProductsUseCase: GetProductsUseCase,
            updateProductUseCase: UpdateProductUseCase,
            deleteProductUseCase: DeleteProductUseCase
    ) = ProductResourceImp(
            useCaseExecutor,
            getProductByIdUseCase,
            createProductUseCase,
            getProductsUseCase,
            updateProductUseCase,
            deleteProductUseCase
    )

    @Bean
    fun useCaseExecutor() = UseCaseExecutorImp()

    @Bean
    fun getProductByIdUseCase(productRepository: ProductRepository) = GetProductByIdUseCase(productRepository)

    @Bean
    fun createProductUseCase(productRepository: ProductRepository) = CreateProductUseCase(productRepository)

    @Bean
    fun updateProductsUseCase(productRepository: ProductRepository) = UpdateProductUseCase(productRepository)

    @Bean
    fun deleteProductUseCase(productRepository: ProductRepository) = DeleteProductUseCase(productRepository)

    @Bean
    fun getProductsUseCase(productRepository: ProductRepository) = GetProductsUseCase(productRepository)

    @Bean
    fun productRepository(dbProductRepository: DBProductRepository) = JpaProductRepository(dbProductRepository)
}

Module.kt : which puts everything together using Spring boots DI framework

Recent Posts