Microservicos representam uma abordagem arquitetural onde uma aplicacao e dividida em servicos pequenos, independentes e especializados, cada um responsavel por uma capacidade de negocio especifica. Kotlin se destaca nesse cenario gracas ao suporte nativo a coroutines para comunicacao assincrona, sintaxe concisa que reduz boilerplate e compatibilidade total com o ecossistema JVM. Neste guia, vamos projetar e implementar uma arquitetura de microservicos com Kotlin, abordando comunicacao entre servicos, resiliencia, observabilidade e estrategias de deploy.

Quando Usar Microservicos

Microservicos nao sao a solucao para todos os problemas. Eles adicionam complexidade operacional significativa e so fazem sentido quando o projeto tem equipes grandes trabalhando em paralelo, necessidade de escalar componentes independentemente ou dominios de negocio claramente separados.

Para projetos pequenos com uma unica equipe, um monolito bem estruturado com Clean Architecture e geralmente mais eficiente. Migre para microservicos quando a complexidade organizacional justificar.

Arquitetura de Referencia

Vamos implementar um sistema de e-commerce com tres microservicos: Catalogo, Pedidos e Notificacoes.

// Estrutura de repositorios
// catalogo-service/     -> Gerencia produtos e categorias
// pedidos-service/      -> Processa pedidos e pagamentos
// notificacoes-service/ -> Envia emails e push notifications
// api-gateway/          -> Ponto de entrada unificado

Servico de Catalogo com Ktor

// catalogo-service/src/main/kotlin/Application.kt
fun main() {
    embeddedServer(Netty, port = 8081) {
        configurarPlugins()
        configurarRoutes()
    }.start(wait = true)
}

fun Application.configurarPlugins() {
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            ignoreUnknownKeys = true
        })
    }
    install(StatusPages) {
        exception<Throwable> { call, causa ->
            call.respond(
                HttpStatusCode.InternalServerError,
                ErroResponse("Erro interno: ${causa.message}")
            )
        }
    }
}

fun Application.configurarRoutes() {
    val service = CatalogoService()

    routing {
        route("/api/produtos") {
            get {
                val produtos = service.listarProdutos()
                call.respond(produtos)
            }

            get("/{id}") {
                val id = call.parameters["id"]?.toLongOrNull()
                    ?: return@get call.respond(
                        HttpStatusCode.BadRequest,
                        ErroResponse("ID invalido")
                    )
                val produto = service.buscarPorId(id)
                    ?: return@get call.respond(
                        HttpStatusCode.NotFound,
                        ErroResponse("Produto nao encontrado")
                    )
                call.respond(produto)
            }

            get("/{id}/disponibilidade") {
                val id = call.parameters["id"]?.toLongOrNull()
                    ?: return@get call.respond(
                        HttpStatusCode.BadRequest,
                        ErroResponse("ID invalido")
                    )
                val disponibilidade = service.verificarDisponibilidade(id)
                call.respond(disponibilidade)
            }
        }

        get("/health") {
            call.respond(mapOf("status" to "UP", "servico" to "catalogo"))
        }
    }
}

Servico de Pedidos com Spring Boot

// pedidos-service
@SpringBootApplication
class PedidosApplication

fun main(args: Array<String>) {
    runApplication<PedidosApplication>(*args)
}

@RestController
@RequestMapping("/api/pedidos")
class PedidoController(
    private val pedidoService: PedidoService
) {
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    suspend fun criarPedido(
        @Valid @RequestBody request: CriarPedidoRequest
    ): PedidoResponse {
        return pedidoService.criarPedido(request)
    }

    @GetMapping("/{id}")
    suspend fun buscarPedido(@PathVariable id: Long): PedidoResponse {
        return pedidoService.buscarPorId(id)
    }

    @GetMapping("/cliente/{clienteId}")
    suspend fun listarPorCliente(
        @PathVariable clienteId: Long
    ): List<PedidoResponse> {
        return pedidoService.listarPorCliente(clienteId)
    }
}

@Service
class PedidoService(
    private val catalogoClient: CatalogoClient,
    private val notificacaoClient: NotificacaoClient,
    private val pedidoRepository: PedidoRepository
) {
    suspend fun criarPedido(request: CriarPedidoRequest): PedidoResponse {
        // Verificar disponibilidade no servico de catalogo
        for (item in request.itens) {
            val disponibilidade = catalogoClient.verificarDisponibilidade(
                item.produtoId
            )
            if (disponibilidade.quantidade < item.quantidade) {
                throw NegocioException(
                    "Produto ${item.produtoId} sem estoque suficiente"
                )
            }
        }

        // Buscar detalhes dos produtos
        val itensCompletos = request.itens.map { item ->
            val produto = catalogoClient.buscarProduto(item.produtoId)
            ItemPedido(
                produtoId = produto.id,
                nomeProduto = produto.nome,
                quantidade = item.quantidade,
                precoUnitario = produto.preco
            )
        }

        // Salvar pedido
        val pedido = Pedido(
            clienteId = request.clienteId,
            itens = itensCompletos,
            status = StatusPedido.PENDENTE
        )
        val pedidoSalvo = pedidoRepository.save(pedido)

        // Notificar de forma assincrona
        coroutineScope {
            launch {
                notificacaoClient.enviarNotificacao(
                    NotificacaoRequest(
                        destinatario = request.emailCliente,
                        tipo = "PEDIDO_CRIADO",
                        dados = mapOf("pedidoId" to pedidoSalvo.id.toString())
                    )
                )
            }
        }

        return pedidoSalvo.toResponse()
    }
}

Comunicacao entre Servicos

Cliente HTTP com Resiliencia

@Component
class CatalogoClient(
    private val httpClient: HttpClient
) {
    private val baseUrl = System.getenv("CATALOGO_SERVICE_URL")
        ?: "http://catalogo-service:8081"

    suspend fun buscarProduto(id: Long): ProdutoDto {
        return comRetry(tentativas = 3) {
            httpClient.get("$baseUrl/api/produtos/$id").body()
        }
    }

    suspend fun verificarDisponibilidade(produtoId: Long): DisponibilidadeDto {
        return comRetry(tentativas = 3) {
            httpClient.get(
                "$baseUrl/api/produtos/$produtoId/disponibilidade"
            ).body()
        }
    }
}

// Funcao de retry generica
suspend fun <T> comRetry(
    tentativas: Int = 3,
    delayInicial: Long = 500L,
    fatorMultiplicacao: Double = 2.0,
    bloco: suspend () -> T
): T {
    var delayAtual = delayInicial
    var ultimaExcecao: Exception? = null

    repeat(tentativas) { tentativa ->
        try {
            return bloco()
        } catch (e: Exception) {
            ultimaExcecao = e
            if (tentativa < tentativas - 1) {
                delay(delayAtual)
                delayAtual = (delayAtual * fatorMultiplicacao).toLong()
            }
        }
    }
    throw ultimaExcecao!!
}

Comunicacao Assincrona com Mensageria

Para operacoes que nao precisam de resposta imediata, use mensageria:

// Produtor de mensagens (Pedidos Service)
@Component
class PedidoEventPublisher(
    private val rabbitTemplate: RabbitTemplate
) {
    fun publicarPedidoCriado(pedido: Pedido) {
        val evento = PedidoCriadoEvento(
            pedidoId = pedido.id,
            clienteId = pedido.clienteId,
            valorTotal = pedido.valorTotal,
            timestamp = Instant.now()
        )
        rabbitTemplate.convertAndSend(
            "pedidos-exchange",
            "pedido.criado",
            evento
        )
    }
}

// Consumidor de mensagens (Notificacoes Service)
@Component
class PedidoEventConsumer(
    private val notificacaoService: NotificacaoService
) {
    @RabbitListener(queues = ["notificacoes-queue"])
    fun processarPedidoCriado(evento: PedidoCriadoEvento) {
        notificacaoService.enviarConfirmacaoPedido(
            clienteId = evento.clienteId,
            pedidoId = evento.pedidoId,
            valor = evento.valorTotal
        )
    }
}

Circuit Breaker

O padrao Circuit Breaker previne falhas em cascata quando um servico esta indisponivel:

class CircuitBreaker(
    private val nome: String,
    private val limiteAberto: Int = 5,
    private val tempoRecuperacao: Long = 30_000L
) {
    private var falhas = AtomicInteger(0)
    private var estado = AtomicReference(Estado.FECHADO)
    private var ultimaFalha = AtomicLong(0)

    enum class Estado { FECHADO, ABERTO, MEIO_ABERTO }

    suspend fun <T> executar(bloco: suspend () -> T): T {
        when (estado.get()) {
            Estado.ABERTO -> {
                if (System.currentTimeMillis() - ultimaFalha.get() > tempoRecuperacao) {
                    estado.set(Estado.MEIO_ABERTO)
                } else {
                    throw CircuitBreakerAberto("Circuit breaker $nome esta aberto")
                }
            }
            else -> { /* continuar */ }
        }

        return try {
            val resultado = bloco()
            onSucesso()
            resultado
        } catch (e: Exception) {
            onFalha()
            throw e
        }
    }

    private fun onSucesso() {
        falhas.set(0)
        estado.set(Estado.FECHADO)
    }

    private fun onFalha() {
        val totalFalhas = falhas.incrementAndGet()
        ultimaFalha.set(System.currentTimeMillis())
        if (totalFalhas >= limiteAberto) {
            estado.set(Estado.ABERTO)
        }
    }
}

Observabilidade

Microservicos exigem monitoramento robusto. Implemente logging estruturado, metricas e tracing:

// Logging estruturado com contexto
fun Application.configurarLogging() {
    install(CallLogging) {
        level = Level.INFO
        format { call ->
            val status = call.response.status()
            val metodo = call.request.httpMethod.value
            val uri = call.request.uri
            val duracao = call.processingTimeMillis()
            "$metodo $uri -> $status (${duracao}ms)"
        }
    }
}

// Endpoint de metricas
fun Route.metricasRoutes(metricas: MetricasService) {
    get("/metrics") {
        call.respond(mapOf(
            "requisicoes_total" to metricas.totalRequisicoes(),
            "requisicoes_por_segundo" to metricas.requisicoesPorSegundo(),
            "latencia_media_ms" to metricas.latenciaMedia(),
            "erros_total" to metricas.totalErros(),
            "uptime_segundos" to metricas.uptime()
        ))
    }
}

Boas Praticas para Microservicos com Kotlin

  • Um banco por servico: cada microservico deve ter seu proprio banco de dados. Nunca compartilhe bancos entre servicos.
  • Contratos bem definidos: use DTOs versionados para comunicacao. Nunca exponha entidades internas.
  • Comunicacao assincrona quando possivel: use mensageria para operacoes que nao exigem resposta imediata.
  • Resiliencia e obrigatoria: implemente retry, circuit breaker e timeout em toda comunicacao entre servicos.
  • Health checks em todos os servicos: permita que orquestradores detectem e reiniciem servicos com problemas.
  • Centralize logs e metricas: use ELK Stack, Grafana ou similar para visualizar o comportamento de todos os servicos.
  • Idempotencia: endpoints de criacao e atualizacao devem ser idempotentes para lidar com retries seguros.

Erros Comuns e Armadilhas

  • Microservicos prematuros: dividir uma aplicacao em microservicos antes de entender os limites do dominio resulta em servicos mal definidos que precisam de refatoracao constante.
  • Acoplamento temporal: se o servico A precisa esperar sincrona-mente pelo servico B, que espera pelo C, voce criou um monolito distribuido. Prefira comunicacao assincrona.
  • Transacoes distribuidas: ACID transactions entre microservicos sao extremamente complexas. Use padroes como Saga para consistencia eventual.
  • Ignorar falhas de rede: a rede falha. Sempre. Implemente timeout, retry e fallback em toda comunicacao entre servicos.
  • Falta de observabilidade: sem logs centralizados e tracing distribuido, debugar problemas em producao se torna quase impossivel.
  • Over-engineering: nao crie um microservico para cada entidade. Agrupe por capacidade de negocio.

Conclusao e Proximos Passos

Microservicos com Kotlin oferecem uma arquitetura poderosa para sistemas que precisam escalar e evoluir independentemente. A combinacao de coroutines para comunicacao assincrona, frameworks leves como Ktor e o ecossistema robusto do Spring Boot cria uma base solida para sistemas distribuidos. Para aprofundar, explore nossos guias sobre Docker para containerizacao, CI/CD para deploy automatizado e REST APIs para design de contratos entre servicos. Lembre-se: comece simples e evolua a arquitetura conforme a necessidade real do projeto.