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
- ¿Por qué mensajería asíncrona?
- Colas (point-to-point) vs Topics (publicación/suscripción)
- Garantías de entrega: at-most-once, at-least-once, exactly-once
- El problema de los duplicados y la idempotencia
- Comparativa: RabbitMQ vs Kafka vs Amazon SQS
- Ejemplo práctico: productor y consumidor idempotente
- Errores comunes y consejos
- Ejercicios y soluciones
- Conclusión
- ¿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.
- 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 |
- 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.
- 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:
- Clave de idempotencia / ID de mensaje: registrar los IDs ya procesados y descartar repetidos.
- Operaciones naturalmente idempotentes:
UPDATE saldo SET valor = 100(asignación absoluta) es idempotente;UPDATE saldo SET valor = valor + 100(incremento) no lo es. UPSERTcon 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 existeExplicación:
mensaje_ides la clave primaria: la base de datos garantiza que no haya dos iguales.ON CONFLICT ... DO NOTHINGhace 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
INSERTafectó a 0 filas, era un duplicado y podemos saltar el procesamiento.
- 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.
- 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:
@KafkaListenersuscribe el método al topicpagos.confirmados. ElgroupId"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). Devuelvefalsesi ya existía → es un duplicado, lo confirmamos y salimos sin reprocesar.- La confirmación (
ack.acknowledge()) se hace después deasentarApunte. 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: falsees esencial: si Kafka confirmara solo, podría confirmar antes de que termináramos de procesar y perderíamos mensajes ante un fallo.ack-mode: manualdelega 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
- 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?
- 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.
- Escribe una sentencia SQL idempotente para "marcar un pedido como pagado" que pueda ejecutarse varias veces sin efectos secundarios.
Soluciones
- 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.
- (a) Cola (un solo receptor, una sola vez). (b) Topic (tres suscriptores reciben el evento). (c) Cola (reparto de trabajo entre workers).
- 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
- ¿Qué es la Arquitectura de Aplicaciones?
- El Rol del Arquitecto de Software
- Atributos de Calidad y Requisitos No Funcionales
- Decisiones Arquitectónicas y Compromisos (Trade-offs)
- Documentación de Arquitectura: Vistas y el Modelo C4
Módulo 2: Principios y Tácticas de Diseño
- Acoplamiento, Cohesión y Separación de Responsabilidades
- Principios SOLID Aplicados a la Arquitectura
- DRY, KISS, YAGNI y Otros Principios de Diseño
- Tácticas Arquitectónicas para los Atributos de Calidad
- Gestión de la Deuda Técnica
Módulo 3: Estilos y Patrones Arquitectónicos
- Arquitectura Monolítica
- Arquitectura en Capas (N-Tier)
- Arquitectura Cliente-Servidor
- Arquitectura Hexagonal (Puertos y Adaptadores)
- Arquitectura Limpia y Cebolla (Clean & Onion)
Módulo 4: Arquitecturas Distribuidas y Microservicios
- Introducción a los Sistemas Distribuidos
- Arquitectura de Microservicios
- Descomposición de Servicios y Bounded Contexts
- API Gateway, Service Discovery y Comunicación entre Servicios
- Patrones de Resiliencia: Circuit Breaker, Retry y Bulkhead
- El Teorema CAP y la Consistencia de Datos
Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería
- Fundamentos de la Arquitectura Orientada a Eventos
- Mensajería Asíncrona: Colas y Brokers
- Patrones de Eventos: Event Sourcing y CQRS
- Gestión de Transacciones Distribuidas: Patrón Saga
- Streaming de Datos en Tiempo Real
Módulo 6: Diseño Dirigido por el Dominio (DDD)
- Conceptos Fundamentales del DDD
- Diseño Estratégico: Bounded Contexts y Lenguaje Ubicuo
- Diseño Táctico: Entidades, Agregados y Repositorios
- Mapeo de Contextos (Context Mapping)
Módulo 7: Datos y Persistencia
- Estrategias de Persistencia: SQL vs NoSQL
- Patrones de Acceso a Datos: Repository, Unit of Work y DAO
- Base de Datos por Servicio y Gestión de Datos Distribuidos
- Caché y Estrategias de Invalidación
Módulo 8: Arquitectura en la Nube y Despliegue
- Fundamentos del Cloud Computing (IaaS, PaaS, SaaS)
- Contenedores y Orquestación con Docker y Kubernetes
- Arquitectura Serverless
- Patrones de Diseño Cloud-Native
- Infraestructura como Código (IaC)
Módulo 9: Calidad, Seguridad y Observabilidad
- Escalabilidad: Horizontal vs Vertical y Balanceo de Carga
- Alta Disponibilidad y Tolerancia a Fallos
- Seguridad por Diseño y Autenticación/Autorización
- Observabilidad: Logging, Métricas y Trazabilidad
- Rendimiento y Pruebas de Carga
