Clean Architecture with Ktor

This section will discuss a potential implementation of the Clean Architecture within Ktor. First, we will have a general look at the Clean Architecture, proposed by Robert C. Martin. Afterward, we will discuss the use case and the module structure.

Finally, we will see the final implementation and which design patterns help to implement this architecture.

Clean Architecture – An Overview

Module Structure and Dependency Flow

Use Case

Clean Architecture module structure in Ktor
Module structure
Module Dependencies in Clean Architecture in Ktor
Module dependencies

Implementation

Domain Layer – Entities

package entities

abstract class Entity(val id: String)

package entities

class Order(id: String) : Entity(id) {
    var name: String = ""
}

Domain Layer – Services

Services

package services

import commands.*
import entities.Order

interface IOrderService {
    suspend fun getAllOrders(command : GetAllOrdersCommand) : List<Order>
    suspend fun getOrderById(command : GetOrderByIdCommand) : Order?
    suspend fun createOrder(command : CreateOrderCommand) : Order
    suspend fun updateOrder(command : UpdateOrderCommand) : Order?
    suspend fun deleteOrder(command : DeleteOrderCommand)
}
package services

import commands.*
import entities.Order
import repositories.interfaces.IOrderRepository


class OrderService(private val orderRepository: IOrderRepository) : IOrderService {


    override suspend fun getAllOrders(command: GetAllOrdersCommand): List<Order> {
        return orderRepository.getAll()
    }

    override suspend fun getOrderById(command: GetOrderByIdCommand): Order? {
        return orderRepository.findById(command.orderId)
    }

    override suspend fun createOrder(command: CreateOrderCommand): Order {
        val order = command.toEntity()
        orderRepository.insert(order)
        return order
    }

    override suspend fun updateOrder(command: UpdateOrderCommand): Order? {
        val order = orderRepository.findById(command.orderId) ?: return null
        command.updateEntity(order)
        orderRepository.replace(order)
        return order
    }

    override suspend fun deleteOrder(command: DeleteOrderCommand) {
        orderRepository.delete(command.orderId)
    }
}

Commands

package commands

data class CreateOrderCommand(val name : String)

data class DeleteOrderCommand (val orderId : String)

class GetAllOrdersCommand

data class GetOrderByIdCommand(val orderId : String)

data class UpdateOrderCommand (val orderId : String, val name : String)

package commands
import entities.Order
import java.util.*

fun CreateOrderCommand.toEntity() : Order {
    val uniqueID = UUID.randomUUID().toString()
    val order = Order(uniqueID)
    order.name = this.name
    return order
}

fun UpdateOrderCommand.updateEntity(order: Order) : Order {
    order.name = this.name
    return order
}

Repositories Interfaces

package repositories.interfaces

interface IEntityRepository<TEntity>{
    suspend fun getAll() : List<TEntity>
    suspend fun findById(id: String) : TEntity?
    suspend fun find(predicate: (TEntity) -> Boolean) : TEntity?
    suspend fun insert(entity : TEntity)
    suspend fun delete(id: String)
    suspend fun replace(item : TEntity)
}

aaa

package repositories.interfaces
import entities.Order

interface IOrderRepository : IEntityRepository<Order>

Application – Web Server

import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import plugins.configureRouting
import plugins.contentNegotiation

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
        .start(wait = true)
}

@Suppress("unused")
fun Application.module() {
    configureRouting()
    contentNegotiation()
}

aaa

package plugins

import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import kotlinx.serialization.json.Json

fun Application.contentNegotiation() {
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            isLenient = true
        })
    }
}

aaa

package plugins

import io.ktor.server.routing.*
import io.ktor.server.application.*
import routing.addOrderRoutes


fun Application.configureRouting() {
    routing {
        addOrderRoutes()
    }
}

Controllers – API Endpoints

package routing

import commands.*
import dtos.extensions.toDto
import dtos.requests.CreateOrderDto
import dtos.requests.UpdateOrderDto
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import repositories.inMemory.OrderRepository
import services.OrderService

// must be handled with dependency injection
var repository = OrderRepository()
var service = OrderService(repository)

fun Route.addOrderRoutes () {

    route("api/v1/orders") {
        get {
            val command = GetAllOrdersCommand()
            val orders = service.getAllOrders(command)
            val ordersDto = orders.map { it.toDto() }
            call.respond(ordersDto)
        }

        get("{id}") {
            val id = call.parameters["id"]
            if(id == null) {
                call.respond(HttpStatusCode.BadRequest)
            }

            val command = GetOrderByIdCommand(id!!)
            val order = service.getOrderById(command)
            if(order == null) {
                call.respond(HttpStatusCode.NotFound)
            }

            val orderDto = order!!.toDto()
            call.respond(orderDto)
        }

        post {
            val dto = call.receive<CreateOrderDto>()
            val command = CreateOrderCommand(dto.name)
            val order = service.createOrder(command)
            val orderDto = order.toDto()

            call.respond(HttpStatusCode.Created, orderDto)
        }

        put("{id}") {
            val id = call.parameters["id"]
            if(id == null) {
                call.respond(HttpStatusCode.BadRequest)
            }

            val dto = call.receive<UpdateOrderDto>()
            val command = UpdateOrderCommand(id!!, dto.name)
            var order = service.updateOrder(command)

            if(order == null) {
                call.respond(HttpStatusCode.NotFound)
            }
            val orderDto = order!!.toDto()

            call.respond(orderDto)
        }

        delete ("{id}") {
            val id = call.parameters["id"]
            if(id == null) {
                call.respond(HttpStatusCode.BadRequest)
            }

            val command = GetOrderByIdCommand(id!!)
            val order = service.getOrderById(command)
            if(order == null) {
                call.respond(HttpStatusCode.NotFound)
            }

            val deleteCommand = DeleteOrderCommand(id)
            service.deleteOrder(deleteCommand)
            call.respond(HttpStatusCode.NoContent)
        }
    }
}

aaa

Repositories – In Memory

package repositories.inMemory

import entities.Entity
import repositories.interfaces.IEntityRepository

open class BaseRepository<TEntity> : IEntityRepository<TEntity> where TEntity : Entity {
    private var entities = mutableListOf<TEntity>()

    override suspend fun getAll(): List<TEntity> {
        return entities
    }

    override suspend fun findById(id: String): TEntity? {
        return entities.find { it.id == id }
    }

    override suspend fun find(predicate: (TEntity) -> Boolean) : TEntity? {
        return entities.find(predicate)
    }

    override suspend fun delete(id: String) {
        var index = entities.indexOfFirst { it.id == id }
        if(index < 0 ) return
        entities.removeAt(index)
    }

    override suspend fun replace(item: TEntity) {
        delete(item.id)
        insert(item)
    }

    override suspend fun insert(entity: TEntity) {
        entities.add(entity)
    }
}

package repositories.inMemory
import entities.Order
import repositories.interfaces.IOrderRepository

class OrderRepository : BaseRepository<Order>() , IOrderRepository

Dtos – Data Transfer Objects

package dtos.responses
import kotlinx.serialization.Serializable


@Serializable
data class OrderDto(
    val id: String,
    val name: String,
)
package dtos.requests
import kotlinx.serialization.Serializable

@Serializable
data class CreateOrderDto(
    val name : String
)


@Serializable
data class UpdateOrderDto(
    val name : String
)
package dtos.extensions
import dtos.responses.OrderDto
import entities.Order

fun Order.toDto() : OrderDto {
    return OrderDto(this.id, this.name)
}

References