Introducción: ¿por qué Kotlin para microservicios?
El desarrollo moderno de backend se aleja cada vez más de los monolitos en favor de la arquitectura de microservicios. Kotlin, al ser un lenguaje de tipado estático en la JVM, ofrece una combinación única de concisión, seguridad y rendimiento. En este artículo, exploraremos técnicas avanzadas para construir microservicios en Kotlin: desde la elección del framework hasta el despliegue en Docker.
Nos centraremos en aspectos prácticos: configuración de endpoints reactivos, inyección de dependencias sin Spring, procesamiento asíncrono con corrutinas y escritura de pruebas. Si ya conoces los fundamentos de Kotlin y quieres pasar al siguiente nivel, este material es para ti.
1. Arquitectura de microservicio con Ktor
Elección del framework: Ktor vs Spring Boot
Para el nivel avanzado elegimos Ktor, un framework asíncrono ligero de JetBrains. A diferencia de Spring Boot, Ktor no utiliza reflexión ni anotaciones al estilo IoC, lo que otorga control total sobre el contexto de la aplicación. Es la elección ideal para microservicios donde importan la velocidad de inicio y el bajo consumo de memoria.
Estructura del proyecto
Estructura recomendada para un microservicio con Ktor:
project/├── src/│ ├── main/kotlin/com/example/│ │ ├── Application.kt // Punto de entrada│ │ ├── routes/ // Rutas (API REST)│ │ ├── services/ // Lógica de negocio│ │ ├── repositories/ // Acceso a datos│ │ ├── models/ // DTOs y entidades│ │ └── di/ // Módulos de inyección de dependencias│ └── test/kotlin/com/example/ // Pruebas└── build.gradle.ktsConfiguración de rutas
Ejemplo de endpoint REST reactivo usando corrutinas:
fun Application.configureRouting() { routing { route("/api/v1/users") { get("/{id}") { val id = call.parameters["id"]?.toIntOrNull() ?: return@get call.respondText("ID inválido", status = HttpStatusCode.BadRequest) val user = userService.getUserById(id) if (user != null) { call.respond(user) } else { call.respondText("Usuario no encontrado", status = HttpStatusCode.NotFound) } } post { val user = call.receive<UserDto>() val created = userService.createUser(user) call.respond(HttpStatusCode.Created, created) } } }}2. Inyección de dependencias con Koin
¿Por qué Koin?
Koin es un DSL para inyección de dependencias, perfectamente compatible con Kotlin. No requiere generación de código ni reflexión, funciona a nivel de constructores y expresiones lambda. Esto permite crear servicios modulares y testeables.
Ejemplo de módulo DI
val appModule = module { single { DatabaseConnection() } single { UserRepository(get()) } factory { UserService(get()) }}
fun Application.configureDI() { startKoin { modules(appModule) }}Observa: usamos factory para los servicios — cada solicitud obtiene una nueva instancia, importante para escenarios reactivos. single se usa para repositorios y conexiones a BD.
3. Asincronía y corrutinas
Reactive Streams y Flow
Para trabajar con flujos de datos en tiempo real, usa kotlinx.coroutines.flow. Es un análogo de Reactive Streams, pero con sintaxis más simple y soporte completo de corrutinas.
Ejemplo de un servicio que procesa datos de una cola:
class OrderProcessor(private val orderRepository: OrderRepository) {
suspend fun processOrders(): Flow<OrderResult> = flow { orderRepository.getPendingOrders().coll