Capítulo 20: Projeto Corporativo: Microsserviço de Alta Vazão com Auditoria JSON
Especialização em Backend com Go & Gin • Gin Framework & Go 1.22+ • GORM, Clean Architecture, Concorrência e Alta Performance
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Cliente HTTP / Frontend"] --> B["API Gateway / Router"]
B --> C["Controller / Handler"]
C --> D["Service Layer (Regras de Negócio)"]
D --> E["Repository / ORM (Persistência)"]
E --> F["Banco de Dados / Cache"]
subgraph ARQ["Arquitetura do Capítulo"]
G["Conceito: Projeto Corporativo: Microsserviço de Alta Vazão com Auditoria JSON"]
H["Segurança, Validação e Resiliência"]
I["Alta Performance e Escalabilidade"]
end
D --> ARQ
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
🏛️ 1. Fundamentos Técnicos de Projeto Corporativo: Microsserviço de Alta Vazão com Auditoria JSON
Este capítulo é o projeto integrador da especialização: em vez de introduzir um conceito novo isolado, ele monta um único microsserviço que combina, em uma arquitetura coerente, praticamente tudo visto nos 19 capítulos anteriores — camadas desacopladas (Cap. 1-3), validação e binding (Cap. 4-6), persistência com GORM (Cap. 7-10), autenticação JWT (Cap. 11-13), testes automatizados (Cap. 14-15), concorrência segura com context (Cap. 16), observabilidade com Prometheus/Zap (Cap. 17), documentação Swagger (Cap. 18) e build de produção em scratch (Cap. 19). O objetivo didático não é apresentar API nova, e sim mostrar como as peças já conhecidas se encaixam sob uma exigência não-funcional que nenhum capítulo isolado força a enfrentar: alta vazão sustentada (dezenas de milhares de requisições por segundo) sem perder auditabilidade nem estabilidade sob deploy.
Arquitetura em camadas com DDD leve
A organização de pacotes separa por responsabilidade, não por tipo técnico — evitando pastas genéricas como models/ ou utils/ que viram gaveta de tudo:
/domain -> entidades e regras de negócio puras, sem dependência de framework
/usecase -> orquestração de regras (o "o que fazer"), depende de domain
/repository -> implementações de persistência (GORM, Redis), depende de domain
/delivery/http -> handlers Gin, middlewares, DTOs de request/response
/config -> carregamento de configuração e wiring (injeção de dependências)
A regra de dependência é sempre de fora para dentro: delivery/http conhece usecase, usecase conhece domain, mas domain não importa nada de gin ou gorm — isolando a regra de negócio de detalhes de infraestrutura, o que é o que já foi trabalhado como Clean Architecture nos capítulos de persistência, aqui apenas aplicado à escala de um serviço completo.
Pipeline assíncrono: por que não gravar cada requisição individualmente
Sob alta vazão, fazer um INSERT por requisição HTTP satura o pool de conexões do banco muito antes da CPU ou da rede saturarem — cada round-trip de rede até o banco custa ordens de magnitude mais que processar a requisição em memória. O padrão de ingestão em lote (batching) desacopla a taxa de chegada de eventos da taxa de persistência: um canal Go com buffer grande absorve picos de tráfego, e uma goroutine dedicada drena esse canal, acumulando registros e persistindo em lote por gatilho duplo — tamanho (lote atingiu N itens) ou tempo (um time.Ticker estourou, mesmo com poucos itens acumulados), o que garante que dados não fiquem “presos” esperando o lote encher em períodos de tráfego baixo:
func (b *BatchIngestor) processarLote() {
lote := make([]domain.Evento, 0, b.tamanhoMaximo)
ticker := time.NewTicker(b.intervaloFlush)
defer ticker.Stop()
for {
select {
case evento, ok := <-b.canal:
if !ok { // canal fechado: drena o que resta e encerra
b.persistirLote(lote)
return
}
lote = append(lote, evento)
if len(lote) >= b.tamanhoMaximo {
b.persistirLote(lote)
lote = lote[:0] // reusa o slice subjacente, evita realocar
}
case <-ticker.C:
if len(lote) > 0 {
b.persistirLote(lote)
lote = lote[:0]
}
}
}
}
O INSERT em lote via GORM (db.CreateInBatches(&lote, 500)) roda como uma única transação por lote, reduzindo drasticamente o número de round-trips e permitindo que o banco otimize a escrita em bloco — trade-off aceito é a durabilidade eventual: um crash do processo entre o recebimento e o flush do lote perde os eventos ainda em memória, por isso essa técnica é apropriada para telemetria/eventos de auditoria (onde perder uma fração ínfima sob falha catastrófica é tolerável), não para transações financeiras que exigem ACID por escrita.
Trilha de auditoria com integridade criptográfica
Um requisito corporativo comum além do “log de erro” é a trilha de auditoria inviolável: um registro de que “o usuário X executou a ação Y no instante Z” que resista a alteração posterior — inclusive por um administrador do próprio banco de dados. A técnica usada é assinar cada evento com HMAC-SHA256, usando uma chave secreta que nunca é exposta:
func AssinarEvento(op, detalhe string, chaveSecreta []byte) EventoAuditoria {
ts := time.Now().UTC().Unix()
mensagem := fmt.Sprintf("%d:%s:%s", ts, op, detalhe)
h := hmac.New(sha256.New, chaveSecreta)
h.Write([]byte(mensagem))
assinatura := hex.EncodeToString(h.Sum(nil))
return EventoAuditoria{Timestamp: ts, Operacao: op, Detalhe: detalhe, Assinatura: assinatura}
}
A verificação posterior recalcula o HMAC com a mesma chave e compara com hmac.Equal (nunca com == direto — comparação de bytes ingênua é vulnerável a timing attacks, já que == retorna assim que encontra a primeira diferença, vazando informação sobre quantos bytes iniciais bateram). Se um único caractere do evento original for alterado no banco, a assinatura recalculada não bate — provando adulteração sem exigir infraestrutura de blockchain.
Graceful Shutdown: terminar sem derrubar requisições em voo
Em um cluster Kubernetes, um deploy novo envia SIGTERM ao contêiner antigo e espera um prazo (terminationGracePeriodSeconds) antes de matá-lo com SIGKILL. Se o servidor Go morrer instantaneamente ao receber o sinal, todo cliente com requisição em andamento recebe erro de conexão resetada. O padrão correto captura o sinal, para de aceitar novas conexões, mas deixa as em andamento concluírem dentro de um timeout:
srv := &http.Server{Addr: ":8080", Handler: router}
go srv.ListenAndServe()
sinal := make(chan os.Signal, 1)
signal.Notify(sinal, syscall.SIGINT, syscall.SIGTERM)
<-sinal // bloqueia até o SO enviar SIGINT/SIGTERM
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("shutdown forçado após timeout:", err)
}
srv.Shutdown(ctx) para o listener imediatamente, mas aguarda handlers ativos terminarem (respeitando o ctx do context já estudado no Cap. 16) — se o timeout de 10s expirar antes de todos concluírem, ele força o encerramento e retorna erro, que deve ser logado (via Zap, Cap. 17) para investigação.
Fechando o ciclo: observabilidade, documentação e empacotamento
O mesmo serviço expõe /metrics (Prometheus, Cap. 17) instrumentando a taxa de ingestão e o tamanho do lote atual como Gauge; documenta suas rotas via anotações swag (Cap. 18); e é empacotado em uma imagem scratch multi-stage (Cap. 19) com o binário estático final. Testes de carga com ferramentas como k6 ou vegeta validam, antes de produção, que o SLO de latência (p95/p99) se mantém sob milhares de requisições concorrentes — fechando o ciclo entre projetar, instrumentar, documentar, empacotar e validar sob carga real.