Día 22 de 30 Fase 6: Mensajería Asíncrona con Kafka, RabbitMQ, EDA y CQRS

Mensajería Asíncrona con Apache Kafka y Spring Boot 3

Topics, Partitions, Consumer Groups, Producers, KafkaTemplate y Serialización Avro/JSON

Visión de conjunto. Sales entendiendo cómo funciona y cuándo usarlo; dominarlo pide más práctica en un proyecto real.

  • 4.5h de estudio
  • 5 conceptos
  • 3 preguntas
  • 1 katas
Apache KafkaSpring KafkaTopicsPartitionsConsumer GroupsEvent Streaming
Al terminar este día sabrás 4
  • Explicar Kafka como un log distribuido y qué significa un offset.
  • Diseñar claves y particiones para conservar el orden donde importa.
  • Escalar consumidores con consumer groups y entender los rebalanceos.
  • Producir y consumir con Spring Kafka y gestionar errores con reintentos y DLT.

Kafka como Log Distribuido Inmutable: El Concepto Fundamental

Kafka NO es una cola de mensajes tradicional. Es un log distribuido, inmutable y ordenado (commit log). Cada mensaje publicado se persiste en disco de forma secuencial, asignándole un offset numérico que nunca cambia. Los consumidores leen mensajes indicando desde qué offset quieren empezar, y pueden releer eventos pasados las veces que necesiten.

En una línea kafkaTemplate.send("ordenes-creadas", ordenId.toString(), new OrdenCreadaEvent(ordenId, items));

Ver el log desde la línea de comandos bash
# Crear un topic con 6 particiones y retención de 7 días
kafka-topics.sh --bootstrap-server localhost:9092 --create \
  --topic tareas.eventos --partitions 6 --replication-factor 3 \
  --config retention.ms=604800000

# Leer desde el principio mostrando partición, offset y clave
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic tareas.eventos \
  --from-beginning --property print.partition=true \
  --property print.offset=true --property print.key=true

# Dónde está cada consumer group y cuánto retraso (lag) acumula
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group notificaciones

# Reprocesar: mover el grupo al inicio (con los consumidores parados)
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group notificaciones \
  --topic tareas.eventos --reset-offsets --to-earliest --execute
Cómo funciona por dentro

Kafka no es una cola de la que los mensajes desaparecen al leerlos. Es un log: una secuencia ordenada de registros a la que solo se añade al final. Cada registro tiene una posición numérica, el offset. Los consumidores no borran nada; cada uno recuerda hasta qué offset ha leído y avanza a su ritmo.

Los registros se conservan según la política de retención del topic (por tiempo, por ejemplo 7 días, o por tamaño), independientemente de si alguien los ha leído. Eso permite cosas imposibles en una cola clásica: que varios sistemas lean los mismos eventos de forma independiente, añadir un consumidor nuevo que reprocese el histórico o volver atrás tras corregir un bug moviendo el offset.

Los topics compactados (cleanup.policy=compact) conservan solo el último registro de cada clave, convirtiendo el topic en una tabla de estado actual que se puede reconstruir en cualquier momento.

A diferencia de RabbitMQ (donde los mensajes se eliminan tras ser consumidos), en Kafka los mensajes se retienen durante el período de retención configurado (horas, días o indefinidamente), permitiendo reconstruir el estado de un sistema desde cero.

Se conecta con Event SourcingReplicación de DatosAudit Trail

Cuatro bloques cronometrados. No pases al siguiente sin tener el resultado esperado.

  1. Bloque 1: Fundamentos del Log Distribuido (0 - 25 min)

    Qué estudiar. Conceptos de Topic, Partition, Offset, Retention. Diferencia con colas de mensajes.

    Pregunta a responder. ¿Por qué Kafka persiste los mensajes en disco pero sigue siendo extremadamente rápido?

    Resultado esperado. Diagrama de un Topic con 3 particiones y 2 Consumer Groups leyendo independientemente.

  2. Bloque 2: Productor y Consumidor con Spring Kafka (25 - 50 min)

    Qué estudiar. Configuración de KafkaTemplate, @KafkaListener, serialización JSON con Jackson.

    Pregunta a responder. ¿Cómo garantizar que todos los eventos de un mismo pedido se procesen en orden?

    Resultado esperado. Productor y consumidor funcionales publicando y leyendo eventos JSON de un Topic local.

  3. Bloque 3: Errores, Reintentos y Dead Letter Topics (50 - 80 min)

    Qué estudiar. Configuración de DefaultErrorHandler, DeadLetterPublishingRecoverer y FixedBackOff.

    Pregunta a responder. ¿Cómo evitar que un mensaje envenenado bloquee toda una partición?

    Resultado esperado. Consumidor con política de reintento y DLT configurados y probados.

  4. Cierre y Consolidación (80 - 90 min)

    Qué estudiar. Repaso de las 5 reglas de oro de Kafka y comparativa inicial con RabbitMQ.

    Pregunta a responder. ¿En qué caso elegirías RabbitMQ sobre Kafka?

    Resultado esperado. Notas consolidadas en la aplicación.

Tres formas de comprobar si lo has entendido, de menos a más exigente: explicarlo con tus palabras, responder preguntas trampa y resolver el reto del tutor.

Explícalo desde cero

Lee cada ficha y explícala en voz alta antes de seguir: si puedes contárselo a alguien que empieza, lo has entendido.

Por qué Kafka escala mucho mejor que una cola de mensajes tradicional para alto throughput

Lo que debes entender

Las colas tradicionales (ActiveMQ, RabbitMQ en modo cola) eliminan el mensaje del almacenamiento una vez confirmado por el consumidor. Kafka escribe secuencialmente en disco y nunca elimina mensajes (hasta la expiración del período de retención). Esto permite que los consumidores sean independientes: cada Consumer Group mantiene su propio puntero de offset. Un nuevo microservicio puede unirse al ecosistema y leer todos los eventos históricos desde el principio sin afectar a los demás.

Analogía para contarlo

RabbitMQ es como un repartidor de correo postal: entrega la carta y se va, nadie puede volver a leerla. Kafka es como un periódico digital: el artículo permanece publicado y cualquiera puede ir a leerlo cuando quiera, empezando desde el primer número o desde hoy.

Lo que suele explicarse mal

Pensar que Kafka es "un RabbitMQ más rápido". Son paradigmas fundamentalmente distintos: cola de trabajo vs log distribuido.

Señal de que aún no lo dominas

Si intentas configurar Kafka para que elimine el mensaje inmediatamente después de que un consumidor lo lea.

Preguntas trampa

Responde de memoria, en voz alta o por escrito, y después abre la respuesta razonada: lo importante es el porqué, no la palabra exacta.

Nivel 1 (Básico) ¿Qué ocurre si envías mensajes a un Topic de Kafka con clave null? Ver respuesta

Respuesta razonada

Sin clave, Kafka no usa el hash para elegir partición. Desde Kafka 2.4 el productor aplica un particionador «pegajoso» (sticky): llena un lote para una partición y, al enviarlo, cambia a otra. Desde 3.3 lo hace de forma uniforme teniendo en cuenta la carga de cada broker. Se consigue buen reparto y buen rendimiento.

La consecuencia es que mensajes relacionados pueden acabar en particiones distintas y procesarse en cualquier orden, incluso a la vez en consumidores diferentes. Si «tarea completada» se procesa antes que «tarea creada», tu consumidor ve un estado imposible. Usa clave null solo cuando los mensajes son realmente independientes entre sí, como logs o métricas.

Qué está evaluando la pregunta

Evalúa si entiendes el particionamiento. Sin clave, Kafka utiliza un particionador round-robin (o sticky) que distribuye los mensajes entre todas las particiones del Topic sin orden lógico. Dos eventos del mismo pedido pueden terminar en particiones distintas y procesarse fuera de secuencia.

Nivel 2 (Intermedio) ¿Por qué no se puede tener más consumidores activos en un Consumer Group que particiones en el Topic? Ver respuesta

Respuesta razonada

Porque cada partición se asigna como máximo a un consumidor dentro de un mismo grupo. Es lo que permite garantizar el orden por partición sin coordinación entre consumidores: si dos consumidores leyeran la misma partición, procesarían mensajes consecutivos en paralelo y en cualquier orden.

Si un topic tiene 6 particiones y despliegas 8 instancias en el mismo grupo, 2 quedan sin particiones y ociosas. No es del todo inútil: actúan como reserva y toman particiones en el rebalanceo si cae otra instancia. Pero para escalar el consumo hay que tener más particiones, por eso se suelen crear los topics con margen (12, 24...) aunque al principio haya pocas instancias.

Qué está evaluando la pregunta

Kafka asigna cada partición a exactamente un consumidor dentro del grupo. Si hay más consumidores que particiones, los sobrantes quedan inactivos. No es posible que dos consumidores lean la misma partición dentro del mismo grupo.

Nivel 3 (Entrevista Senior) Si un microservicio consumidor de Kafka hace un commit del offset y luego falla al guardar el resultado en la base de datos, ¿qué problema de consistencia ocurre? Ver respuesta

Respuesta razonada

Si el offset se confirma antes de terminar el procesamiento y el guardado en base de datos falla, Kafka ya considera el mensaje como leído. Al reiniciar, el consumidor continúa desde el siguiente offset y ese mensaje se pierde para siempre. Es la semántica «como mucho una vez» (at-most-once), casi nunca deseable en datos de negocio.

Lo correcto es procesar primero y confirmar después (at-least-once), que es lo que hace Spring Kafka por defecto: confirma cuando tu listener termina sin excepción. El precio es que, si la aplicación cae entre el guardado y el commit, el mensaje se volverá a entregar. Por eso los consumidores deben ser idempotentes: guardar el id del mensaje procesado en la misma transacción que los datos y descartar repetidos, o usar operaciones naturalmente idempotentes como un upsert.

Qué está evaluando la pregunta

El offset avanza pero el efecto de negocio no se persiste, provocando pérdida silenciosa de mensajes ("at-most-once delivery"). La solución es usar procesamiento idempotente: guardar el offset junto con el resultado en una misma transacción de base de datos, o diseñar las operaciones para ser idempotentes (que repetir el mismo evento no cause efectos duplicados).

Reto del tutor

Un equipo diseña un sistema donde el servicio de pedidos publica un evento PedidoCreado en Kafka. El servicio de facturación y el servicio de inventario deben consumir ese mismo evento de forma independiente. Un desarrollador junior configura los dos servicios con el MISMO group-id: "microservicios". ¿Qué problema ocurrirá y cómo lo corregirías?

Lo que una respuesta suele tener bien

Identifica que con el mismo group-id, Kafka entregará cada mensaje a solo UNO de los dos servicios (balanceo), no a ambos.

Lo que suele quedarse en la superficie

Decir que "hay que crear dos topics".

El concepto de fondo que suele faltar

No comprender que los Consumer Groups son la unidad de suscripción en Kafka: todos los miembros de un mismo grupo se REPARTEN los mensajes. Para que ambos servicios reciban TODOS los mensajes, cada servicio debe tener su propio group-id único.

Cómo lo respondería alguien senior en una entrevista

El problema de raíz es conceptual: dentro de un Consumer Group, las particiones se REPARTEN entre los consumidores (solo uno recibe cada mensaje). Si facturación e inventario comparten el group-id "microservicios", cada evento llegará a uno u otro servicio aleatoriamente, pero jamás a ambos. La corrección es asignar group-ids independientes: group-id: "servicio-facturacion" y group-id: "servicio-inventario". Así, cada Consumer Group mantiene su propio puntero de offset y ambos reciben el 100% de los eventos del Topic de forma independiente.

Proyecto «Pedidos» · paso 22 de 30

Eventos de pedido con Kafka

Avisar a otros servicios de lo que pasa con los pedidos sin llamarlos directamente.

De qué va el proyecto

El backend de una tienda online: clientes, catálogo de productos y pedidos con sus líneas, pagos y envíos. Empieza como un proyecto Maven con records y termina como un sistema con API REST, seguridad, mensajería, contenedores y observabilidad. Cada paso construye sobre el anterior, así que trabaja siempre en el mismo repositorio.

Qué hacer

  1. Publica PedidoConfirmado en el topic pedidos.confirmados con el id del pedido como clave.
  2. Crea un servicio notificaciones que lo consume en su propio consumer group.
  3. Configura reintentos y un Dead Letter Topic para los mensajes que fallan.

Está listo cuando

  • Si el servicio de notificaciones está caído, los eventos se procesan al volver.
  • Un mensaje que falla siempre acaba en el DLT y no bloquea a los demás.

Katas: ejercicios cortos

Reglas de oro

  • Siempre incluye una clave significativa (key) en los mensajes para garantizar orden dentro de una partición.
  • Cada microservicio consumidor debe tener su propio group-id único si quiere recibir todos los eventos.
  • Configura siempre un Dead Letter Topic (DLT) para no perder mensajes que fallen tras los reintentos.
  • El número de particiones de un Topic es el límite máximo de paralelismo de consumidores en un grupo.
  • Diseña tus consumidores para ser idempotentes: que procesar el mismo evento dos veces no cause efectos duplicados.
Se guarda automáticamente

Fuentes oficiales

Revisado el . Este día en Markdown