Event-Driven Architecture com RabbitMQ: Do Conceito à Produção
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 mensagemSetup 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 recentesEDA 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.