DEV Community

Lucas Costa
Lucas Costa

Posted on

Do Zero à Produção: EDA, Clean Arch e Observabilidade com Go e Kotlin

"Ler sobre arquitetura é fácil. Tomar decisões reais de produção é onde o jogo acontece."

Neste projeto, decidi construir um pipeline de preços que simula um ambiente de alta disponibilidade. O objetivo não era apenas fazer o código funcionar, mas garantir que ele fosse resiliente, escalável e, acima de tudo, visível.


🛠️ A Arquitetura do Sistema

O fluxo de dados foi desenhado para ser totalmente desacoplado através de EDA (Event-Driven Architecture):

graph LR
    A[POST /prices] --> B(price-crawler Go)
    B --> C{Kafka}
    C --> D(price-processor Kotlin)
    D --> E[(PostgreSQL)]
Enter fullscreen mode Exit fullscreen mode
  • price-crawler (Go): Responsável por receber o dado via HTTP, persistir localmente e garantir o envio ao Kafka.
  • price-processor (Kotlin): Consome os eventos, aplica validações de negócio e persiste no banco de dados final.

🏗️ Clean Architecture: O Domínio como "Coração"

No Kotlin (price-processor), utilizei uma abordagem de módulos Gradle para garantir o isolamento total:

  • Módulo Domain: Contém apenas POJOs e lógica pura, sem dependências de frameworks como Spring ou bibliotecas de mensageria.
  • Módulo Infrastructure: Onde residem as implementações concretas de persistência e consumo de mensagens.
// domain module — zero Spring, zero Kafka
data class PriceEvent(
    val id: String,
    val product: String,
    val source: String,
    val price: BigDecimal,
    val timestamp: Instant
) {
    fun validate() {
        require(product.isNotBlank()) { "product é obrigatório" }
        require(price > BigDecimal.ZERO) { "price deve ser positivo" }
        require(source.length <= 100) { "source excede 100 caracteres" }
    }
}
Enter fullscreen mode Exit fullscreen mode

📦 Resiliência Extrema: Transactional Outbox + Circuit Breaker

Para evitar a perda de dados caso o Kafka fique indisponível, implementei o Transactional Outbox no serviço em Go:

  1. O dado é salvo na tabela outbox dentro da mesma transação do banco de dados local.
  2. Um Relay Worker assíncrono lê essa tabela e realiza o despacho para o broker.
  • O pulo do gato: Circuit Breaker Implementei um Circuit Breaker que monitora falhas de publicação. Se o Kafka cair, o circuito "abre", silenciando o worker temporariamente para preservar recursos e evitar logs de erro infinitos.

📈 Escalabilidade com SKIP LOCKED

Para escalar o worker sem duplicar envios, utilizei o SELECT FOR UPDATE SKIP LOCKED do PostgreSQL:

SELECT id, payload FROM outbox
WHERE status = 'PENDING'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 100
Enter fullscreen mode Exit fullscreen mode

Isso permite que múltiplas instâncias do worker rodem em paralelo, onde cada uma "pula" as linhas já bloqueadas por outra instância.


📊 Observabilidade: Onde o filho chora e a mãe não vê

Utilizei a stack Loki + Prometheus + Grafana para monitorar a saúde do pipeline:

  • Métricas: Comparação em tempo real entre o que o Go publica e o que o Kotlin consome.
  • Alertas: Notificações de lag no Kafka e latência acima do esperado.
  • Logs: Rastreamento centralizado para identificar falhas entre os containers através do Loki.

💣 Stress Test: Bombardeando o Sistema

Para validar a resiliência, utilizei um script em Bash para saturar o endpoint de entrada e observar o comportamento do sistema sob carga:

# Exemplo do stress test utilizado
./bombardear.sh http://localhost:8080/prices 50
Enter fullscreen mode Exit fullscreen mode

Este teste foi vital para ajustar os limites do pool de conexões do Postgres e validar a recuperação automática do sistema sob pressão.


🔗 Código Fonte

O projeto completo, incluindo as configurações do Docker Compose, o script de stress test e toda a infraestrutura de observabilidade, está disponível no meu GitHub:

Se você está estudando System Design, Go ou Kotlin, o código pode ajudar — especialmente a implementação do relay com FOR UPDATE SKIP LOCKED e a separação rigorosa de módulos no Kotlin.


💡 O que aprendi nessa jornada

  1. Resiliência por design: Desacoplar serviços via eventos muda como você pensa em falhas. O sistema se torna tolerante por natureza.
  2. Garantia real: O Transactional Outbox é a diferença entre perder dados ou apenas atrasar o processamento em momentos de instabilidade.
  3. Métricas são vitais: Sem monitoramento e alertas, você está voando às cegas. A stack Prometheus + Grafana + Loki é um divisor de águas.

E você, como valida a carga e a resiliência dos seus sistemas em produção? Já utilizou o SKIP LOCKED ou prefere outras estratégias de escalonamento de workers? Vamos conversar nos comentários!

Top comments (0)