"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)]
-
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" }
}
}
📦 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:
- O dado é salvo na tabela outbox dentro da mesma transação do banco de dados local.
- 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
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
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:
- Repositório do Projeto Go: https://github.com/costa-lucas/price-crawler.git
- Repositório do Projeto Kotlin: https://github.com/costa-lucas/price-consumer.git
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
- Resiliência por design: Desacoplar serviços via eventos muda como você pensa em falhas. O sistema se torna tolerante por natureza.
- Garantia real: O Transactional Outbox é a diferença entre perder dados ou apenas atrasar o processamento em momentos de instabilidade.
- 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)