Introduction: Why Kotlin for Microservices?
Modern backend development is increasingly moving away from monoliths in favor of microservice architecture. Kotlin, as a statically typed language on the JVM, offers a unique combination of conciseness, safety, and performance. In this article, we will explore advanced techniques for building microservices in Kotlin: from framework selection to deployment in Docker.
We will focus on practical aspects: setting up reactive endpoints, dependency injection without Spring, asynchronous processing with coroutines, and writing tests. If you are already familiar with the basics of Kotlin and want to take it to the next level, this material is for you.
1. Microservice Architecture with Ktor
Framework Selection: Ktor vs Spring Boot
For an advanced level, we choose Ktor — a lightweight asynchronous framework from JetBrains. Unlike Spring Boot, Ktor does not use reflection or IoC-style annotations, giving you full control over the application context. This is an ideal choice for microservices where startup speed and low memory consumption are important.
Project Structure
Recommended structure for a microservice with Ktor:
project/├── src/│ ├── main/kotlin/com/example/│ │ ├── Application.kt // Entry point│ │ ├── routes/ // Routes (REST API)│ │ ├── services/ // Business logic│ │ ├── repositories/ // Data access│ │ ├── models/ // DTOs and entities│ │ └── di/ // Dependency injection modules│ └── test/kotlin/com/example/ // Tests└── build.gradle.ktsRoute Configuration
Example of a reactive REST endpoint using coroutines:
fun Application.configureRouting() { routing { route("/api/v1/users") { get("/{id}") { val id = call.parameters["id"]?.toIntOrNull() ?: return@get call.respondText("Invalid ID", status = HttpStatusCode.BadRequest) val user = userService.getUserById(id) if (user != null) { call.respond(user) } else { call.respondText("User not found", status = HttpStatusCode.NotFound) } } post { val user = call.receive<UserDto>() val created = userService.createUser(user) call.respond(HttpStatusCode.Created, created) } } }}2. Dependency Injection with Koin
Why Koin?
Koin is a DSL for dependency injection, perfectly compatible with Kotlin. It does not require code generation or reflection, working at the level of constructors and lambda expressions. This allows you to create modular and testable services.
DI Module Example
val appModule = module { single { DatabaseConnection() } single { UserRepository(get()) } factory { UserService(get()) }}
fun Application.configureDI() { startKoin { modules(appModule) }}Note: we use factory for services — each request gets a new instance, which is important for reactive scenarios. single is used for repositories and database connections.
3. Asynchrony and Coroutines
Reactive Streams and Flow
For working with real-time data streams, use kotlinx.coroutines.flow. It is an analog of Reactive Streams, but with simpler syntax and full coroutine support.
Example of a service that processes data from a queue:
class OrderProcessor(private val orderRepository: OrderRepository) {
suspend fun processOrders(): Flow<OrderResult> = flow { orderRepository.getPendingOrders().coll