En la lección anterior vimos qué es un evento y quién lo produce o consume. Ahora bajamos un nivel: ¿cómo viaja físicamente ese mensaje de un servicio a otro de forma fiable cuando ambos no se conocen y pueden estar arrancando, cayéndose o saturados en momentos distintos? La respuesta es la mensajería asíncrona a través de brokers de mensajes. Un broker es una pieza de infraestructura intermedia que recibe mensajes de los productores, los almacena de forma duradera y los entrega a los consumidores cuando estos están listos.

Dominar la mensajería asíncrona es imprescindible porque de ella dependen la fiabilidad y la coherencia de cualquier sistema distribuido. En esta lección estudiaremos las dos primitivas básicas (colas y topics), las garantías de entrega, el problema de los duplicados y cómo resolverlo con idempotencia, y compararemos las tres tecnologías más usadas del mercado: RabbitMQ, Apache Kafka y Amazon SQS.

Contenido

  1. ¿Por qué mensajería asíncrona?
  2. Colas (point-to-point) vs Topics (publicación/suscripción)
  3. Garantías de entrega: at-most-once, at-least-once, exactly-once
  4. El problema de los duplicados y la idempotencia
  5. Comparativa: RabbitMQ vs Kafka vs Amazon SQS
  6. Ejemplo práctico: productor y consumidor idempotente
  7. Errores comunes y consejos
  8. Ejercicios y soluciones
  9. Conclusión

  1. ¿Por qué mensajería asíncrona?

En una llamada síncrona, si el servicio B está caído, la llamada de A falla. Con mensajería asíncrona, A deposita el mensaje en el broker y sigue trabajando; cuando B se recupere, lo consumirá. Esto aporta:

  • Desacoplamiento temporal: productor y consumidor no necesitan estar vivos al mismo tiempo.
  • Amortiguación de picos (buffering): si llegan 10.000 mensajes de golpe, el broker los retiene y el consumidor los procesa a su ritmo.
  • Resiliencia: un consumidor lento o caído no tumba al productor.

  1. Colas (point-to-point) vs Topics (publicación/suscripción)

Existen dos modelos de distribución de mensajes.

Cola (point-to-point)

Un mensaje en una cola lo procesa un único consumidor. Si hay varios consumidores conectados a la misma cola, el broker reparte (balancea) los mensajes entre ellos, pero cada mensaje va a uno solo. Se usa para distribuir trabajo (work queue).

Topic (publicación/suscripción)

Un mensaje publicado en un topic se entrega a todos los suscriptores. Se usa para difundir eventos a múltiples interesados.

flowchart TB
    subgraph Cola["COLA (point-to-point)"]
        P1[Productor] --> Q[(Cola)]
        Q --> CA[Consumidor A]
        Q --> CB[Consumidor B]
        Q -. cada mensaje a UNO solo .-> CA
    end
    subgraph Topic["TOPIC (pub/sub)"]
        P2[Productor] --> T{{Topic}}
        T --> SA[Suscriptor A]
        T --> SB[Suscriptor B]
        T -. cada mensaje a TODOS .-> SA
    end
Aspecto Cola (point-to-point) Topic (pub/sub)
Receptores por mensaje Uno Todos los suscriptores
Objetivo Repartir carga de trabajo Difundir eventos
Patrón típico Comandos / tareas Eventos de dominio
Ejemplo Cola de envío de emails "PedidoCreado" a inventario, facturación, envíos

  1. Garantías de entrega: at-most-once, at-least-once, exactly-once

Cuando un mensaje viaja por la red, pueden perderse acuses de recibo (acks), caerse procesos, etc. Por eso existen distintos niveles de garantía:

Garantía Significado Riesgo Coste
At-most-once Se entrega 0 o 1 vez Puede perderse un mensaje Mínimo (sin reintentos)
At-least-once Se entrega 1 o más veces Puede duplicarse Medio (requiere reintentos y acks)
Exactly-once Se entrega exactamente 1 vez Ninguno (ideal) Alto (complejo, no siempre real)

Puntos clave:

  • At-most-once: el consumidor confirma antes de procesar. Si falla, el mensaje se pierde. Solo válido si perder algún dato es aceptable (p. ej. métricas no críticas).
  • At-least-once: el consumidor confirma después de procesar con éxito. Si falla justo antes del ack, el mensaje se reentrega → puede haber duplicados. Es el nivel más habitual y recomendado.
  • Exactly-once: semánticamente perfecto, pero costoso. En sistemas distribuidos puros es muy difícil de garantizar de extremo a extremo; suele lograrse combinando at-least-once con idempotencia en el consumidor (lo veremos a continuación). Kafka ofrece "exactly-once" dentro de sus propios límites (transacciones), pero en cuanto el efecto sale fuera de Kafka (escribir en otra BD, llamar a un API), vuelve a depender de la idempotencia.

Regla de oro del mundo real: diseña para at-least-once y haz a tus consumidores idempotentes. Eso te da, en la práctica, el efecto de exactly-once.

  1. El problema de los duplicados y la idempotencia

Una operación es idempotente si ejecutarla varias veces produce el mismo resultado que ejecutarla una sola vez. Si nuestros consumidores son idempotentes, los duplicados de at-least-once dejan de ser un problema.

Técnicas habituales para lograr idempotencia:

  1. Clave de idempotencia / ID de mensaje: registrar los IDs ya procesados y descartar repetidos.
  2. Operaciones naturalmente idempotentes: UPDATE saldo SET valor = 100 (asignación absoluta) es idempotente; UPDATE saldo SET valor = valor + 100 (incremento) no lo es.
  3. UPSERT con clave única: insertar o ignorar si ya existe.
-- Tabla que registra qué mensajes ya hemos procesado.
CREATE TABLE mensajes_procesados (
    mensaje_id  VARCHAR(64) PRIMARY KEY,  -- clave de idempotencia
    procesado_en TIMESTAMP NOT NULL
);

-- Al recibir un mensaje, intentamos insertar su ID.
-- Si ya existe (clave primaria duplicada), sabemos que es un duplicado.
INSERT INTO mensajes_procesados (mensaje_id, procesado_en)
VALUES ('msg-abc-123', CURRENT_TIMESTAMP)
ON CONFLICT (mensaje_id) DO NOTHING;  -- PostgreSQL: ignora si ya existe

Explicación:

  • mensaje_id es la clave primaria: la base de datos garantiza que no haya dos iguales.
  • ON CONFLICT ... DO NOTHING hace que el segundo intento de insertar el mismo ID no produzca error ni efecto. Así detectamos el duplicado sin lógica adicional compleja.
  • Si el INSERT afectó a 0 filas, era un duplicado y podemos saltar el procesamiento.

  1. Comparativa: RabbitMQ vs Kafka vs Amazon SQS

Característica RabbitMQ Apache Kafka Amazon SQS
Modelo principal Broker de colas (AMQP) Log distribuido de eventos Cola gestionada (cloud)
Paradigma Colas + exchanges (pub/sub) Topics particionados + offsets Colas (Standard y FIFO)
Retención de mensajes Hasta consumir (se borran) Configurable (días/semanas), reproducible Hasta 14 días
Reproducción (replay) No nativa Sí (releer desde un offset) No
Orden Por cola Por partición FIFO solo en colas FIFO
Throughput Alto Muy alto (millones/s) Alto (escala automática)
Garantía típica At-least-once At-least-once / exactly-once* At-least-once (Std) / exactly-once (FIFO)
Gestión Autogestionado / cloud Autogestionado / gestionado Totalmente gestionado (AWS)
Caso ideal Enrutado complejo, RPC, tareas Streaming, event sourcing, big data Desacople simple en AWS sin operar infra

Resumen práctico:

  • RabbitMQ: excelente cuando necesitas enrutado flexible (exchanges con reglas) y patrones tradicionales de colas de trabajo.
  • Kafka: la elección para alto volumen, retención y reproducción de eventos; base de event sourcing y streaming (lección 05-05).
  • SQS: lo más simple si ya estás en AWS y solo quieres desacoplar sin gestionar servidores.

  1. Ejemplo práctico: productor y consumidor idempotente

Veamos un consumidor de Kafka en Spring que aplica at-least-once + idempotencia.

@Component
public class ConsumidorPagos {

    private final RepositorioIdempotencia idempotencia;
    private final ServicioContabilidad contabilidad;

    public ConsumidorPagos(RepositorioIdempotencia idempotencia,
                           ServicioContabilidad contabilidad) {
        this.idempotencia = idempotencia;
        this.contabilidad = contabilidad;
    }

    @KafkaListener(topics = "pagos.confirmados", groupId = "contabilidad")
    public void consumir(PagoConfirmadoEvent evento, Acknowledgment ack) {
        // 1. ¿Ya procesamos este mensaje? -> idempotencia
        if (!idempotencia.registrarSiEsNuevo(evento.pagoId())) {
            ack.acknowledge(); // duplicado: confirmamos y salimos
            return;
        }
        // 2. Lógica de negocio real
        contabilidad.asentarApunte(evento);
        // 3. Confirmamos SOLO tras procesar con éxito (at-least-once)
        ack.acknowledge();
    }
}

Explicación detallada:

  • @KafkaListener suscribe el método al topic pagos.confirmados. El groupId "contabilidad" identifica este grupo de consumidores; Kafka reparte las particiones entre los miembros del grupo.
  • registrarSiEsNuevo(...) intenta insertar el ID (como en el SQL anterior). Devuelve false si ya existía → es un duplicado, lo confirmamos y salimos sin reprocesar.
  • La confirmación (ack.acknowledge()) se hace después de asentarApunte. Si el proceso muere antes del ack, Kafka reentregará el mensaje (at-least-once), pero la idempotencia evitará el doble asiento.
# Configuración Spring Kafka para confirmación manual (clave del at-least-once)
spring:
  kafka:
    consumer:
      group-id: contabilidad
      enable-auto-commit: false      # NO confirmar automáticamente
      auto-offset-reset: earliest    # leer desde el principio si no hay offset
    listener:
      ack-mode: manual               # confirmamos nosotros con ack.acknowledge()
  • enable-auto-commit: false es esencial: si Kafka confirmara solo, podría confirmar antes de que termináramos de procesar y perderíamos mensajes ante un fallo.
  • ack-mode: manual delega en nuestro código el momento exacto de la confirmación.

Errores Comunes y Consejos

  • Confirmar antes de procesar. Convierte tu at-least-once en at-most-once accidental y pierdes mensajes ante fallos. Confirma siempre al final.
  • Creer que "exactly-once" elimina la necesidad de idempotencia. En cuanto el efecto cruza la frontera del broker (otra BD, un API externo), necesitas idempotencia igualmente.
  • No dimensionar la dead-letter queue (DLQ). Los mensajes que fallan repetidamente deben ir a una cola de descarte para no bloquear la cola principal en un bucle infinito de reintentos.
  • Usar topic cuando querías cola (o viceversa). Difundir un comando a "todos" puede ejecutar la misma acción N veces.
  • Consejo: define siempre un campo mensaje_id único en tus eventos desde el día uno; añadirlo después es doloroso.

Ejercicios

  1. Un equipo procesa pagos con un consumidor que confirma el mensaje justo al recibirlo y luego contacta con la pasarela bancaria. Si el proceso muere entre el ack y la llamada bancaria, ¿qué garantía real tienen y qué problema ocurre? ¿Cómo lo corregirías?
  2. Indica para cada caso si usarías cola o topic: (a) enviar el correo de bienvenida una sola vez; (b) notificar a inventario, facturación y CRM de un nuevo pedido; (c) repartir 1.000 tareas de generación de PDF entre 5 workers.
  3. Escribe una sentencia SQL idempotente para "marcar un pedido como pagado" que pueda ejecutarse varias veces sin efectos secundarios.

Soluciones

  1. Tienen at-most-once: si muere tras el ack, el mensaje no se reentrega y el pago nunca llega a la pasarela (se pierde). Corrección: confirmar después de la llamada bancaria (at-least-once) y hacer la operación idempotente con la clave de idempotencia del pago para no cobrar dos veces ante un reintento.
  2. (a) Cola (un solo receptor, una sola vez). (b) Topic (tres suscriptores reciben el evento). (c) Cola (reparto de trabajo entre workers).
  3. Por ejemplo:
-- Asignación absoluta del estado: idempotente.
UPDATE pedidos
SET estado = 'PAGADO', pagado_en = COALESCE(pagado_en, CURRENT_TIMESTAMP)
WHERE pedido_id = :pedidoId AND estado <> 'PAGADO';

Ejecutarla de nuevo no cambia nada porque la condición estado <> 'PAGADO' ya no se cumple y pagado_en se conserva con COALESCE.

Conclusión

La mensajería asíncrona es el sistema circulatorio de las arquitecturas dirigidas por eventos. Diferenciamos colas (un receptor, reparto de carga) de topics (todos los suscriptores, difusión). Entendimos las tres garantías de entrega y por qué at-least-once + idempotencia es la combinación pragmática que usa la industria. Por último, comparamos RabbitMQ, Kafka y SQS para saber cuándo elegir cada una.

En la siguiente lección, "Patrones de Eventos: Event Sourcing y CQRS", veremos cómo, en lugar de guardar solo el estado actual, podemos almacenar la secuencia completa de eventos como fuente de verdad, y cómo separar los modelos de lectura y escritura para escalar.

Curso de Arquitectura de Aplicaciones

Módulo 1: Fundamentos de la Arquitectura de Aplicaciones

Módulo 2: Principios y Tácticas de Diseño

Módulo 3: Estilos y Patrones Arquitectónicos

Módulo 4: Arquitecturas Distribuidas y Microservicios

Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería

Módulo 6: Diseño Dirigido por el Dominio (DDD)

Módulo 7: Datos y Persistencia

Módulo 8: Arquitectura en la Nube y Despliegue

Módulo 9: Calidad, Seguridad y Observabilidad

Módulo 10: Evolución, Gobernanza y Casos Prácticos

© Copyright 2026. Todos los derechos reservados