# Día 23: RabbitMQ, Patrones de Mensajería y Comparativa Estratégica con Kafka

> Exchanges, Queues, Bindings, Routing Keys, AMQP 0-9-1, Spring AMQP y Cuándo Elegir Cada Uno

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 23 de 30
- Fase 6: Mensajería Asíncrona con Kafka, RabbitMQ, EDA y CQRS
- Dedicación: 3.5h
- Profundidad: visión de conjunto
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/rabbitmq-vs-kafka/

## Al terminar este día sabrás

- Enrutar mensajes con exchanges, bindings y routing keys en RabbitMQ.
- Controlar la entrega con acknowledgements y prefetch.
- Desviar mensajes fallidos a un Dead Letter Exchange y reintentar con retardo.
- Elegir con argumentos entre RabbitMQ y Kafka para un caso concreto.


## Conceptos

### 1. Modelo AMQP: Exchange, Queue, Binding y Routing Key

En RabbitMQ, los productores NUNCA envían mensajes directamente a una cola. Publican en un **Exchange** (buzón de enrutamiento). El Exchange usa las **Bindings** (reglas de enrutamiento) y la **Routing Key** del mensaje para decidir a qué **Queue(s)** entregar el mensaje. Tipos de Exchange: `direct` (routing key exacta), `fanout` (broadcast a todas las colas vinculadas), `topic` (patrón con comodines `*` y `#`), `headers` (por atributos del header).

**Cómo funciona por dentro.**

En AMQP los productores nunca publican directamente en una cola. Publican en un exchange con una routing key, y el exchange decide a qué colas copiar el mensaje según sus bindings (reglas que unen exchange y cola). Los consumidores leen de las colas.

Hay cuatro tipos de exchange. `direct` entrega a las colas cuyo binding coincide exactamente con la routing key. `topic` admite patrones con palabras separadas por puntos: `*` sustituye una palabra y `#` cero o más (`tareas.*.completada`, `tareas.#`). `fanout` ignora la clave y copia a todas las colas enlazadas. `headers` decide por cabeceras del mensaje.

Esta capa de enrutamiento en el broker es la gran fortaleza de RabbitMQ: puedes añadir un nuevo consumidor interesado solo en ciertos eventos creando una cola y un binding, sin tocar al productor.

*Topología declarada con Spring AMQP*

```java
@Configuration
class RabbitTopologia {

    @Bean
    TopicExchange tareasExchange() {
        return new TopicExchange("tareas");                      // durable por defecto
    }

    @Bean
    Queue notificacionesQueue() {
        return QueueBuilder.durable("tareas.notificaciones").quorum().build();
    }

    @Bean
    Queue auditoriaQueue() {
        return QueueBuilder.durable("tareas.auditoria").quorum().build();
    }

    @Bean
    Binding soloCompletadas(Queue notificacionesQueue, TopicExchange tareasExchange) {
        // tareas.<proyecto>.completada  ->  notificaciones
        return BindingBuilder.bind(notificacionesQueue).to(tareasExchange).with("tareas.*.completada");
    }

    @Bean
    Binding todosLosEventos(Queue auditoriaQueue, TopicExchange tareasExchange) {
        return BindingBuilder.bind(auditoriaQueue).to(tareasExchange).with("tareas.#");
    }
}
```

- **En una línea:** `rabbitTemplate.convertAndSend("ordenes.exchange", "orden.creada", evento);`
- **Por qué importa:** Permite patrones de mensajería sofisticados (pub/sub, routing selectivo, broadcast) sin modificar el código del productor.
- **Error típico:** Publicar directamente a una cola usando el Exchange por defecto (`""`) y acoplar el productor al nombre exacto de la cola del consumidor.
- **Java 17 frente a 21:** Independiente de Java.
- **Se conecta con:** Patrón Pub/Sub, Routing, Desacoplamiento

### 2. Acknowledgements y Prefetch: Control de Flujo del Consumidor

Cuando un consumidor recibe un mensaje, debe confirmar (acknowledge) que lo ha procesado correctamente. En modo `MANUAL`, el consumidor llama a `channel.basicAck()` explícitamente tras procesar el mensaje con éxito. Si algo falla, puede rechazarlo con `basicNack()` para que RabbitMQ lo reencole o lo envíe a una Dead Letter Queue. El **prefetch** controla cuántos mensajes no confirmados puede tener el consumidor simultáneamente.

**Cómo funciona por dentro.**

Un acknowledgement (ack) es la confirmación del consumidor de que procesó un mensaje; solo entonces RabbitMQ lo borra de la cola. Si el consumidor rechaza el mensaje (`nack` o `reject`), puede pedir que se reencole o que se descarte (o se desvíe a un DLX). Si la conexión se cae con mensajes sin confirmar, RabbitMQ los reencola automáticamente para otro consumidor.

El prefetch (QoS) limita cuántos mensajes sin confirmar puede tener un consumidor a la vez. Con prefetch 1, RabbitMQ no envía el siguiente hasta recibir el ack del anterior: reparto justo entre consumidores lentos y rápidos, pero poco rendimiento. Con valores altos, más rendimiento, pero un consumidor puede acaparar mensajes que otro procesaría antes. Spring usa 250 por defecto.

En Spring AMQP, el modo `AUTO` confirma cuando tu método termina sin excepción y hace `nack` si lanza una. El modo `MANUAL` te da el `Channel` para confirmar tú mismo, útil cuando el procesamiento continúa de forma asíncrona.

*Prefetch y confirmación manual*

```java
// application.yml
// spring:
//   rabbitmq:
//     listener:
//       simple:
//         acknowledge-mode: manual
//         prefetch: 10            # como máximo 10 mensajes sin confirmar por consumidor
//         concurrency: 2
//         max-concurrency: 8

@RabbitListener(queues = "tareas.notificaciones")
void alRecibir(TareaEvento evento, Channel canal,
               @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
    try {
        notificador.avisar(evento);
        canal.basicAck(tag, false);                    // procesado: RabbitMQ lo borra
    } catch (ServicioCaidoException e) {
        canal.basicNack(tag, false, true);             // transitorio: vuelve a la cola
    } catch (Exception e) {
        canal.basicNack(tag, false, false);            // permanente: al DLX si está configurado
    }
}
```

- **En una línea:** `spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.rabbitmq.listener.simple.prefetch=10`
- **Por qué importa:** Previene la pérdida de mensajes si el consumidor falla a mitad de procesamiento y controla la presión sobre consumidores lentos.
- **Error típico:** Usar modo de confirmación automática (`AUTO`) en producción, donde el mensaje se elimina de la cola inmediatamente al ser entregado sin verificar que el procesamiento fue exitoso.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Backpressure, Fiabilidad, Control de Concurrencia

### 3. Dead Letter Exchanges (DLX) en RabbitMQ

Cuando un mensaje es rechazado (`nack` sin requeue), ha expirado por TTL, o la cola destino ha alcanzado su límite de longitud, RabbitMQ redirige ese mensaje a un **Dead Letter Exchange** (DLX) vinculado a una cola de errores para inspección, alerta o reprocesamiento automático.

**Cómo funciona por dentro.**

Un Dead Letter Exchange es un exchange normal al que RabbitMQ reenvía los mensajes «muertos» de una cola. Se configura en la cola con el argumento `x-dead-letter-exchange` (y opcionalmente `x-dead-letter-routing-key`). Un mensaje muere cuando se rechaza sin reencolar, cuando caduca su TTL o cuando la cola supera su longitud máxima.

RabbitMQ añade la cabecera `x-death` con el motivo, la cola de origen y cuántas veces ha muerto, lo que permite analizar fallos o decidir si reintentar.

Un patrón clásico es el reintento con retardo: la cola principal desvía los fallos a una cola de espera sin consumidores con un TTL (por ejemplo, 30 s), cuyo DLX devuelve el mensaje a la cola principal. Tras varios intentos (contando en `x-death`), se envía a una cola de aparcamiento para revisión manual.

*Cola principal, cola de espera y aparcamiento*

```java
@Bean
Queue principal() {
    return QueueBuilder.durable("pagos.procesar")
        .deadLetterExchange("pagos.dlx")
        .deadLetterRoutingKey("espera")
        .build();
}

@Bean
Queue espera() {                                      // sin consumidores: solo espera
    return QueueBuilder.durable("pagos.espera")
        .ttl(30_000)                                  // 30 s
        .deadLetterExchange("pagos")                  // al caducar, vuelve al exchange principal
        .deadLetterRoutingKey("procesar")
        .build();
}

@Bean
Queue aparcados() {
    return QueueBuilder.durable("pagos.aparcados").build();
}

@RabbitListener(queues = "pagos.procesar")
void procesar(Message mensaje, Pago pago) {
    long intentos = Optional.ofNullable(mensaje.getMessageProperties().getXDeathHeader())
        .map(d -> d.stream().mapToLong(x -> (Long) x.get("count")).sum()).orElse(0L);
    if (intentos >= 3) {
        rabbit.convertAndSend("", "pagos.aparcados", pago);  // revisión manual
        return;
    }
    pasarela.cobrar(pago);                            // si lanza excepción -> DLX -> espera 30 s
}
```

- **En una línea:** `@Queue(value = "ordenes", arguments = @Argument(name = "x-dead-letter-exchange", value = "ordenes.dlx"))`
- **Por qué importa:** Proporciona un mecanismo robusto de manejo de fallos sin perder mensajes ni bloquear colas principales.
- **Error típico:** No configurar un DLX y que los mensajes fallidos se reencoleen en bucle infinito, saturando la cola y consumiendo CPU.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Resiliencia, Monitorización, Retry Patterns

### 4. Cuándo Elegir RabbitMQ y Cuándo Elegir Kafka

RabbitMQ excelle en: colas de trabajo punto a punto, routing complejo con exchanges y bindings, mensajes con TTL y prioridades, confirmación individual de cada mensaje. Kafka excelle en: streaming de eventos de alto throughput, retención de histórico, reproducción de eventos, integración de múltiples consumidores independientes con Consumer Groups, y sistemas de Event Sourcing.

**Cómo funciona por dentro.**

RabbitMQ es un broker inteligente con consumidores sencillos: enruta mensajes con reglas flexibles, entrega cada mensaje a un consumidor, lleva la cuenta de confirmaciones mensaje a mensaje y lo borra al confirmarse. Destaca en colas de trabajo, enrutamiento complejo, prioridades, TTL por mensaje y patrones petición-respuesta.

Kafka es un log con consumidores inteligentes: guarda todo durante un tiempo, cada consumidor lleva su posición y puede releer. Destaca en volumen muy alto, en que muchos sistemas consuman los mismos eventos de forma independiente, en reprocesar el histórico y en procesamiento de flujos (Kafka Streams, Flink). Ordena por partición.

Las preguntas que deciden: ¿necesito releer o que lleguen nuevos consumidores al histórico? (Kafka). ¿Necesito enrutamiento flexible, colas de tareas con reintentos por mensaje o prioridades? (RabbitMQ). ¿Cientos de miles de mensajes por segundo de forma sostenida? (Kafka). Muchos sistemas usan ambos: Kafka para eventos de dominio y RabbitMQ para colas de trabajo.

*Comparativa rápida*

```text
                         RabbitMQ                          Kafka
Modelo                   cola con enrutamiento             log particionado
Tras consumir            el mensaje se borra (ack)         se conserva según retención
Releer el pasado         no                                sí (mover offset)
Enrutamiento             exchanges direct/topic/fanout     por topic y clave de partición
Orden                    por cola (con 1 consumidor)       por partición
Reintento por mensaje    nack, DLX, TTL                    error handler, topics de reintento, DLT
Rendimiento típico       decenas de miles msg/s            cientos de miles a millones msg/s
Encaja con               colas de trabajo, RPC, tareas     event streaming, event sourcing, CDC
```

- **En una línea:** `Kafka para flujo de eventos (pedidos, pagos, inventario). RabbitMQ para tareas puntuales (enviar email, generar PDF, notificación push).`
- **Por qué importa:** No existe una herramienta universalmente superior; la elección depende del patrón de comunicación dominante en tu arquitectura.
- **Error típico:** Usar Kafka para una simple cola de tareas en background (e.g. enviar emails) donde RabbitMQ sería mucho más sencillo y adecuado.
- **Java 17 frente a 21:** Independiente de Java.
- **Se conecta con:** Arquitectura de Microservicios, Event Sourcing, Colas de Trabajo

### 5. Spring AMQP: @RabbitListener y RabbitTemplate

Spring AMQP proporciona `RabbitTemplate` como productor de alto nivel y `@RabbitListener` como consumidor declarativo. Soporta conversión automática de mensajes con Jackson (`Jackson2JsonMessageConverter`), reintentos con `RetryTemplate` y configuración declarativa de exchanges, queues y bindings.

**Cómo funciona por dentro.**

Spring AMQP ofrece `RabbitTemplate` para publicar y `@RabbitListener` para consumir, con Spring Boot configurando conexión, listeners y conversión de mensajes. Por defecto los mensajes se serializan con Java, lo que acopla productor y consumidor a las mismas clases; lo recomendable es registrar un `Jackson2JsonMessageConverter` para enviar JSON.

`convertAndSend(exchange, routingKey, objeto)` convierte y publica. Para no perder mensajes por el camino, activa las confirmaciones del publicador (`publisher-confirm-type: correlated`): el broker confirma que recibió y enrutó el mensaje, y puedes reaccionar si no lo hizo.

Para reintentos simples en el consumidor, Spring Boot ofrece reintento con backoff en memoria (`listener.simple.retry`). Al agotar los intentos, con `default-requeue-rejected: false`, el mensaje se rechaza sin reencolar y acaba en el DLX de la cola.

*Publicar y consumir JSON con reintentos*

```java
@Bean
MessageConverter jsonConverter(ObjectMapper mapper) {
    return new Jackson2JsonMessageConverter(mapper);     // Boot lo usa en template y listeners
}

@Service
class PublicadorTareas {
    private final RabbitTemplate rabbit;
    PublicadorTareas(RabbitTemplate rabbit) { this.rabbit = rabbit; }

    void tareaCompletada(Tarea t) {
        var evento = new TareaEvento(t.id(), "COMPLETADA", Instant.now());
        rabbit.convertAndSend("tareas", "tareas." + t.proyecto() + ".completada", evento);
    }
}

// application.yml
// spring:
//   rabbitmq:
//     publisher-confirm-type: correlated
//     listener:
//       simple:
//         default-requeue-rejected: false     # agotados los reintentos -> DLX
//         retry:
//           enabled: true
//           max-attempts: 4
//           initial-interval: 1s
//           multiplier: 2
```

- **En una línea:** `@RabbitListener(queues = "ordenes.creadas") public void procesarOrden(OrdenCreadaEvent evento) { ... }`
- **Por qué importa:** Simplifica enormemente la integración con RabbitMQ eliminando la configuración manual del canal AMQP.
- **Error típico:** No registrar un `MessageConverter` de Jackson y recibir los mensajes como bytes crudos en vez de objetos Java deserializados.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Spring Boot Auto-Configuration, Serialización, Retry Pattern


## Sesión de 90 minutos

### Bloque 1: Modelo AMQP y Tipos de Exchange (0 - 25 min)

- **Qué estudiar:** Exchanges direct, fanout, topic y headers. Bindings y Routing Keys.
- **Pregunta a responder:** ¿Cómo lograr que un mismo evento llegue a 3 servicios diferentes usando un solo exchange?
- **Entregable:** Diagrama de arquitectura con exchange fanout y 3 colas vinculadas.

### Bloque 2: Productor y Consumidor con Spring AMQP (25 - 50 min)

- **Qué estudiar:** `RabbitTemplate`, `@RabbitListener`, `Jackson2JsonMessageConverter`.
- **Pregunta a responder:** ¿Cómo garantizar que un mensaje no se pierde si el consumidor falla?
- **Entregable:** Productor y consumidor funcionales con acknowledge manual.

### Bloque 3: Dead Letter Queues y Comparativa Kafka vs RabbitMQ (50 - 80 min)

- **Qué estudiar:** Configuración de DLX, criterios de selección entre Kafka y RabbitMQ.
- **Pregunta a responder:** ¿En qué 3 escenarios es mejor Kafka y en qué 3 es mejor RabbitMQ?
- **Entregable:** Tabla de decisión Kafka vs RabbitMQ según caso de uso.

### Cierre y Consolidación (80 - 90 min)

- **Qué estudiar:** Repaso de las reglas de oro de mensajería asíncrona.
- **Pregunta a responder:** ¿Por qué la idempotencia es obligatoria en consumidores de mensajes?
- **Entregable:** Notas consolidadas en la app.


## Explícalo desde cero

### El Exchange como director de orquesta: el productor solo sabe a quién enviar, no quién escucha

- **Qué entender:** El productor publica mensajes a un Exchange con una routing key. No sabe ni le importa cuántas colas están vinculadas ni cuántos consumidores existen. Esto permite añadir o quitar consumidores sin modificar el productor. Es desacoplamiento puro.
- **Qué suele explicarse mal:** Mostrar ejemplos donde el productor envía directamente a una cola por nombre, acoplando productor y consumidor.
- **Ejemplo sencillo:** El Exchange es como la centralita de un hotel: tú llamas a "recepción" (routing key) y la centralita redirige la llamada a la habitación correcta. Si cambian la habitación, el sistema de enrutamiento se actualiza sin que tú tengas que saberlo.
- **Señal de alerta:** Si configuras el productor con el nombre exacto de la cola destino en lugar de con el nombre del exchange y la routing key.


## Preguntas

### Nivel 1 (Básico): ¿Qué ocurre si un consumidor de RabbitMQ con acknowledge manual falla al procesar un mensaje y la conexión se cierra antes de enviar el ack o nack?

RabbitMQ detecta la desconexión, marca el mensaje como no confirmado y lo reencola para su entrega a otro consumidor disponible (o al mismo cuando se reconecte). Este es el mecanismo de fiabilidad fundamental de AMQP.

**Respuesta razonada:** Si la conexión o el canal se cierran con mensajes entregados pero sin confirmar, RabbitMQ los considera no procesados y los vuelve a poner en la cola, en su posición original si es posible. Se entregarán a otro consumidor (o al mismo cuando se reconecte) con la marca `redelivered=true`.

Es la garantía de entrega «al menos una vez»: no se pierde, pero puede procesarse dos veces si el consumidor llegó a hacer su trabajo (por ejemplo, enviar un correo) y cayó justo antes del ack. Por eso los consumidores deben ser idempotentes, por ejemplo registrando el id del mensaje procesado y descartando repetidos.

### Nivel 2 (Intermedio): ¿Por qué un exchange de tipo `fanout` ignora la routing key del mensaje y un exchange de tipo `direct` la requiere exacta?

Un `fanout` distribuye el mensaje a TODAS las colas vinculadas al exchange sin importar la routing key. Un `direct` solo entrega el mensaje a las colas cuyo binding tenga una routing key que coincida EXACTAMENTE con la del mensaje.

**Respuesta razonada:** Porque resuelven problemas distintos. Un `fanout` es una difusión: su propósito es copiar cada mensaje a todas las colas enlazadas, sin filtrar, así que la routing key no aporta nada y se ignora. Encaja con «todos los interesados deben enterarse», como invalidar cachés en todas las instancias.

Un `direct` enruta de forma selectiva: compara la routing key del mensaje con la clave de cada binding y solo entrega si coinciden exactamente. Encaja con distribuir trabajo por tipo (`pdf`, `email`). Si necesitas algo intermedio, con patrones, usas un exchange `topic`.

### Nivel 3 (Entrevista Senior): En un sistema de alta disponibilidad con RabbitMQ en cluster, ¿qué diferencia hay entre una cola clásica, una cola en espejo (mirrored queue) y una cola tipo Quorum Queue?

Las colas clásicas viven en un solo nodo; si cae, los mensajes se pierden. Las mirrored queues replican la cola en múltiples nodos pero con un algoritmo de consenso frágil (deprecated). Las Quorum Queues (recomendadas desde RabbitMQ 3.8+) usan el algoritmo Raft para consenso distribuido robusto, garantizando que los mensajes sobrevivan a la caída de nodos sin inconsistencias.

**Respuesta razonada:** Una cola clásica vive en un único nodo: si ese nodo cae, la cola no está disponible y, si se pierde el disco, sus mensajes también. Las colas clásicas en espejo (mirrored) replicaban la cola en varios nodos, pero su algoritmo de sincronización tenía problemas conocidos de pérdida de datos y de rendimiento; quedaron obsoletas y se eliminaron en RabbitMQ 4.0.

Las Quorum Queues son la opción recomendada para datos importantes: se replican en varios nodos con el algoritmo de consenso Raft, un mensaje se confirma cuando lo tiene la mayoría de réplicas, y si cae el líder se elige otro sin perder mensajes confirmados. Son siempre durables y tienen límites de entrega integrados para mensajes envenenados. Su coste es algo más de latencia y de uso de disco.


## Reto del tutor

**Reto:** Tu equipo discute si usar Kafka o RabbitMQ para un nuevo sistema de procesamiento de pagos. Los requisitos son: (1) Cada pago debe procesarse EXACTAMENTE una vez; (2) Si el servicio de pagos falla, el mensaje debe reenviarse automáticamente; (3) No se necesita historial de pagos; (4) Se procesan ~500 pagos por segundo en hora punta. ¿Qué herramienta recomendarías y por qué?

- **Lo que está bien:** Reconoce que RabbitMQ encaja mejor para este caso de uso.
- **Respuesta superficial:** Decir "Kafka porque es más moderno".
- **Lo que falta de fondo:** No evaluar que 500 msg/s es tráfico moderado, que no se necesita historial, y que el modelo de cola con ack/nack de RabbitMQ ofrece semántica más simple para "procesamiento exactamente una vez" en un patrón de cola de trabajo punto a punto.
- **Respuesta senior:** RabbitMQ es la opción más adecuada para este escenario: (1) El patrón es una cola de trabajo punto a punto, no un flujo de eventos multi-consumidor; (2) RabbitMQ ofrece acknowledge manual con requeue/DLX nativo perfecto para reintentos fiables; (3) 500 msg/s está dentro del rango cómodo de RabbitMQ sin necesitar la complejidad de Kafka; (4) "Exactamente una vez" se logra combinando acks manuales con procesamiento idempotente en la base de datos del servicio; (5) No necesitan retención de historial ni reproducción de eventos, que son las fortalezas de Kafka. Kafka añadiría complejidad operativa innecesaria para este patrón.


## Katas

### Kata 23.1: Exchange Fanout con Múltiples Consumidores

Configura un exchange fanout, vincula 2 colas distintas, publica un evento y verifica que ambos consumidores (`@RabbitListener`) reciben el mismo mensaje de forma independiente.


## Reglas de oro

- Publica siempre a un Exchange con routing key, nunca directamente a una cola por nombre.
- Usa acknowledge manual en producción para no perder mensajes ante fallos del consumidor.
- Configura siempre un Dead Letter Exchange (DLX) para capturar mensajes fallidos.
- Para tareas puntuales (emails, PDFs), RabbitMQ es más sencillo y adecuado que Kafka.
- Para streaming de eventos con múltiples consumidores independientes, elige Kafka.


## Proyecto guiado «Pedidos»: Cola de facturas con RabbitMQ

Repartir trabajo pesado entre varios consumidores sin perder tareas.

**Tareas:**

- Al pagar un pedido, encola una tarea `GenerarFactura` en RabbitMQ.
- Crea un consumidor con acknowledgement manual que simula generar la factura.
- Configura un Dead Letter Exchange para las facturas que fallan tres veces.

**Está listo cuando:**

- Con dos consumidores arrancados, las facturas se reparten entre ellos.
- Si un consumidor se cae a mitad de una factura, otro la termina.


## Fuentes oficiales

- [Tutoriales de RabbitMQ](https://www.rabbitmq.com/tutorials)
- [RabbitMQ: streams](https://www.rabbitmq.com/docs/streams)
- [Spring AMQP: referencia](https://docs.spring.io/spring-amqp/reference/)

