Table Of Contents
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
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)
}