Microsserviços representam uma abordagem arquitetural onde uma aplicação e dividida em serviços pequenos, independentes e especializados, cada um responsavel por uma capacidade de negócio específica. Kotlin se destaca nesse cenário gracas ao suporte nativo a coroutines para comunicação assíncrona, sintaxe concisa que reduz boilerplate e compatibilidade total com o ecossistema JVM. Neste guia, vamos projetar e implementar uma arquitetura de microsserviços com Kotlin, abordando comunicação entre serviços, resiliência, observabilidade e estrategias de deploy.
Quando Usar Microsserviços
Microsserviços não são a solução para todos os problemas. Eles adicionam complexidade operacional significativa e só fazem sentido quando o projeto tem equipes grandes trabalhando em paralelo, necessidade de escalar componentes independentemente ou dominios de negócio claramente separados.
Para projetos pequenos com uma única equipe, um monolito bem estruturado com Clean Architecture e geralmente mais eficiente. Migre para microsserviços quando a complexidade organizacional justificar.
Arquitetura de Referência
Vamos implementar um sistema de e-commerce com tres microsserviços: Catálogo, Pedidos e Notificacoes.
// Estrutura de repositórios
// catálogo-service/ -> Gerencia produtos e categorias
// pedidos-service/ -> Processa pedidos e pagamentos
// notificacoes-service/ -> Envia emails e push notifications
// api-gateway/ -> Ponto de entrada unificado
Serviço de Catálogo com Ktor
// catálogo-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", "serviço" to "catálogo"))
}
}
}
Serviço 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 serviço de catálogo
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 assíncrona
coroutineScope {
launch {
notificacaoClient.enviarNotificacao(
NotificacaoRequest(
destinatario = request.emailCliente,
tipo = "PEDIDO_CRIADO",
dados = mapOf("pedidoId" to pedidoSalvo.id.toString())
)
)
}
}
return pedidoSalvo.toResponse()
}
}
Comunicação entre Serviços
Cliente HTTP com Resiliência
@Component
class CatalogoClient(
private val httpClient: HttpClient
) {
private val baseUrl = System.getenv("CATALOGO_SERVICE_URL")
?: "http://catálogo-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()
}
}
}
// Função 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!!
}
Comunicação Assíncrona com Mensageria
Para operações que não 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 padrão Circuit Breaker previne falhas em cascata quando um serviço 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
Microsserviços exigem monitoramento robusto. Implemente logging estruturado, métricas 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 métricas
fun Route.metricasRoutes(métricas: MetricasService) {
get("/metrics") {
call.respond(mapOf(
"requisicoes_total" to métricas.totalRequisicoes(),
"requisicoes_por_segundo" to métricas.requisicoesPorSegundo(),
"latencia_media_ms" to métricas.latenciaMedia(),
"erros_total" to métricas.totalErros(),
"uptime_segundos" to métricas.uptime()
))
}
}
Boas Práticas para Microsserviços com Kotlin
- Um banco por serviço: cada microservico deve ter seu próprio banco de dados. Nunca compartilhe bancos entre serviços.
- Contratos bem definidos: use DTOs versionados para comunicação. Nunca exponha entidades internas.
- Comunicação assíncrona quando possível: use mensageria para operações que não exigem resposta imediata.
- Resiliência e obrigatória: implemente retry, circuit breaker e timeout em toda comunicação entre serviços.
- Health checks em todos os serviços: permita que orquestradores detectem e reiniciem serviços com problemas.
- Centralize logs e métricas: use ELK Stack, Grafana ou similar para visualizar o comportamento de todos os serviços.
- Idempotencia: endpoints de criação e atualização devem ser idempotentes para lidar com retries seguros.
Erros Comuns e Armadilhas
- Microsserviços prematuros: dividir uma aplicação em microsserviços antes de entender os limites do dominio resulta em serviços mal definidos que precisam de refatoração constante.
- Acoplamento temporal: se o serviço A precisa esperar sincrona-mente pelo serviço B, que espera pelo C, você criou um monolito distribuído. Prefira comunicação assíncrona.
- Transacoes distribuidas: ACID transactions entre microsserviços são extremamente complexas. Use padrões como Saga para consistencia eventual.
- Ignorar falhas de rede: a rede falha. Sempre. Implemente timeout, retry e fallback em toda comunicação entre serviços.
- Falta de observabilidade: sem logs centralizados e tracing distribuído, debugar problemas em producao se torna quase impossivel.
- Over-engineering: não crie um microservico para cada entidade. Agrupe por capacidade de negócio.
Conclusão e Próximos Passos
Microsserviços com Kotlin oferecem uma arquitetura poderosa para sistemas que precisam escalar e evoluir independentemente. A combinacao de coroutines para comunicação assíncrona, frameworks leves como Ktor e o ecossistema robusto do Spring Boot cria uma base solida para sistemas distribuidos. Go é outra linguagem excelente para microsserviços, com binários estáticos e startup instantâneo, enquanto Rust oferece performance máxima com consumo mínimo de recursos. Para aprofundar, explore nossos guias sobre Docker para containerizacao, CI/CD para deploy automatizado e REST APIs para design de contratos entre serviços. Lembre-se: comece simples e evolua a arquitetura conforme a necessidade real do projeto.