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["Requisição Gin: POST /pedidos"] --> B["Transação SQL Atômica (GORM)"]
    
    subgraph OUTBOX["Padrão Transacional Outbox (Evita Dual-Write)"]
        B --> C["1. Grava Pedido na Tabela 'pedidos'"]
        B --> D["2. Grava Evento na Tabela 'outbox_events'"]
    end

    D --> E["Outbox Worker (Go Poller / CDC Debezium)"]
    E --> F["amqp.PublishWithContext (RabbitMQ Exchange)"]
    
    subgraph BROKER["RabbitMQ Broker (AMQP 0-9-1)"]
        F --> G["Direct / Topic Exchange"]
        G --> H["Fila Principal: 'pedidos_queue' (Durable)"]
        H -->|"Falhas Repetidas / Nack"| I["Dead Letter Exchange (DLX)"]
        I --> J["Fila DLQ: 'pedidos_dlq' (Inspeção Humana)"]
    end

    H --> K["Consumidor Go Worker (QoS Prefetch: 10)"]
    K --> L{"Processamento com Idempotência"}
    L -->|"Sucesso"| M["msg.Ack(false) -> Remove da Fila"]
    L -->|"Erro Transitório"| N["msg.Nack(false, true) -> Reenfileira"]
    L -->|"Payload Inválido"| O["msg.Nack(false, false) -> Desvia para DLQ"]

    style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style C fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style D fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style E fill:#fffde7,stroke:#fbc02d,stroke-width:2px
    style F fill:#fce4ec,stroke:#e91e63,stroke-width:2px
    style H fill:#e0f2f1,stroke:#009688,stroke-width:2px
    style I fill:#ffebee,stroke:#e53935,stroke-width:2px
    style J fill:#b71c1c,stroke:#b71c1c,stroke-width:2px,color:#fff
    style K fill:#d1c4e9,stroke:#512da8,stroke-width:2px
    style M fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
    style N fill:#fffde7,stroke:#fbc02d,stroke-width:2px
    style O fill:#ffebee,stroke:#e53935,stroke-width:2px

🏛️ 1. O Dilema Arquitetural: RabbitMQ vs Apache Kafka

Em arquiteturas modernas de microsserviços em Go, sistemas orientados a eventos (Event-Driven Architecture) desacoplam o processamento síncrono da API HTTP de tarefas intensivas ou pipelines analíticos. A escolha entre RabbitMQ e Kafka define a semântica de entrega e o modelo de escalabilidade da plataforma:

Dimensão de Comparação RabbitMQ (Message Broker / AMQP) Apache Kafka (Distributed Event Streaming)
Modelo Conceitual Smart Broker, Dumb Consumer (O broker gerencia o estado). Dumb Broker, Smart Consumer (O cliente gerencia seu offset).
Retenção de Mensagens A mensagem é deletada imediatamente após o Ack. Mantém logs persistidos em disco por dias/meses (Message Replay).
Roteamento Exchanges avançadas (Direct, Fanout, Topic, Headers). Roteamento por Chave de Partição (Partition Key).
Throughput / Vazão Dezenas de milhares de msgs/segundo. Milhões de eventos/segundo por partição.
Ordenação Garantida estritamente por fila única (sem paralelismo concorrente). Garantida dentro da mesma partição por chave.
Caso de Uso Ideal Filas de processamento assíncrono, comandos, e-mails, webhooks. Big Data, telemetria IoT, Event Sourcing e auditoria imutável.

🛡️ 2. A Armadilha do Dual-Write e o Padrão Transacional Outbox

Um dos problemas mais comuns em microsserviços é a inconsistência de dados por escrita dupla:

// ❌ ANTI-PATTERN: Inconsistência Crítica Garantida
func CriarPedido(db *gorm.DB, ch *amqp.Channel, pedido *Pedido) error {
    if err := db.Create(pedido).Error; err != nil {
        return err
    }
    // E se a rede cair ou o RabbitMQ reiniciar EXATAMENTE AQUI?
    // O pedido estará salvo no banco, mas o evento NUNCA será publicado!
    return ch.PublishWithContext(...)
}

A Solução Canônica: Transactional Outbox Pattern

Ambas as gravações ocorrem dentro da mesma transação SQL local no banco de dados. Um worker separado lê a tabela outbox_events e publica as mensagens no broker:

-- Tabela de Outbox no PostgreSQL
CREATE TABLE outbox_events (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    agregado_tipo VARCHAR(50) NOT NULL,
    agregado_id VARCHAR(50) NOT NULL,
    payload JSONB NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDENTE',
    criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

⚙️ 3. Implementação Resiliente de Consumo com amqp091-go

No Go, utilizamos a biblioteca oficial github.com/rabbitmq/amqp091-go. Para obter alto rendimento sem sobrecarregar a memória do worker, é mandatório configurar o QoS (Prefetch Count):

package consumer

import (
	"encoding/json"
	"log"
	amqp "github.com/rabbitmq/amqp091-go"
)

type EventoFaturamento struct {
	PedidoID uint    `json:"pedido_id"`
	Valor    float64 `json:"valor"`
}

// IniciarConsumidor configura canal, fila durável, DLQ e prefetch
func IniciarConsumidor(conn *amqp.Connection) error {
	ch, err := conn.Channel()
	if err != nil {
		return err
	}
	defer ch.Close()

	// 1. Configura QoS: limita o buffer local da goroutine a 20 mensagens não confirmadas
	if err := ch.Qos(20, 0, false); err != nil {
		return err
	}

	// 2. Declaração da Fila Principal com Dead-Letter Exchange associada
	argumentos := amqp.Table{
		"x-dead-letter-exchange":    "dlx.pedidos",
		"x-dead-letter-routing-key": "pedidos.mortos",
	}

	fila, err := ch.QueueDeclare(
		"pedidos.faturamento",
		true,  // Durable: sobrevive a restart do RabbitMQ
		false, // Auto-delete
		false, // Exclusive
		false, // No-wait
		argumentos,
	)
	if err != nil {
		return err
	}

	mensagens, err := ch.Consume(
		fila.Name,
		"worker-faturamento-1",
		false, // ⚠️ Auto-Ack = FALSE (Ack manual obrigatório para resiliência)
		false,
		false,
		false,
		nil,
	)
	if err != nil {
		return err
	}

	log.Println("[MESSAGING] Worker escutando pedidos com Ack manual ativo...")

	// 3. Processamento concorrente das entregas
	for entrega := range mensagens {
		var evento EventoFaturamento
		if err := json.Unmarshal(entrega.Body, &evento); err != nil {
			// Erro estrutural grave (Poison Message): Nack sem requeue envia para DLQ
			log.Printf("[ERRO] Payload malformado: %v", err)
			entrega.Nack(false, false)
			continue
		}

		if err := processarComIdempotencia(evento); err != nil {
			// Erro de rede ou indisponibilidade temporária: devolve para a fila
			log.Printf("[ALERTA] Falha temporária no pedido %d: %v", evento.PedidoID, err)
			entrega.Nack(false, true)
			continue
		}

		// Confirmação atômica de entrega e remoção da fila
		entrega.Ack(false)
	}

	return nil
}

🔁 4. Idempotência do Consumidor

Devido à semântica At-Least-Once Delivery (entrega ao menos uma vez) do RabbitMQ e Kafka, uma falha de rede temporária no momento em que o consumidor emite o Ack fará com que o broker reenfileire a mensagem. Portanto, o consumidor receberá e processará a mesma mensagem uma segunda vez.

Para evitar duplicações financeiras ou pedidos duplicados:

  • Toda mensagem deve carregar um identificador único de idempotência (Idempotency-Key ou UUID do evento).
  • O consumidor deve verificar no banco relacional ou Redis se aquele evento_id já foi registrado antes de executar qualquer transação.