--- title: "Patrones de comunicación entre microservicios con RabbitMQ" description: "Compara la comunicación síncrona y asíncrona entre microservicios y monta en Ubuntu 24.04 un API gateway con Nginx, eventos en RabbitMQ y una cola de fallidos." category: "API y Microservicios" language: es source: https://cubepath.com/es/docs/api-y-microservicios/patrones-de-comunicacion-de-microservicios updated: 2026-09-25 --- # Patrones de comunicación entre microservicios con RabbitMQ La forma en que se comunican tus microservicios decide cómo se comportan cuando algo falla: una llamada HTTP encadenada propaga la caída de un servicio a todos los demás, mientras que un evento en una cola espera a que el consumidor vuelva. En esta guía verás los patrones principales (petición/respuesta síncrona, mensajería asíncrona, sagas) y montarás en Ubuntu 24.04 un ejemplo funcional con Docker Compose: un API gateway con Nginx, un servicio de pedidos que publica eventos en RabbitMQ y un servicio de inventario que los consume, con cola de mensajes fallidos (dead letter queue). ## Requisitos previos - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 GB de RAM. - Un usuario no root con privilegios `sudo`. - Docker Engine y el plugin de Docker Compose instalados desde el repositorio oficial de Docker. - Conocimientos básicos de HTTP y JavaScript. No hace falta instalar Node.js en el host: el código se ejecuta en contenedores `node:22-alpine`. ## Qué patrón usar en cada caso Antes de escribir código conviene tener claro qué resuelve cada patrón: | Patrón | Cómo funciona | Úsalo cuando | Riesgo principal | |---|---|---|---| | Petición/respuesta síncrona (REST, gRPC) | El cliente llama y espera la respuesta | Necesitas el resultado para continuar (consultar un precio, validar un token) | Fallos en cascada y latencia acumulada | | Cola de trabajo (point-to-point) | Un productor deja mensajes, un grupo de consumidores los reparte | Tareas en segundo plano: enviar correos, generar PDF | Mensajes duplicados si el consumidor no es idempotente | | Publicación/suscripción de eventos | Un servicio anuncia "ha pasado X" y cualquiera puede suscribirse | Varios servicios reaccionan al mismo hecho (pedido creado) | Consistencia eventual, difícil de depurar sin trazas | | Saga | Secuencia de transacciones locales con acciones de compensación | Una operación de negocio toca varias bases de datos | Estados intermedios visibles y compensaciones mal diseñadas | | API gateway | Punto de entrada único que enruta a cada servicio | Clientes externos que no deben conocer la topología interna | Punto único de fallo si no se replica | La regla práctica: usa llamadas síncronas solo cuando el llamante no puede seguir sin la respuesta, y eventos para todo lo que pueda ocurrir "después". El ejemplo de esta guía combina ambos: el cliente hace un `POST` síncrono al gateway, el servicio de pedidos responde `202 Accepted` en cuanto el evento está guardado en RabbitMQ, y el inventario se actualiza de forma asíncrona. ## Paso 1: Preparar el proyecto y RabbitMQ Crea el directorio del proyecto con una carpeta para el código de los servicios y otra para la configuración del gateway: ```bash mkdir -p ~/microservicios/{app,gateway} cd ~/microservicios ``` Genera una contraseña aleatoria para RabbitMQ en un archivo `.env`, que Docker Compose lee automáticamente. Se usa formato hexadecimal para que no contenga caracteres que haya que escapar en la URL de conexión: ```bash echo "RABBITMQ_PASS=$(openssl rand -hex 16)" > .env chmod 600 .env ``` Crea el archivo de Compose: ```bash nano compose.yaml ``` ```yaml services: rabbitmq: image: rabbitmq:4-management hostname: rabbitmq environment: RABBITMQ_DEFAULT_USER: app RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS} ports: - "127.0.0.1:15672:15672" volumes: - rabbitmq_data:/var/lib/rabbitmq healthcheck: test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped pedidos: image: node:22-alpine working_dir: /app command: node servicio-pedidos.js volumes: - ./app:/app:ro environment: AMQP_URL: amqp://app:${RABBITMQ_PASS}@rabbitmq:5672 depends_on: rabbitmq: condition: service_healthy restart: unless-stopped inventario: image: node:22-alpine working_dir: /app command: node servicio-inventario.js volumes: - ./app:/app:ro environment: AMQP_URL: amqp://app:${RABBITMQ_PASS}@rabbitmq:5672 depends_on: rabbitmq: condition: service_healthy restart: unless-stopped gateway: image: nginx:stable-alpine ports: - "127.0.0.1:8080:80" volumes: - ./gateway/default.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - pedidos restart: unless-stopped volumes: rabbitmq_data: ``` Puntos importantes de este archivo: - `hostname: rabbitmq` fija el nombre del nodo de RabbitMQ; sin él, cada vez que se recrea el contenedor el nodo cambia de nombre y no encuentra los datos del volumen. - La consola de administración (15672) y el gateway (8080) solo se publican en `127.0.0.1`. Docker añade sus propias reglas de iptables y un puerto publicado en todas las interfaces quedaría expuesto aunque UFW lo bloquee. - El puerto AMQP (5672) no se publica: los servicios llegan a RabbitMQ por la red interna de Compose. Levanta solo RabbitMQ para comprobar que arranca: ```bash docker compose up -d rabbitmq docker compose ps ``` Tras unos segundos el estado debe ser `healthy`: ```output NAME IMAGE SERVICE STATUS PORTS microservicios-rabbitmq-1 rabbitmq:4-management rabbitmq Up 20 seconds (healthy) ... ``` ## Paso 2: Declarar la topología de mensajería La topología (exchanges, colas y enlaces) se declara desde el código de forma idempotente: si ya existe con los mismos parámetros, RabbitMQ no hace nada. El diseño es el siguiente: - Exchange `pedidos` de tipo `topic`: el servicio de pedidos publica con claves como `pedido.creado` o `pedido.cancelado`. - Cola `inventario.pedidos`, durable, enlazada con el patrón `pedido.*`. Si mañana otro servicio necesita los mismos eventos, crea su propia cola y la enlaza al mismo exchange. - Exchange `pedidos.dlx` y cola `pedidos.dlq`: los mensajes que el consumidor rechaza acaban aquí en lugar de perderse o de reintentarse en bucle. Instala la librería `amqplib` con un contenedor temporal de Node.js, ejecutándolo con tu usuario para que `node_modules` no quede con propietario root: ```bash echo '{ "name": "microservicios", "private": true }' > app/package.json docker run --rm -u "$(id -u):$(id -g)" -e npm_config_cache=/tmp/.npm \ -v "$PWD/app":/app -w /app node:22-alpine npm install amqplib ``` Crea el módulo compartido que conecta y declara la topología: ```bash nano app/topologia.js ``` ```javascript const amqp = require('amqplib'); async function conectar() { const conexion = await amqp.connect(process.env.AMQP_URL); // Canal con confirmaciones: el broker confirma cada mensaje publicado const canal = await conexion.createConfirmChannel(); await canal.assertExchange('pedidos', 'topic', { durable: true }); await canal.assertExchange('pedidos.dlx', 'fanout', { durable: true }); await canal.assertQueue('pedidos.dlq', { durable: true }); await canal.bindQueue('pedidos.dlq', 'pedidos.dlx', ''); await canal.assertQueue('inventario.pedidos', { durable: true, deadLetterExchange: 'pedidos.dlx', }); await canal.bindQueue('inventario.pedidos', 'pedidos', 'pedido.*'); conexion.on('close', () => { console.error('Conexión con RabbitMQ cerrada, saliendo para que Docker reinicie el servicio'); process.exit(1); }); return canal; } module.exports = { conectar }; ``` Salir del proceso cuando se cierra la conexión es deliberado: con `restart: unless-stopped`, Docker vuelve a arrancar el servicio y este se reconecta limpio, que es más fiable que reimplementar la reconexión a mano. ## Paso 3: Publicar eventos desde el servicio de pedidos El servicio de pedidos expone una API HTTP mínima. Al recibir un pedido publica el evento `pedido.creado` como mensaje persistente, espera la confirmación del broker y solo entonces responde `202 Accepted`. Así el cliente sabe que el pedido no se perderá aunque el inventario esté caído en ese momento. ```bash nano app/servicio-pedidos.js ``` ```javascript const http = require('node:http'); const { randomUUID } = require('node:crypto'); const { conectar } = require('./topologia'); function responder(res, estado, cuerpo) { res.writeHead(estado, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(cuerpo)); } async function main() { const canal = await conectar(); const servidor = http.createServer(async (req, res) => { if (req.method === 'GET' && req.url === '/health') { return responder(res, 200, { status: 'ok' }); } if (req.method !== 'POST' || req.url !== '/') { return responder(res, 404, { detail: 'Ruta no encontrada' }); } let cuerpo = ''; for await (const trozo of req) cuerpo += trozo; let pedido; try { pedido = JSON.parse(cuerpo); } catch { return responder(res, 400, { detail: 'El cuerpo no es JSON válido' }); } const evento = { ...pedido, pedidoId: randomUUID(), creadoEn: new Date().toISOString() }; try { canal.publish('pedidos', 'pedido.creado', Buffer.from(JSON.stringify(evento)), { persistent: true, contentType: 'application/json', messageId: randomUUID(), }); await canal.waitForConfirms(); } catch (err) { console.error(`No se pudo publicar el evento: ${err.message}`); return responder(res, 503, { detail: 'Servicio de mensajería no disponible' }); } console.log(`Pedido ${evento.pedidoId} publicado`); return responder(res, 202, { pedidoId: evento.pedidoId, estado: 'pendiente' }); }); servidor.listen(3000, () => console.log('Servicio de pedidos escuchando en :3000')); } main().catch((err) => { console.error(err); process.exit(1); }); ``` El campo `messageId` es único por mensaje y es lo que el consumidor usará para detectar duplicados. ## Paso 4: Consumir eventos en el servicio de inventario RabbitMQ garantiza entrega "al menos una vez": si el consumidor se cae después de procesar un mensaje pero antes de confirmarlo, el mensaje se vuelve a entregar. Por eso el consumidor debe ser idempotente y confirmar (`ack`) solo cuando ha terminado su trabajo. ```bash nano app/servicio-inventario.js ``` ```javascript const { conectar } = require('./topologia'); // En producción guarda los IDs procesados en la base de datos del servicio, // en la misma transacción que la actualización de stock. const procesados = new Set(); async function main() { const canal = await conectar(); // Como máximo 10 mensajes sin confirmar por consumidor await canal.prefetch(10); await canal.consume('inventario.pedidos', (msg) => { if (msg === null) return; const id = msg.properties.messageId; try { if (procesados.has(id)) { console.log(`Mensaje ${id} duplicado, se ignora`); return canal.ack(msg); } const pedido = JSON.parse(msg.content.toString()); if (!Array.isArray(pedido.items) || pedido.items.length === 0) { throw new Error('el pedido no tiene items'); } console.log(`Reservando stock para el pedido ${pedido.pedidoId}: ${pedido.items.length} líneas`); procesados.add(id); canal.ack(msg); } catch (err) { console.error(`Mensaje ${id} rechazado: ${err.message}`); // requeue=false: el mensaje va al dead letter exchange en lugar de reintentarse en bucle canal.nack(msg, false, false); } }); console.log('Servicio de inventario escuchando pedido.*'); } main().catch((err) => { console.error(err); process.exit(1); }); ``` Un mensaje con datos inválidos nunca se va a procesar bien por mucho que se reintente, así que se envía directamente a `pedidos.dlq` para revisarlo a mano. Para errores transitorios (la base de datos no responde) lo habitual es reintentar con retardo antes de mandarlo a la DLQ, por ejemplo con una cola de espera con TTL. ## Paso 5: Configurar Nginx como API gateway El gateway es el único punto de entrada para los clientes: enruta por prefijo de URL, limita la tasa de peticiones, añade un identificador de petición para poder seguir una llamada en los logs de todos los servicios y fija tiempos de espera cortos para que un servicio lento no bloquee conexiones indefinidamente. ```bash nano gateway/default.conf ``` ```nginx limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s; upstream pedidos_backend { server pedidos:3000; keepalive 16; } server { listen 80; location /api/pedidos/ { limit_req zone=api burst=40 nodelay; limit_req_status 429; proxy_pass http://pedidos_backend/; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Request-ID $request_id; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 2s; proxy_read_timeout 5s; } location = /health { default_type application/json; return 200 '{"status":"ok"}'; } } ``` Como `proxy_pass` termina en `/`, Nginx sustituye el prefijo: una petición a `/api/pedidos/` llega al servicio como `/`. Cuando añadas el servicio de inventario al gateway bastará con otro bloque `upstream` y otra `location /api/inventario/`. Arranca todos los servicios: ```bash docker compose up -d docker compose ps ``` Los cuatro servicios deben aparecer en estado `Up`, con RabbitMQ en `healthy`. Revisa que los dos servicios de Node.js han conectado: ```bash docker compose logs pedidos inventario ``` ```output pedidos-1 | Servicio de pedidos escuchando en :3000 inventario-1 | Servicio de inventario escuchando pedido.* ``` ## Paso 6: Probar el flujo completo Envía un pedido válido a través del gateway: ```bash curl -s -X POST http://127.0.0.1:8080/api/pedidos/ \ -H "Content-Type: application/json" \ -d '{"clienteId": "c-42", "items": [{"sku": "A-100", "cantidad": 2}]}' ``` La respuesta llega en cuanto RabbitMQ confirma el evento, sin esperar al inventario: ```output {"pedidoId":"3f1c9a52-7d7e-4c1b-9e57-0f2a6c1d8b11","estado":"pendiente"} ``` En los logs del inventario verás que ha procesado el evento: ```bash docker compose logs --tail 5 inventario ``` ```output inventario-1 | Reservando stock para el pedido 3f1c9a52-7d7e-4c1b-9e57-0f2a6c1d8b11: 1 líneas ``` Ahora envía un pedido sin líneas, que el inventario rechazará: ```bash curl -s -X POST http://127.0.0.1:8080/api/pedidos/ \ -H "Content-Type: application/json" \ -d '{"clienteId": "c-42", "items": []}' ``` Comprueba el estado de las colas. El mensaje rechazado debe estar en `pedidos.dlq` y la cola del inventario vacía: ```bash docker compose exec rabbitmq rabbitmqctl list_queues name messages messages_unacknowledged ``` ```output Timeout: 60.0 seconds ... Listing queues for vhost / ... name messages messages_unacknowledged inventario.pedidos 0 0 pedidos.dlq 1 0 ``` Por último, comprueba el desacoplamiento, que es la ventaja principal de la mensajería asíncrona. Para el inventario, envía un pedido y vuelve a arrancarlo: ```bash docker compose stop inventario curl -s -X POST http://127.0.0.1:8080/api/pedidos/ \ -H "Content-Type: application/json" \ -d '{"clienteId": "c-7", "items": [{"sku": "B-200", "cantidad": 1}]}' docker compose start inventario docker compose logs --tail 3 inventario ``` El pedido se aceptó con el inventario parado y se procesó en cuanto el servicio volvió. Con una llamada HTTP síncrona entre pedidos e inventario, ese pedido habría fallado. Si quieres inspeccionar mensajes desde el navegador, abre un túnel SSH a la consola de administración con `ssh -L 15672:127.0.0.1:15672 your_user@your_server_ip` y entra en `http://localhost:15672` con el usuario `app` y la contraseña del archivo `.env`. ## Paso 7: Llamadas síncronas resilientes Cuando un servicio sí necesita la respuesta de otro (por ejemplo, consultar el precio actual antes de aceptar un pedido), aplica tres reglas: 1. **Tiempo de espera siempre.** Una llamada sin timeout puede dejar un proceso esperando indefinidamente. 2. **Reintentos solo en operaciones idempotentes** y con espera exponencial más una parte aleatoria (jitter), para que muchos clientes no reintenten a la vez. 3. **Circuit breaker** cuando el servicio remoto falla de forma sostenida: dejar de llamarlo durante un tiempo protege a ambos lados. En Node.js la librería `opossum` implementa este patrón; en una malla de servicios (Istio, Linkerd) se configura en la infraestructura. Este es un ejemplo de consulta con timeout y reintentos usando `fetch`, disponible de serie en Node.js 18 y posteriores: ```javascript async function obtenerConReintentos(url, { intentos = 3, timeoutMs = 2000 } = {}) { for (let intento = 1; intento <= intentos; intento++) { try { const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }); if (res.ok) return await res.json(); // Los 4xx son errores del cliente: reintentar no los arregla if (res.status < 500 && res.status !== 429) { throw Object.assign(new Error(`HTTP ${res.status}`), { definitivo: true }); } } catch (err) { if (err.definitivo || intento === intentos) throw err; } const espera = Math.min(200 * 2 ** intento, 3000) + Math.random() * 100; await new Promise((r) => setTimeout(r, espera)); } throw new Error(`Sin respuesta de ${url} tras ${intentos} intentos`); } ``` Evita cadenas largas de llamadas síncronas (A llama a B, que llama a C, que llama a D): la disponibilidad total es el producto de la de cada servicio y la latencia se suma. ## Paso 8: Transacciones distribuidas con sagas En microservicios cada servicio tiene su propia base de datos, así que no existe una transacción que abarque "cobrar el pedido y reservar el stock". Una saga divide la operación en transacciones locales y define, para cada paso, una acción que lo deshace (compensación). Para el flujo de un pedido: | Paso | Servicio | Evento si va bien | Compensación si un paso posterior falla | |---|---|---|---| | 1. Crear pedido en estado `pendiente` | Pedidos | `pedido.creado` | Marcar el pedido como `cancelado` | | 2. Reservar stock | Inventario | `stock.reservado` | Liberar la reserva (`stock.liberado`) | | 3. Cobrar | Pagos | `pago.completado` | Reembolsar | | 4. Confirmar pedido | Pedidos | `pedido.confirmado` | (último paso, no necesita compensación) | Hay dos formas de coordinarla: - **Coreografía:** cada servicio escucha eventos y publica los suyos. Si pagos publica `pago.rechazado`, inventario libera la reserva y pedidos cancela el pedido. Encaja con la topología de esta guía y no tiene un coordinador central, pero con muchos pasos cuesta seguir el flujo. - **Orquestación:** un orquestador (normalmente dentro del servicio de pedidos) envía comandos a cada servicio, guarda el estado de la saga en su base de datos y lanza las compensaciones en orden inverso. Es más fácil de razonar cuando hay más de tres o cuatro pasos. En ambos casos hay dos detalles que suelen fallar en la práctica: - **Publicar el evento y guardar el cambio en la base de datos no es atómico.** Si el servicio guarda el pedido y se cae antes de publicar, el evento se pierde. El patrón *transactional outbox* lo resuelve: el evento se inserta en una tabla `outbox` en la misma transacción que el cambio, y un proceso aparte lo publica y lo marca como enviado. - **Las compensaciones también se reintentan**, así que deben ser idempotentes igual que los consumidores. ## Solución de problemas **Los servicios de Node.js se reinician en bucle con `ACCESS_REFUSED`.** RabbitMQ solo crea el usuario de `RABBITMQ_DEFAULT_USER` la primera vez que arranca con un volumen vacío. Si cambiaste la contraseña en `.env` después, el volumen conserva la antigua. En un entorno de pruebas puedes borrar el volumen con `docker compose down -v` y volver a levantar el proyecto; en uno real, cambia la contraseña con `docker compose exec rabbitmq rabbitmqctl change_password app nueva_contraseña`. **`PRECONDITION_FAILED - inequivalent arg` al arrancar.** Has cambiado los parámetros de una cola que ya existe (por ejemplo, añadiendo `deadLetterExchange`). RabbitMQ no modifica colas existentes al declararlas; elimínala desde la consola de administración o con `rabbitmqctl delete_queue inventario.pedidos` y reinicia el servicio. **Mensajes en `messages_unacknowledged` que no bajan.** El consumidor recibió los mensajes pero no llama a `ack` ni a `nack`, normalmente por una excepción no capturada o una promesa que nunca se resuelve. Revisa `docker compose logs inventario`; al reiniciar el consumidor, RabbitMQ vuelve a entregar esos mensajes. **El gateway devuelve 502 o 504.** Un 502 indica que Nginx no puede conectar con el servicio (comprueba `docker compose ps` y los logs de `pedidos`); un 504 que el servicio tardó más que `proxy_read_timeout`. Los errores de Nginx aparecen en `docker compose logs gateway`. ## Conclusión Has montado una arquitectura pequeña pero completa: un API gateway que enruta y limita peticiones, un servicio que acepta trabajo de forma síncrona y lo delega mediante eventos persistentes, y un consumidor idempotente con cola de mensajes fallidos. Como siguientes pasos, puedes añadir el patrón outbox con la base de datos real de cada servicio, publicar el gateway en un dominio con HTTPS y añadir trazas distribuidas con OpenTelemetry para seguir cada `X-Request-ID` a través de todos los servicios.