MÓDULO 09 · CONCEITO 08 DE 14

Message Queues

desacoplamento temporal · garantias de entrega · RabbitMQ · SQS · AMQP · dead letter queues

Tempo de leitura ~23 min Pré-requisito Conceito 01 — Taxonomia de Comunicação Próximo 09 · Apache Kafka

Uma message queue é um buffer persistente que fica entre produtor e consumidor — o produtor deposita mensagens sem saber quem vai processá-las, e o consumidor processa no seu próprio ritmo sem saber quem as produziu. Esse desacoplamento temporal é o benefício central: se o consumidor está lento, sobrecarregado ou indisponível, as mensagens ficam na fila esperando — sem derrubar o produtor. É a fundação de toda comunicação assíncrona entre serviços.

Anatomia de uma message queue

Os componentes fundamentais são consistentes entre brokers diferentes:

Modelos de entrega

Point-to-point (queue)

Uma mensagem é entregue a exatamente um consumidor — mesmo que vários estejam ouvindo a mesma fila. O broker faz load balancing entre os consumidores disponíveis. Modelo correto para tarefas que devem ser executadas uma única vez: processar um pagamento, gerar um relatório, enviar um e-mail.

# Point-to-point: 1 mensagem → 1 consumidor
Producer → [Queue] → Consumer A  (ou Consumer B, não ambos)
                   → Consumer B

# Se Consumer A trava após receber mas antes de ACKar,
# o broker reentrega para Consumer B após o timeout

Publish/Subscribe (topic/exchange)

Uma mensagem é entregue a todos os consumidores subscritos. Modelo correto para eventos que múltiplos sistemas precisam conhecer: pedido criado → notificar estoque, notificar faturamento, atualizar dashboard, enviar e-mail de confirmação.

# Pub/Sub: 1 mensagem → N consumidores
Producer → [Topic/Exchange] → Queue de Estoque    → Consumer Estoque
                            → Queue de Faturamento → Consumer Faturamento
                            → Queue de Dashboard   → Consumer Dashboard

Garantias de entrega

A garantia de entrega determina quantas vezes uma mensagem pode ser recebida pelo consumidor — e é um trade-off entre complexidade e confiabilidade:

GarantiaDescriçãoRiscoQuando usar
At-most-once Entrega no máximo uma vez — pode perder mensagens Perda de dados se consumidor cair após receber mas antes de processar Métricas, logs de baixa prioridade, dados que podem ser recalculados
At-least-once Entrega no mínimo uma vez — pode duplicar Processamento duplicado se consumidor cair após processar mas antes de ACKar Padrão recomendado — exige consumidores idempotentes
Exactly-once Entrega exatamente uma vez Complexidade muito alta, overhead de performance significativo Transações financeiras críticas (Kafka com transações, ou compensação manual)
atenção Exactly-once é uma ilusão na maioria dos brokers: o que parece exactly-once frequentemente é at-least-once com idempotência no consumidor — o que é a abordagem correta. True exactly-once requer transações distribuídas que têm custo de performance e complexidade muito altos. Projete consumidores idempotentes e use at-least-once.

Acknowledgements e visibilidade

O mecanismo de ACK é o coração da confiabilidade das filas. Quando um consumidor recebe uma mensagem, ela fica "invisível" para outros consumidores por um período (visibility timeout). Se o ACK não chega dentro do timeout, o broker assume que o consumidor falhou e reentrega a mensagem.

# Ciclo de vida de uma mensagem
1. Producer publica mensagem → mensagem fica em estado READY
2. Consumer recebe mensagem → mensagem fica em INVISIBLE/PROCESSING (timeout: 30s)
3a. Consumer processa com sucesso → envia ACK → mensagem é DELETADA da fila
3b. Consumer falha → envia NACK (ou timeout expira) → mensagem volta para READY
3c. Após N tentativas → mensagem vai para Dead Letter Queue

# Visibility timeout no AWS SQS
# Se o processamento leva 25s, configure timeout para 60s (2-3x o tempo esperado)
# Visibility timeout muito curto → reentregas desnecessárias
# Visibility timeout muito longo → mensagens travadas por muito tempo se consumidor falhar

Dead Letter Queue (DLQ)

Uma DLQ é uma fila especial que recebe mensagens que falharam repetidamente. Em vez de tentar infinitamente ou descartar, o broker move a mensagem para a DLQ após N tentativas. Isso evita que mensagens "poison" (que sempre falham) bloqueiem processamento de mensagens saudáveis.

# RabbitMQ — configurar DLQ via políticas
rabbitmqadmin declare queue \
  name=orders.processing \
  durable=true \
  arguments='{"x-dead-letter-exchange": "dlx", "x-dead-letter-routing-key": "orders.dead", "x-message-ttl": 86400000, "x-max-length": 100000}'

rabbitmqadmin declare exchange name=dlx type=direct durable=true
rabbitmqadmin declare queue name=orders.dead durable=true
rabbitmqadmin declare binding source=dlx destination=orders.dead routing_key=orders.dead

# AWS SQS — configurar DLQ via redrive policy
{
  "deadLetterTargetArn": "arn:aws:sqs:us-east-1:123456789:orders-dead",
  "maxReceiveCount": 3  // após 3 falhas, vai para DLQ
}
nota DLQ é alarme, não lixo: configure alertas para quando mensagens chegam na DLQ — cada mensagem lá representa uma falha que precisa de atenção. Implemente um processo de replay: investigar a causa raiz, corrigir o bug, e reprocessar as mensagens da DLQ para a fila original.

RabbitMQ em profundidade

RabbitMQ implementa o protocolo AMQP 0-9-1 e é um dos brokers de fila mais usados no mundo. Seu modelo de roteamento usa exchanges como roteadores entre producers e queues — os producers nunca publicam diretamente em filas, mas em exchanges com routing keys.

Tipos de exchange

TipoRoteamentoUso típico
directRouting key exataFilas de trabalho point-to-point
fanoutBroadcast para todas as filas bindingsPub/sub simples, notificações
topicRouting key com wildcards (* e #)Roteamento por categoria: "orders.created.us"
headersHeaders da mensagem em vez de routing keyRoteamento por atributos complexos
# RabbitMQ — topologia completa para sistema de pedidos
# Exchange principal para eventos de pedidos
rabbitmqadmin declare exchange name=orders type=topic durable=true

# Filas por subsistema com bindings para routing keys específicas
rabbitmqadmin declare queue name=inventory.updates durable=true \
  arguments='{"x-dead-letter-exchange": "dlx"}'
rabbitmqadmin declare queue name=billing.events durable=true \
  arguments='{"x-dead-letter-exchange": "dlx"}'
rabbitmqadmin declare queue name=notifications.orders durable=true \
  arguments='{"x-dead-letter-exchange": "dlx"}'

# Bindings: routing key patterns
# orders.# → todas as queues (qualquer evento de pedido)
# orders.created → apenas eventos de criação
# orders.*.us → eventos de pedidos americanos

rabbitmqadmin declare binding \
  source=orders destination=inventory.updates routing_key="orders.created"
rabbitmqadmin declare binding \
  source=orders destination=billing.events routing_key="orders.#"
rabbitmqadmin declare binding \
  source=orders destination=notifications.orders routing_key="orders.created"

Prefetch e QoS

Por padrão, RabbitMQ pode enviar infinitas mensagens a um consumidor antes de receber ACKs — o que pode sobrecarregar consumidores lentos. O basicQos(prefetchCount) limita quantas mensagens o broker envia antes de receber ACK, implementando backpressure.

# prefetchCount = 1: envia uma mensagem por vez — fair dispatch
# Mais lento (mais round-trips de ACK), mas distribui carga uniformemente
channel.basic_qos(prefetch_count=1)

# prefetchCount = 10-50: bom equilíbrio entre throughput e fairness
channel.basic_qos(prefetch_count=10)

# prefetchCount = 0 (default): sem limite — risco de memory pressure no consumidor

Amazon SQS

SQS é um serviço de fila gerenciado da AWS — sem infraestrutura para operar, alta disponibilidade nativa, e integração com o ecossistema AWS (Lambda, SNS, EventBridge). Dois tipos:

# SQS — padrão de uso com Lambda consumer
# 1. Criar fila com DLQ
aws sqs create-queue \
  --queue-name orders-processing \
  --attributes '{
    "VisibilityTimeout": "60",
    "MessageRetentionPeriod": "86400",
    "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:...:orders-dead\",\"maxReceiveCount\":\"3\"}"
  }'

# 2. Lambda trigger — SQS invoca Lambda com batch de mensagens
# Configurar Event Source Mapping:
aws lambda create-event-source-mapping \
  --function-name process-order \
  --event-source-arn arn:aws:sqs:us-east-1:123456789:orders-processing \
  --batch-size 10 \
  --maximum-batching-window-in-seconds 5

# 3. Lambda retorna falhas parciais (não reprocessar mensagens que já funcionaram)
# Response da Lambda para SQS:
{
  "batchItemFailures": [
    { "itemIdentifier": "msg-id-que-falhou" }
    // mensagens NÃO listadas → consideradas sucesso → deletadas
    // mensagens listadas → voltam para fila ou DLQ
  ]
}

Patterns de uso de filas

Worker queue — distribuição de trabalho

# Múltiplos workers consumindo uma fila — horizontal scaling
                     → Worker 1 (processa pedido 1)
Queue de pedidos    → Worker 2 (processa pedido 2)
                     → Worker 3 (processa pedido 3)

# Escalar workers conforme tamanho da fila:
# queue.depth > 1000 → adicionar workers
# queue.depth < 100  → remover workers
# Com Kubernetes HPA + KEDA (Kubernetes Event-Driven Autoscaling)

Request/Reply via fila — RPC assíncrono

# Padrão correlation ID + reply-to queue
# Útil quando o resultado é necessário mas a latência pode ser alta

# 1. Producer envia request com correlation_id e reply_to
{
  "payload": {"order_id": "abc"},
  "correlation_id": "uuid-123",
  "reply_to": "responses.api-gateway-1"  # fila privada do producer
}

# 2. Worker processa e envia response para reply_to
{
  "payload": {"status": "approved"},
  "correlation_id": "uuid-123"  # producer correlaciona com request original
}

# 3. Producer aguarda na fila de responses com timeout
# Desvantagem: complexidade alta — prefira REST/gRPC para request-response

Competing consumers — escalonamento horizontal

O padrão mais comum: múltiplos consumidores idênticos lendo da mesma fila. O broker distribui as mensagens entre eles. Para escalar, adicionar mais consumidores — sem alterar producers ou a fila.

Comparação por linguagem

Publicação e consumo de mensagens com RabbitMQ em cada linguagem, incluindo confirmações, DLQ e tratamento de falhas.

C# — MassTransit + RabbitMQ
// MassTransit abstrai o broker — mesmo código para RabbitMQ, SQS, Azure Service Bus

// Definir a mensagem (contrato)
public record OrderCreated(
    string OrderId,
    string CustomerId,
    decimal TotalAmount,
    DateTimeOffset CreatedAt);

// Consumer — implementa IConsumer<T>
public class OrderCreatedConsumer : IConsumer<OrderCreated>
{
    private readonly IInventoryService _inventory;
    private readonly ILogger<OrderCreatedConsumer> _logger;

    public OrderCreatedConsumer(IInventoryService inventory, ILogger<OrderCreatedConsumer> logger)
    {
        _inventory = inventory;
        _logger = logger;
    }

    public async Task Consume(ConsumeContext<OrderCreated> context)
    {
        var msg = context.Message;
        _logger.LogInformation("Processando pedido {OrderId}", msg.OrderId);

        try
        {
            await _inventory.ReserveStockAsync(msg.OrderId, context.CancellationToken);
            // ACK implícito — se o método retornar sem exceção, MassTransit faz ACK
        }
        catch (StockUnavailableException ex)
        {
            // Falha de negócio — não retentável, mover para DLQ imediatamente
            _logger.LogWarning("Estoque indisponível para {OrderId}", msg.OrderId);
            throw new Exception($"Estoque indisponível: {ex.Message}");
            // MassTransit faz NACK → após retry_count → DLQ
        }
    }
}

// Program.cs — configuração
builder.Services.AddMassTransit(x =>
{
    x.AddConsumer<OrderCreatedConsumer>()
        .Endpoint(e =>
        {
            e.Name = "inventory.order-created";
            e.PrefetchCount = 10;
        });

    x.UsingRabbitMq((ctx, cfg) =>
    {
        cfg.Host("rabbitmq://rabbitmq:5672", h =>
        {
            h.Username("guest");
            h.Password("guest");
        });

        // Configurar DLQ automático
        cfg.ReceiveEndpoint("inventory.order-created", e =>
        {
            e.ConfigureDeadLetterQueueDeadLetterTransport();
            e.ConfigureDeadLetterQueueErrorTransport();
            e.UseMessageRetry(r =>
                r.Exponential(3, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(5)));
            e.ConfigureConsumer<OrderCreatedConsumer>(ctx);
        });
    });
});

// Publicar evento
public class OrderService
{
    private readonly IPublishEndpoint _bus;

    public OrderService(IPublishEndpoint bus) => _bus = bus;

    public async Task<Order> CreateOrderAsync(CreateOrderCommand cmd, CancellationToken ct)
    {
        var order = await _repo.CreateAsync(cmd, ct);

        // Publicar evento — MassTransit roteia para o exchange correto
        await _bus.Publish(new OrderCreated(
            order.Id, order.CustomerId, order.Total, DateTimeOffset.UtcNow), ct);

        return order;
    }
}

MassTransit gerencia retries com backoff exponencial, DLQ automático e serialização — sem código de infraestrutura no consumidor. Trocar de RabbitMQ para SQS é mudar apenas a configuração de UsingRabbitMq.

Python — aio-pika (asyncio RabbitMQ)
import asyncio
import json
import logging
from dataclasses import dataclass
from datetime import datetime, timezone
import aio_pika
from aio_pika import DeliveryMode, Message

logger = logging.getLogger(__name__)

@dataclass
class OrderCreated:
    order_id: str
    customer_id: str
    total_amount: float
    created_at: str

# Producer
async def publish_order_created(
    channel: aio_pika.Channel,
    order: OrderCreated,
) -> None:
    message = Message(
        body=json.dumps({
            "order_id": order.order_id,
            "customer_id": order.customer_id,
            "total_amount": order.total_amount,
            "created_at": order.created_at,
        }).encode(),
        delivery_mode=DeliveryMode.PERSISTENT,  # sobrevive a restart do broker
        content_type="application/json",
        headers={
            "event_type": "order.created",
            "version": "1",
        },
    )
    # Publicar no exchange topic — routing key define o roteamento
    await channel.default_exchange.publish(
        message, routing_key="orders.created"
    )
    logger.info("Evento order.created publicado: %s", order.order_id)

# Consumer com retry e DLQ
async def consume_order_created(connection: aio_pika.Connection) -> None:
    channel = await connection.channel()
    await channel.set_qos(prefetch_count=10)  # processar 10 por vez

    # Declarar DLX e DLQ
    dlx = await channel.declare_exchange(
        "dlx", aio_pika.ExchangeType.DIRECT, durable=True
    )
    dlq = await channel.declare_queue(
        "inventory.order-created.dead", durable=True
    )
    await dlq.bind(dlx, routing_key="inventory.order-created")

    # Fila principal com DLQ configurado
    queue = await channel.declare_queue(
        "inventory.order-created",
        durable=True,
        arguments={
            "x-dead-letter-exchange": "dlx",
            "x-dead-letter-routing-key": "inventory.order-created",
            "x-max-delivery-attempts": 3,
        },
    )
    await queue.bind("orders", routing_key="orders.created")

    async def handle_message(message: aio_pika.IncomingMessage) -> None:
        async with message.process(requeue=False):  # NACK sem requeue → DLQ
            try:
                data = json.loads(message.body)
                order = OrderCreated(**data)

                # Verificar se é reentrega (idempotência)
                delivery_count = message.headers.get("x-death", [{}])[0].get("count", 0)
                if delivery_count > 0:
                    logger.warning(
                        "Reentrega #%d para pedido %s",
                        delivery_count, order.order_id
                    )

                await process_inventory(order)
                # ACK automático ao sair do context manager sem exceção

            except StockUnavailableError as e:
                logger.error("Estoque indisponível: %s", e)
                raise  # NACK → DLQ após max attempts
            except Exception as e:
                logger.exception("Erro ao processar: %s", e)
                raise  # NACK → retry → DLQ

    await queue.consume(handle_message)
    logger.info("Consumindo fila inventory.order-created...")
    await asyncio.Future()  # aguardar indefinidamente

async def main():
    connection = await aio_pika.connect_robust(
        "amqp://guest:guest@rabbitmq:5672/",
        # Reconexão automática em caso de queda do broker
    )
    await consume_order_created(connection)

aio_pika.connect_robust reconecta automaticamente ao broker — essencial para produção. O async with message.process() faz ACK ao sair normalmente e NACK em exceção.

Go — amqp091 (RabbitMQ)
package messaging

import (
    "context"
    "encoding/json"
    "fmt"
    "log/slog"
    "time"

    amqp "github.com/rabbitmq/amqp091-go"
)

type OrderCreated struct {
    OrderID    string    `json:"order_id"`
    CustomerID string    `json:"customer_id"`
    Total      float64   `json:"total_amount"`
    CreatedAt  time.Time `json:"created_at"`
}

// Publisher
type Publisher struct {
    conn    *amqp.Connection
    channel *amqp.Channel
}

func NewPublisher(url string) (*Publisher, error) {
    conn, err := amqp.Dial(url)
    if err != nil {
        return nil, fmt.Errorf("conectar ao RabbitMQ: %w", err)
    }
    ch, err := conn.Channel()
    if err != nil {
        return nil, fmt.Errorf("abrir channel: %w", err)
    }
    // Confirm mode — garantia de que o broker recebeu a mensagem
    if err := ch.Confirm(false); err != nil {
        return nil, fmt.Errorf("ativar confirm mode: %w", err)
    }
    return &Publisher{conn: conn, channel: ch}, nil
}

func (p *Publisher) PublishOrderCreated(ctx context.Context, order OrderCreated) error {
    body, err := json.Marshal(order)
    if err != nil {
        return fmt.Errorf("serializar mensagem: %w", err)
    }

    // PublishWithDeferredConfirmWithContext — aguarda confirmação do broker
    confirm, err := p.channel.PublishWithDeferredConfirmWithContext(ctx,
        "orders",        // exchange
        "orders.created", // routing key
        true,            // mandatory — retorna erro se não há binding
        false,           // immediate
        amqp.Publishing{
            ContentType:  "application/json",
            DeliveryMode: amqp.Persistent, // sobrevive a restart
            Timestamp:    time.Now(),
            Body:         body,
            Headers: amqp.Table{
                "event_type": "order.created",
                "version":    "1",
            },
        },
    )
    if err != nil {
        return fmt.Errorf("publicar mensagem: %w", err)
    }

    // Aguardar ACK do broker (confirm mode)
    if !confirm.Wait() {
        return fmt.Errorf("broker não confirmou a mensagem")
    }

    slog.Info("evento publicado", "order_id", order.OrderID)
    return nil
}

// Consumer com retry e reconexão
type Consumer struct {
    url     string
    handler func(context.Context, OrderCreated) error
}

func (c *Consumer) Start(ctx context.Context) error {
    for {
        if err := c.consume(ctx); err != nil {
            slog.Error("erro no consumer, reconectando em 5s", "err", err)
            select {
            case <-ctx.Done():
                return ctx.Err()
            case <-time.After(5 * time.Second):
            }
        }
    }
}

func (c *Consumer) consume(ctx context.Context) error {
    conn, err := amqp.Dial(c.url)
    if err != nil {
        return err
    }
    defer conn.Close()

    ch, err := conn.Channel()
    if err != nil {
        return err
    }
    defer ch.Close()

    ch.Qos(10, 0, false) // prefetch = 10

    deliveries, err := ch.Consume(
        "inventory.order-created",
        "",    // consumer tag gerado automaticamente
        false, // autoAck=false — ACK manual
        false, false, false, nil,
    )
    if err != nil {
        return err
    }

    for {
        select {
        case <-ctx.Done():
            return nil
        case d, ok := <-deliveries:
            if !ok {
                return fmt.Errorf("channel fechado")
            }
            c.processDelivery(ctx, d)
        }
    }
}

func (c *Consumer) processDelivery(ctx context.Context, d amqp.Delivery) {
    var order OrderCreated
    if err := json.Unmarshal(d.Body, &order); err != nil {
        slog.Error("mensagem inválida, movendo para DLQ", "err", err)
        d.Nack(false, false) // requeue=false → DLQ
        return
    }

    if err := c.handler(ctx, order); err != nil {
        slog.Error("falha ao processar", "order_id", order.OrderID, "err", err)
        d.Nack(false, false) // NACK → DLQ após max-attempts
        return
    }

    d.Ack(false) // sucesso → deletar da fila
}

Go usa confirm mode para garantir que o broker recebeu e persistiu a mensagem antes de retornar do Publish — essencial para at-least-once no produtor. O consumer se reconecta automaticamente em loop.

Decisões de engenharia

RabbitMQ vs SQS
RabbitMQ oferece modelo de roteamento rico (exchanges direct/topic/fanout/headers), baixa latência (~1ms), e controle total do broker — adequado quando o roteamento de mensagens é complexo, quando você precisa de pub/sub sem fan-out manual via SNS, ou quando a latência importa. O custo é operar o cluster, gerenciar HA com quorum queues, e monitorar filas que crescem. SQS é a escolha quando você quer zero operações: sem cluster para gerenciar, HA nativa, scaling automático, e integração direta com Lambda, SNS e EventBridge. A desvantagem é latência maior (5-20ms), sem roteamento nativo por conteúdo, e custo por requisição que cresce em volumes muito altos. Para times AWS-first sem expertise em RabbitMQ, SQS + SNS é o padrão racional. Para sistemas que precisam de roteamento complexo ou não são AWS-only, RabbitMQ com cluster gerenciado (CloudAMQP, Bitnami) é preferível.
SQS Standard vs FIFO
Standard Queue oferece throughput praticamente ilimitado e at-least-once delivery com ordenação de melhor esforço — adequada para a maioria dos casos onde os consumidores são idempotentes e a ordem absoluta não importa (processar pagamentos em qualquer ordem, gerar relatórios). FIFO Queue garante ordenação estrita dentro de um message group e exactly-once processing por deduplication ID — mas limita a 3000 msg/s com batching (300 sem). Use FIFO quando: a ordem de processamento tem consequência de negócio (débitos e créditos na mesma conta devem ser ordenados), ou quando os consumidores são difíceis de tornar idempotentes. O custo do FIFO é o throughput 10-100× menor — que raramente é o gargalo real, mas considere antes de escolher.
At-least-once + idempotência vs Exactly-once
Exactly-once real requer transações distribuídas entre o broker e o banco de dados do consumidor — o que ou não é suportado (RabbitMQ, SQS) ou tem custo de performance significativo (Kafka com transações). At-least-once com consumidores idempotentes é a solução correta para a maioria dos sistemas: o consumidor verifica se já processou a mensagem (via idempotency_key em Redis ou banco) e descarta duplicatas. O overhead é uma leitura de Redis por mensagem (~0.5ms), trivial comparado ao custo de transações distribuídas. A verdade incômoda: exactly-once em sistemas distribuídos é impossível sem protocolo 2PC, que tem seus próprios problemas de disponibilidade. Projete para at-least-once desde o início, torne os consumidores idempotentes por design, e economize a complexidade de exactly-once para onde genuinamente não há alternativa (ex: débito financeiro sem idempotency key externa).
Push-based vs Pull-based consumption
No modelo push (RabbitMQ), o broker empurra mensagens para o consumidor conforme chegam — baixa latência, mas o consumidor precisa controlar backpressure via prefetch para não ser sobrecarregado. No modelo pull (SQS, Kafka), o consumidor busca mensagens ativamente — mais controle sobre a taxa de processamento, mas latência adicional de polling (SQS long polling até 20s) e complexidade de gerenciar o loop de pull. RabbitMQ com prefetch é melhor para latência baixa e processamento uniforme. SQS com Lambda é melhor para cargas variáveis onde você quer que a AWS gerencie o scaling de consumidores (Lambda escala automaticamente com o tamanho da fila). O trade-off é: push é mais eficiente, pull é mais controlado.

Exercícios práticos

  1. Configure um RabbitMQ local com Docker e crie a topologia: exchange topic "orders", três filas (inventory, billing, notifications) com bindings para "orders.#". Publique eventos com routing keys diferentes e verifique o roteamento no Management UI. Critério: Management UI mostra exchange "orders" com os 3 bindings; publicar "orders.created" entrega nas 3 filas; publicar "orders.cancelled" entrega somente nas filas com binding "orders.#"; reiniciar o container com volume persistido não perde mensagens enfileiradas.
  2. Implemente DLQ completa: configure maxReceiveCount=3, crie a DLQ, e escreva um consumidor que falha nas 3 primeiras tentativas. Implemente também um script de replay que reprocessa mensagens da DLQ. Critério: após 3 NACKs, a mensagem aparece na DLQ visível no Management UI com header x-death mostrando histórico de tentativas; script de replay republica a mensagem na fila original; segunda execução (sem falha) processa com sucesso e remove da DLQ.
  3. Meça o impacto do prefetchCount: consuma uma fila com 1000 mensagens onde cada processamento leva 10ms simulado. Compare throughput e distribuição de carga entre 3 workers com prefetch=1, prefetch=10, prefetch=100. Critério: com prefetch=1, distribuição entre workers ≤10% de desvio e throughput total calculado; com prefetch=100, throughput ≥3× maior mas distribuição com desvio >30% (um worker acumula); tabela com valores numéricos reais medidos.
  4. Implemente idempotência: adicione um campo idempotency_key à mensagem e armazene keys processadas em Redis (TTL de 30 segundos para facilitar o teste). Critério: publicar a mesma mensagem (mesmo idempotency_key) duas vezes em sequência: apenas uma operação de negócio ocorre (verificável via log ou banco); após o TTL de 30s, publicar novamente a mensagem resulta em reprocessamento normal (segunda janela de deduplicação); throughput não cai mais que 5% vs consumidor sem Redis.
  5. Configure SQS FIFO com message group IDs para garantir ordenação por customer_id: 10 pedidos do cliente A e 10 do cliente B enviados intercalados. Critério: pedidos do cliente A chegam ao consumidor em ordem sequencial (sequence_number 1-10); pedidos de A e B são processados em paralelo (timestamps de processamento sobrepostos); publicar pedido com deduplication_id já usado na janela de 5 minutos não gera duplicata no consumidor.

Perguntas de entrevista

Explique o mecanismo de ACK em message queues e o problema do visibility timeout muito curto.

Quando um consumidor recebe uma mensagem, o broker não a deleta imediatamente — ele a marca como "in-flight" (ou "invisible" no SQS) por um período chamado visibility timeout. Durante esse período, outros consumidores não veem a mensagem. O consumidor tem esse tempo para processar e enviar o ACK. Se o ACK não chegar dentro do timeout, o broker assume que o consumidor falhou — independente do motivo real — e reentrega a mensagem para outro consumidor.

Visibility timeout muito curto cria o problema de reentregas desnecessárias: se o processamento leva 25s e o timeout é 30s, qualquer variação de latência (garbage collection, lentidão de banco) pode fazer o timeout expirar antes do ACK — a mensagem é reenviada, dois consumidores processam a mesma mensagem em paralelo. O resultado é processamento duplicado que precisa de idempotência para ser seguro.

A regra prática: visibility timeout deve ser 3-5× o tempo médio de processamento. Se o processamento leva 10s, configure 30-60s. No SQS, você pode extender o visibility timeout dinamicamente se souber que o processamento vai demorar mais que o previsto — útil para workers que processam itens de tamanho variável. No RabbitMQ, o heartbeat entre broker e consumidor serve como keepalive — se a conexão cair, o broker detecta e reentrega automaticamente.

O que é uma Dead Letter Queue, como configurar e como operar o processo de replay?

DLQ é uma fila especial que recebe mensagens que excederam o número máximo de tentativas de processamento. Em vez de tentar infinitamente (loop eterno) ou descartar silenciosamente (perda de dados), o broker move a mensagem para a DLQ depois de N falhas — preservando o payload para análise e correção.

Configuração: no RabbitMQ, a fila principal declara x-dead-letter-exchange (DLX) e o broker roteia para a DLQ quando o NACK tem requeue=false. No SQS, a RedrivePolicy define deadLetterTargetArn e maxReceiveCount. A mensagem na DLQ carrega metadados de morte: quantas vezes foi tentada, qual o erro, qual a fila de origem.

O processo de replay é: (1) receber alerta de mensagens na DLQ (configurar alarme no CloudWatch ou Prometheus), (2) investigar a causa raiz — log do erro, inspecionar o payload, identificar o bug, (3) corrigir o bug e fazer deploy, (4) republicas as mensagens da DLQ na fila original. O replay não deve ser automático — uma mensagem que falhou 3 vezes tem causa raiz que precisa ser investigada. Replay automático sem correção apenas move o problema de volta para a fila principal. O SQS tem "DLQ redrive" nativo no console; RabbitMQ requer um consumer customizado que publica de volta na exchange original.

Como implementar consumidores idempotentes para at-least-once delivery?

Idempotência significa que processar a mesma mensagem N vezes tem o mesmo efeito que processá-la uma vez. Com at-least-once delivery, duplicatas são inevitáveis — o consumidor deve ser projetado para lidar com elas.

Padrão de deduplicação com chave: cada mensagem carrega um idempotency_key (UUID gerado pelo produtor, ou hash do conteúdo). O consumidor, antes de processar, verifica em um store rápido (Redis com SET NX, ou banco com UNIQUE constraint) se já processou essa key. Se sim, descarta. Se não, processa e registra a key (dentro da mesma transação com o banco, se possível).

Exemplos de idempotência natural: INSERT com ON CONFLICT DO NOTHING (upsert); operações de SET (setar o campo X para o valor Y é idempotente, incrementar não é); deleção (deletar algo já deletado é seguro). Exemplos que precisam de controle explícito: incrementar um contador, enviar um e-mail, disparar uma transferência bancária.

O TTL do registro de deduplicação é crítico: muito curto (1 minuto) e mensagens reentregues após o TTL são processadas novamente; muito longo (30 dias) e o store cresce indefinidamente. O padrão é TTL igual ao visibility timeout × max delivery attempts × fator de segurança — tipicamente 24-72h. O SQS FIFO tem deduplicação nativa por 5 minutos via MessageDeduplicationId.

Qual a diferença entre exchanges direct, topic e fanout no RabbitMQ, e quando usar cada um?

Direct exchange: roteia para filas cujo binding key é exatamente igual à routing key da mensagem. Simples e eficiente para ponto-a-ponto ou quando o roteamento é por categoria exata: routing_key="orders.created" → fila "orders-processor". Use para worker queues onde cada tipo de trabalho tem uma fila dedicada.

Topic exchange: routing keys com wildcards — * substitui uma palavra, # substitui zero ou mais palavras. Uma mensagem com routing_key="orders.created.us-east" pode ser roteada para: fila com binding orders.# (qualquer evento de orders), fila com binding orders.created.* (criações em qualquer região), e fila com binding *.*.us-east (qualquer evento do us-east). É o exchange mais poderoso para sistemas de eventos onde múltiplos consumidores precisam de subconjuntos diferentes dos eventos.

Fanout exchange: ignora a routing key e entrega para todas as filas com binding. Mais simples que topic para pub/sub puro. Útil para broadcast: evento de "sistema em manutenção" precisa chegar a todos os serviços independente de routing key. O custo é zero granularidade de roteamento.

A escolha prática: use topic exchange para a maioria dos sistemas de eventos — ele é mais expressivo que direct e mais fácil de raciocinar que headers exchange. Use fanout para notificações que genuinamente vão para todos. Use direct para filas de trabalho simples onde a routing key é o tipo de tarefa.

Como escalar consumidores dinamicamente com base no tamanho da fila usando KEDA?

Escalar workers manualmente (adicionar instâncias quando a fila cresce) é operacionalmente custoso e reativo. KEDA (Kubernetes Event-Driven Autoscaling) é um operador Kubernetes que escala Deployments baseado em métricas externas — incluindo tamanho de fila.

Com KEDA, você define um ScaledObject que observa uma fila (RabbitMQ, SQS, Kafka) e ajusta o número de réplicas de um Deployment: queueLength / messagesPerWorker determina o número ideal de réplicas. Se a fila tem 1000 mensagens e cada worker processa 100 por minuto, o KEDA escala para 10 workers. Quando a fila esvazia, escala de volta para 0 (scale to zero — economia em cargas variáveis).

Configuração para SQS: o ScaledObject aponta para a fila SQS, define queueLength como trigger, e targetQueueLength como mensagens por réplica (ex: 100). Para RabbitMQ: usa a API de management para ler o messages_ready da fila. O KEDA também funciona com HPA (Horizontal Pod Autoscaler) como backend — mantendo a lógica de scaling do Kubernetes padrão.

Cuidados: scale-down abrupto pode causar perda de mensagens em processamento se o Pod for terminado com SIGTERM sem graceful shutdown. Configure terminationGracePeriodSeconds suficiente para o worker completar o processamento atual e fazer ACK. Scale-up tem latência (tempo de criar o Pod) — configure cooldownPeriod para não oscilar entre scale-up/down desnecessariamente.

Referências

  1. livro Enterprise Integration Patterns — Gregor Hohpe & Bobby Woolf (Addison-Wesley, 2003). eaipatterns.com — O catálogo canônico de padrões de mensageria: Message Channel, Message Router, Dead Letter Channel, Competing Consumers, Correlation Identifier. Disponível gratuitamente no site. Leitura obrigatória para quem projeta sistemas assíncronos — os padrões são agnósticos de tecnologia e aplicam-se a qualquer broker.
  2. livro Designing Data-Intensive Applications — Martin Kleppmann (O'Reilly, 2017). Capítulo 11 — "Stream Processing": cobre message brokers (RabbitMQ, Kafka), garantias de entrega (at-least-once, exactly-once), e log-based message brokers. Explica por que "exactly-once" é uma abstração leaky em sistemas distribuídos e o que a idempotência resolve na prática.
  3. docs RabbitMQ Documentation — Broadcom. rabbitmq.com/docs — Referência completa de exchanges, bindings, políticas, prefetch, quorum queues (HA), e DLQ. O tutorial "Work Queues" e "Publish/Subscribe" cobrem os padrões principais. Guia de clustering e mirroring é essencial para alta disponibilidade em produção.
  4. docs Amazon SQS Developer Guide — AWS. docs.aws.amazon.com/sqs — Documentação oficial de Standard vs FIFO Queue, visibility timeout, DLQ com redrive policy, message deduplication (FIFO), e integração com Lambda (event source mapping), SNS (fanout) e EventBridge.
  5. docs MassTransit Documentation — MassTransit Project. masstransit.io/docs — Framework de message bus para .NET: suporte a RabbitMQ, SQS, Azure Service Bus e outros com API unificada. Cobre retry com backoff exponencial, DLQ automático, sagas (long-running transactions), e routing slips. Abstração de alto nível que elimina código de infraestrutura nos consumidores.
  6. docs KEDA Documentation — KEDA Project / CNCF. keda.sh/docs — Kubernetes Event-Driven Autoscaling: escalador de Deployments baseado em métricas externas. Scalers para RabbitMQ, SQS, Kafka, Azure Service Bus e outros. Essencial para escalar workers de fila automaticamente sem HPA customizado.
  7. artigo Exactly-Once Semantics Are Possible: Here's How Kafka Does It — Neha Narkhede (Confluent, 2017). confluent.io/blog — Explica por que exactly-once é tecnicamente difícil, e como Kafka resolve com producers idempotentes e transações. Contexto essencial para entender por que at-least-once + idempotência é a abordagem correta para a maioria dos sistemas com outros brokers.
  8. artigo You Cannot Have Exactly-Once Delivery — Tyler Treat (2015). bravenewgeek.com/you-cannot-have-exactly-once-delivery — Explica formalmente por que exactly-once delivery é impossível em sistemas distribuídos sem consenso, e por que "exactly-once processing" (diferente de delivery) é o que sistemas reais precisam. Leitura essencial antes de qualquer discussão sobre garantias de entrega.
  9. artigo Transactional Outbox Pattern — Chris Richardson (microservices.io). microservices.io/patterns/data/transactional-outbox.html — Padrão para garantir atomicidade entre escrita no banco e publicação de mensagem: salvar a mensagem na mesma transação do banco (tabela outbox) e publicar via polling ou CDC. Resolve o problema de "publicar no broker e salvar no banco" que pode falhar a meio caminho.
  10. standard AMQP 0-9-1 Specification — OASIS / RabbitMQ. rabbitmq.com/amqp-0-9-1-reference.html — Especificação do protocolo AMQP implementado pelo RabbitMQ: frames, channels, exchanges, queues, bindings e métodos (basic.publish, basic.ack, basic.nack). Referência para entender o comportamento exato de confirmações e o que acontece em cada etapa do ciclo de vida de uma mensagem.
  11. docs Azure Service Bus Documentation — Microsoft. learn.microsoft.com/azure/service-bus-messaging — Documentação do Azure Service Bus: queues, topics/subscriptions (equivalente ao exchange topic do RabbitMQ), sessions (ordering por group), DLQ, e integração com Azure Functions e Logic Apps. Alternativa gerenciada ao RabbitMQ no ecossistema Microsoft.
  12. artigo Competing Consumers Pattern — Microsoft Architecture Center. learn.microsoft.com/azure/architecture/patterns/competing-consumers — Descreve o padrão de múltiplos consumidores concorrentes lendo da mesma fila: benefícios (escala horizontal automática, resiliência), desafios (ordenação, idempotência), e como implementar graceful shutdown para não perder mensagens em processamento durante scale-down.