Advanced

RabbitMQ: patrones y casos de uso en sistemas distribuidos

Resumen práctico de RabbitMQ: exchanges, colas, patrones de mensajería y cómo se usa para sincronizar microservicios y sostener transacciones críticas como pagos y banca.

Marcos Dionel

Marcos Dionel

RabbitMQMicroserviciosArquitecturaMensajeríaBackend
RabbitMQ: patrones y casos de uso en sistemas distribuidos

Trabajo con RabbitMQ en el día a día en una arquitectura de microservicios donde cada servicio tiene su propia base de datos con muchas tablas. Cuando un servicio necesita información de otro, en vez de acoplarlos con llamadas síncronas, emitimos eventos por RabbitMQ: varias colas consumen esos mensajes y rellenan las tablas locales del servicio receptor para mantenerlo sincronizado.

Esa es apenas una de las formas de usar RabbitMQ. En esta guía te dejo un resumen de los patrones más importantes y cómo se aplican en escenarios reales, incluyendo sistemas de pagos y banca, donde perder o duplicar un mensaje no es una opción.

¿Qué es RabbitMQ?

RabbitMQ es un message broker: un intermediario que recibe mensajes de un productor (publisher) y los entrega a uno o varios consumidores (consumers), sin que ambos lados se conozcan ni tengan que estar disponibles al mismo tiempo.

Hay tres conceptos que tenés que tener claros sí o sí:

  • Exchange: recibe los mensajes del productor y decide a qué colas enrutarlos.
  • Queue: almacena los mensajes hasta que un consumidor los procesa.
  • Binding: la regla que conecta un exchange con una o varias colas.
Arquitectura básica de RabbitMQ
Arquitectura básica de RabbitMQ

Algo clave que mucha gente pasa por alto: un productor nunca envía un mensaje directo a una cola. Siempre pasa primero por un exchange, y es el tipo de exchange el que define el patrón de comunicación que vas a usar.

Los 4 tipos de exchange

  1. Direct: enruta el mensaje a la cola cuya routing key coincide exactamente. Ideal para distribuir tareas.
  2. Fanout: ignora la routing key y envía el mensaje a todas las colas conectadas. Ideal para broadcast de eventos.
  3. Topic: enruta usando patrones sobre la routing key (* para una palabra, # para cero o más). Ideal para que cada consumidor elija qué le interesa.
  4. Headers: enruta según los headers del mensaje en vez de la routing key. Se usa poco, pero existe para casos donde la lógica de ruteo es más compleja que una simple key.

Patrón Work Queues: distribuir tareas pesadas

Cuando tenés una tarea costosa (procesar una imagen, generar un PDF, enviar un email masivo) no querés que la resuelva un solo proceso. Con un exchange direct y varios consumidores escuchando la misma cola, RabbitMQ reparte los mensajes en round-robin entre los workers disponibles.

await channel.assertQueue("image-processing", { durable: true });

channel.sendToQueue("image-processing", Buffer.from(JSON.stringify(payload)), {
  persistent: true,
});

Acá es clave usar prefetch(1) en cada consumidor y confirmación manual (ack). Si no lo hacés, RabbitMQ puede entregarle diez mensajes de golpe a un worker que todavía está procesando el primero, y si ese worker se cae, perdés el trabajo en curso.

Patrón Publish/Subscribe: sincronizar microservicios

Este es el caso que más uso en el trabajo. Un servicio emite un evento de dominio (order.created, user.updated) contra un exchange fanout o topic, y varios servicios totalmente independientes —cada uno con su propia cola— reciben una copia del mismo mensaje.

await channel.assertExchange("orders", "topic", { durable: true });

channel.publish("orders", "order.created", Buffer.from(JSON.stringify(order)), {
  persistent: true,
});

Por ejemplo: el servicio de Órdenes emite order.created. El servicio de Facturación y el de Envíos tienen cada uno su propia cola bindeada a ese exchange, y al consumir el evento actualizan sus propias tablas con la información que necesitan. No hay llamadas HTTP entre servicios, no hay acoplamiento directo, y si un servicio está caído, sus mensajes quedan esperando en la cola hasta que vuelve.

Publish/Subscribe entre microservicios
Publish/Subscribe entre microservicios

Patrón Routing y Topics: filtrar qué escucha cada servicio

Con un exchange topic podés ser mucho más selectivo que con un fanout. Un binding con payment.* escucha payment.created y payment.failed, pero no payment.created.eu.retry. Un binding con payment.# escucha cualquier cosa que empiece con payment..

Esto te permite que un mismo exchange sirva eventos muy distintos, y que cada consumidor decida con precisión qué le interesa sin tener que filtrar mensajes que no le corresponden dentro de su propio código.

RPC: request/reply cuando necesitás una respuesta

RabbitMQ está pensado para comunicación asíncrona, pero también se puede simular un patrón de request/reply: el productor crea una cola exclusiva y temporal, la indica en el header reply-to junto con un correlationId, y el consumidor le responde publicando en esa cola.

Lo uso poco porque agrega acoplamiento temporal (el productor queda esperando una respuesta), pero sirve para casos puntuales, como validar sincrónicamente el estado de un pago antes de continuar con el flujo.

RabbitMQ en pagos y banca

Acá es donde RabbitMQ deja de ser “una cola de mensajes” y pasa a sostener transacciones donde un error cuesta dinero real. Las prácticas que marcan la diferencia:

  • Durabilidad: exchanges y colas declarados como durable, y mensajes marcados como persistent, para que sobrevivan a un reinicio del broker.
  • Publisher confirms: el productor espera la confirmación de que RabbitMQ recibió el mensaje antes de dar por completada la transacción de origen.
  • Ack manual: un mensaje solo se marca como procesado cuando realmente se aplicó del lado del consumidor, evitando pérdidas si el proceso se cae a mitad de camino.
  • Dead Letter Exchange (DLX): los mensajes que fallan repetidamente (un pago que no puede procesarse) se redirigen a una cola de “cartas muertas” para revisión manual, en vez de perderse o bloquear la cola principal.
  • Idempotencia: RabbitMQ garantiza entrega at-least-once, no exactly-once. Cada consumidor tiene que ser idempotente, usando un id de transacción único para detectar duplicados y no procesar dos veces el mismo pago.
  • Transactional outbox: en vez de escribir en la base de datos y publicar el evento como dos pasos separados (con riesgo de inconsistencia si el proceso muere en el medio), el evento se guarda en una tabla outbox dentro de la misma transacción, y un proceso aparte lo publica a RabbitMQ.
Dead Letter Exchange en un flujo de pagos
Dead Letter Exchange en un flujo de pagos

Alta disponibilidad: quorum queues

En sistemas financieros no te podés dar el lujo de perder mensajes porque se cayó un nodo. Para eso existen las quorum queues: colas replicadas entre varios nodos del cluster usando el algoritmo de consenso Raft. Si un nodo se cae, otro nodo con la réplica sigue sirviendo la cola sin pérdida de datos. Reemplazan a las viejas colas espejadas (mirrored queues), que ya están deprecadas.

Buenas prácticas que aprendí en el camino

  • Prefetch bajo en colas críticas (1 o 10, según el caso) para no saturar un consumidor y perder trabajo si se cae a mitad de un lote.
  • Ack manual siempre que la pérdida de un mensaje sea inaceptable para el negocio.
  • TTL en eventos que dejan de ser relevantes pasado un tiempo, para no acumular mensajes indefinidamente.
  • Alternate exchange para capturar mensajes que no matchean ningún binding, en vez de perderlos silenciosamente.
  • Monitorear el largo de las colas. Una cola que crece sin parar es la primera señal de que un consumidor está caído o es más lento que la tasa de publicación.

RabbitMQ es mucho más que una cola de mensajes: bien aplicado permite desacoplar microservicios, distribuir carga de trabajo pesada, y sostener flujos críticos como pagos donde perder o duplicar un mensaje no es una opción. Con los patrones que vimos ya tenés una base sólida para elegir el que mejor se adapte a lo que estés construyendo.