Saltar para o conteúdo
Event-Driven Architecture com RabbitMQ: Do Conceito à Produção
Arquitetura de Software

Event-Driven Architecture com RabbitMQ: Do Conceito à Produção

1 de novembro de 2024·Paulo de Paula

Event-Driven Architecture (EDA) desacopla serviços ao substituir chamadas síncronas por mensagens assíncronas. Em vez de o serviço A chamar o serviço B diretamente, A publica um evento e B (e C, e D) reagem quando conveniente.

Quando usar EDA

Bom fit:

  • Processamento que pode ser assíncrono (envio de email, geração de relatório)
  • Comunicação entre domínios diferentes
  • Workloads com picos de carga (fila absorve a pressão)
  • Múltiplos consumidores para o mesmo evento

Mau fit:

  • Operações que precisam de resposta imediata (login, checkout)
  • Fluxos simples sem necessidade de desacoplamento
  • Times pequenos sem experiência em sistemas distribuídos

Conceitos do RabbitMQ

Producer → Exchange → (Binding) → Queue → Consumer
  • Exchange: roteador de mensagens. Decide para qual fila enviar
  • Queue: buffer persistente de mensagens
  • Binding: regra que conecta exchange à fila (routing key)

Tipos de exchange

Direct Exchange:   routing_key === queue_name (1:1)
Fanout Exchange:   broadcast para todas as filas ligadas (1:N)
Topic Exchange:    padrão com wildcards (user.# ou *.created)
Headers Exchange:  baseado em headers da mensagem

Setup com Node.js

// lib/rabbitmq.js
import amqp from 'amqplib';

let connection = null;
let channel = null;

export async function getChannel() {
  if (channel) return channel;
  
  connection = await amqp.connect(process.env.RABBITMQ_URL);
  channel = await connection.createChannel();
  
  // Prefetch: quantas mensagens não confirmadas por consumer
  await channel.prefetch(10);
  
  connection.on('error', (err) => {
    console.error('RabbitMQ connection error:', err);
    connection = null;
    channel = null;
  });
  
  return channel;
}

Publicando eventos

// publishers/userPublisher.js
import { getChannel } from '../lib/rabbitmq.js';

const EXCHANGE = 'user.events';

export async function setupUserExchange() {
  const ch = await getChannel();
  await ch.assertExchange(EXCHANGE, 'topic', { durable: true });
}

export async function publishUserCreated(user) {
  const ch = await getChannel();
  
  const event = {
    eventType: 'user.created',
    version: '1.0',
    timestamp: new Date().toISOString(),
    data: {
      userId: user.id,
      email: user.email,
      name: user.name,
    },
  };
  
  ch.publish(
    EXCHANGE,
    'user.created',           // routing key
    Buffer.from(JSON.stringify(event)),
    {
      contentType: 'application/json',
      persistent: true,       // sobrevive a restart do RabbitMQ
      messageId: crypto.randomUUID(),
    }
  );
}

Consumindo com Dead Letter Queue

Dead Letter Queue (DLQ) é a parte que a maioria dos tutoriais ignora. Mensagens que falham repetidamente precisam de um destino — sem DLQ, elas ficam presas na fila principal para sempre.

// consumers/emailConsumer.js
import { getChannel } from '../lib/rabbitmq.js';

export async function startEmailConsumer() {
  const ch = await getChannel();
  
  // 1. Declara a Dead Letter Exchange
  await ch.assertExchange('dlx.email', 'direct', { durable: true });
  await ch.assertQueue('email.dead-letter', {
    durable: true,
    arguments: {
      'x-message-ttl': 7 * 24 * 60 * 60 * 1000, // 7 dias
    }
  });
  await ch.bindQueue('email.dead-letter', 'dlx.email', 'email.send');
  
  // 2. Declara a fila principal com DLX configurada
  await ch.assertQueue('email.send', {
    durable: true,
    arguments: {
      'x-dead-letter-exchange': 'dlx.email',
      'x-dead-letter-routing-key': 'email.send',
      'x-max-retries': 3,           // não nativo — controle no consumer
    }
  });
  
  // 3. Faz bind na exchange de eventos
  await ch.assertExchange('user.events', 'topic', { durable: true });
  await ch.bindQueue('email.send', 'user.events', 'user.created');
  await ch.bindQueue('email.send', 'user.events', 'user.#'); // qualquer user.*
  
  // 4. Consome com ack manual
  ch.consume('email.send', async (msg) => {
    if (!msg) return;
    
    const event = JSON.parse(msg.content.toString());
    const retryCount = (msg.properties.headers?.['x-retry-count'] || 0);
    
    try {
      await sendWelcomeEmail(event.data);
      ch.ack(msg);  // remove da fila
      
    } catch (error) {
      console.error(`Erro ao processar ${event.eventType}:`, error);
      
      if (retryCount < 3) {
        // Recoloca com delay e incrementa contador
        ch.nack(msg, false, false); // envia para DLQ
        setTimeout(() => {
          ch.publish('user.events', 'user.created',
            msg.content,
            {
              ...msg.properties,
              headers: { 'x-retry-count': retryCount + 1 }
            }
          );
        }, Math.pow(2, retryCount) * 1000); // backoff exponencial
      } else {
        // Esgotou retentativas — vai para dead letter
        ch.nack(msg, false, false);
      }
    }
  }, { noAck: false });
}

Idempotência: processar duas vezes deve ser seguro

Mensagens podem ser entregues mais de uma vez (network failures, consumer crash). Seu handler deve ser idempotente:

async function sendWelcomeEmail(user) {
  // Verifica se já processou essa mensagem
  const processed = await redis.get(`email:welcome:${user.userId}`);
  if (processed) {
    console.log(`Email já enviado para ${user.userId}, ignorando`);
    return;
  }
  
  await emailService.send({
    to: user.email,
    template: 'welcome',
    data: user,
  });
  
  // Marca como processado (expira em 24h)
  await redis.setex(`email:welcome:${user.userId}`, 86400, '1');
}

Monitoramento com Management Plugin

# Acessa via browser: http://localhost:15672
# user: guest / senha: guest

# Métricas importantes:
# - Messages Ready: acumulando = consumer lento ou morto
# - Messages Unacked: processando agora
# - Dead Letter Queue: falhas recentes

EDA aumenta a complexidade operacional. Antes de adotar, certifique-se de ter observabilidade (rastreamento de mensagens, alertas em DLQ) e capacidade de reprocessar eventos históricos. O debugging de sistemas event-driven requer ferramentas diferentes do monolito síncrono.