---
name: event-driven
description: Arquitectura event-driven. Message brokers RabbitMQ y Kafka, event sourcing, CQRS, pub/sub, event schemas, idempotency, dead letter queues, retry strategies, eventual consistency. Con ejemplos Python.
version: "1.0.0"
herramientasPermitidas: [Read, Grep]
evolvable: true  # default para skill estandar
exclusiones:
  - "No cargar para pipelines de datos ETL con Kafka como transporte de datos masivos — para ingeniería de datos cargar `datos-etl`."
  - "No cargar para microservicios sin mensajería (REST, gRPC sincrónico entre servicios) — para microservicios síncronos cargar `microservicios`."
  - "No cargar para colas de tareas simples (Celery, Bull, RQ) sin patrones de evento — las colas de tareas son un subconjunto; cargar el skill del framework correspondiente."
  - "No cargar para websockets y notificaciones push en tiempo real al cliente — para realtime client-side cargar el skill del framework frontend."
---
# Arquitectura Orientada a Eventos

Los sistemas event-driven desacoplan productores de consumidores, permitiendo escalar
componentes independientemente y resilir ante fallos.

---

## Cuándo NO cargar

- El uso de Kafka/RabbitMQ es para ingesta masiva de datos ETL, no para eventos de negocio: cargar `datos-etl`.
- Los microservicios se comunican de forma síncrona (REST, gRPC) sin mensajería: cargar `microservicios`.
- La cola es de tareas background simples (Celery, Bull, RQ) sin esquemas de evento ni idempotencia: cargar el skill del framework.
- La funcionalidad es de notificaciones push al cliente via WebSockets: cargar el skill del framework frontend.

## Cuándo Usar Event-Driven

**USAR cuando**:
- Múltiples consumidores reaccionan al mismo evento
- El productor no necesita respuesta inmediata
- Necesitas escalar productores y consumidores independientemente
- Necesitas auditoría completa de cambios (event log = audit log)

**NO USAR cuando**:
- Necesitas respuesta síncrona inmediata para el usuario
- El flujo es simple y lineal (no hay múltiples consumidores)
- La consistencia eventual no es aceptable para el dominio

---

## Reglas Obligatorias

1. **Schemas de eventos tipados** — SIEMPRE definir con Pydantic. Son el contrato entre servicios.
2. **Versioning en schemas** — campo `version: "1.0"`. NUNCA romper compatibilidad hacia atrás.
3. **Idempotency en todos los handlers** — verificar antes de procesar, marcar después.
4. **Dead Letter Queue** — OBLIGATORIA para cada cola. Mensajes que fallan van a DLQ.
5. **Retry con backoff exponencial + jitter** — evitar thundering herd.
6. **Correlation ID** — propagar en todos los eventos para trazabilidad.
7. **Transactional Outbox** — para garantía de publicación atómica con BD.
8. **Consumer group names** — descriptivos y estables (no dinámicos).

---

## RabbitMQ vs Kafka — Cuándo Usar Cuál

| Criterio | RabbitMQ | Kafka |
|----------|----------|-------|
| Patrón principal | Pub/Sub, work queues | Streaming, event log |
| Orden de mensajes | Por cola | Por partition |
| Retención | Hasta consumir | Configurable (días/ilimitado) |
| Escala | Miles de msgs/seg | Millones de msgs/seg |
| Complejidad operativa | Menor | Mayor |

---

## Patrón de Evento Base

```python
from pydantic import BaseModel
from datetime import datetime, timezone
import uuid

class EventoBase(BaseModel):
    id: str = ""
    timestamp: datetime = None
    version: str = "1.0"
    correlation_id: str = ""
    origen: str

    def model_post_init(self, *args):
        if not self.id:
            self.id = str(uuid.uuid4())
        if not self.timestamp:
            self.timestamp = datetime.now(timezone.utc)
```

---

## Idempotency — Patrón Esencial

```python
class HandlerConIdempotencia:
    async def manejar(self, evento) -> None:
        consumer_id = "mi_handler"
        # Verificar ANTES de procesar
        if await self._idempotencia.ya_procesado(evento.id, consumer_id):
            return
        # Procesar
        await self._procesar(evento)
        # Marcar DESPUÉS del procesamiento exitoso
        await self._idempotencia.marcar_procesado(evento.id, consumer_id)
```

---

## Transactional Outbox — Publicación Garantizada

Problema: publicar evento Y hacer commit en BD de forma atómica.
Solución: escribir evento a tabla "outbox" en misma transacción, worker publica al broker.

```python
# Mismo commit = garantía de consistencia
empleado = Empleado(**comando.model_dump())
db.add(empleado)
evento_outbox = EventoOutbox(tipo="empleado.creado", payload={...})
db.add(evento_outbox)
await db.commit()  # Atómico
```

---

## Eventual Consistency — Manejo en UI

- Responder con `202 Accepted` + endpoint de polling para verificar estado.
- El frontend consulta periódicamente hasta que el recurso esté disponible.

---

## Checklist de Sistema Event-Driven

- [ ] Schemas de eventos definidos con Pydantic (tipado fuerte)
- [ ] Versioning en schemas (campo `version`)
- [ ] Idempotency en todos los handlers
- [ ] Dead Letter Queue para cada cola
- [ ] Retry con backoff exponencial y jitter
- [ ] Correlation ID propagado
- [ ] Transactional Outbox para garantía de publicación
- [ ] Monitoring de DLQ con alertas
- [ ] Tests de integración con broker real
- [ ] Schemas backward-compatible (cambios aditivos)

Para implementaciones completas de RabbitMQ, Kafka, CQRS, Outbox y más, ver [recursos/implementaciones-completas.md](recursos/implementaciones-completas.md).

## Gotchas / Errores comunes no obvios

**El handler de idempotencia verifica el `evento.id` pero el mismo evento puede llegar con IDs diferentes desde distintas fuentes**: si el mismo evento lógico se publica dos veces (por reintentos del Outbox o doble procesamiento del productor), y cada publicación genera un UUID diferente, la verificación `ya_procesado(evento.id)` no detecta el duplicado. Causa: la idempotencia basada en ID del evento asume que el productor genera el mismo ID para el mismo evento lógico. Fix: para eventos donde la duplicación puede ocurrir en el productor, usar un ID de negocio determinista como clave de idempotencia: `f"pedido-{pedido_id}-pagado"` en lugar del UUID del evento. El UUID es correcto para los reintentos de entrega del broker, no para prevenir duplicación lógica.

**El Transactional Outbox garantiza que el evento se publica, pero no garantiza el orden de publicación**: si dos operaciones de negocio ocurren en orden A→B pero el worker del Outbox publica B antes que A (por diferencias de timing entre transacciones), los consumidores procesan B antes que A y el estado del sistema queda inconsistente. Causa: el Outbox garantiza publicación eventual, no orden de publicación entre eventos de diferentes transacciones. Fix: para flujos donde el orden importa, incluir un campo `numero_secuencia` o `timestamp` en el evento y que los consumidores validen el orden antes de procesar. Alternativamente, usar particiones de Kafka con la misma clave para el mismo agregado, garantizando orden dentro del agregado.

**`correlation_id` se pierde al cruzar una cola de mensajes asíncrona (Kafka, RabbitMQ)**: el `correlation_id` se propaga correctamente en llamadas HTTP (vía header `X-Correlation-ID`), pero cuando un servicio publica a una cola, el handler del consumidor crea un nuevo contexto de logging sin extraer el `correlation_id` del mensaje. Causa: a diferencia de HTTP donde los middlewares extraen headers automáticamente, los consumidores de Kafka/RabbitMQ requieren código explícito para extraer y bindear el `correlation_id`. Fix: definir un patrón estándar donde todos los handlers de mensajes extraen el campo `correlation_id` del evento antes del primer log: `log = logger.bind(correlation_id=evento.correlation_id)` y usan ese logger vinculado durante todo el procesamiento.

**La Dead Letter Queue se llena silenciosamente y nadie la monitorea hasta que hay un incidente**: los mensajes en DLQ representan operaciones de negocio fallidas (pedidos no procesados, pagos no registrados), pero la DLQ se configura como "red de seguridad" y luego se olvida. Causa: la DLQ previene pérdida de mensajes pero no resuelve el problema subyacente. Sin alertas, los mensajes se acumulan indefinidamente. Fix: configurar alertas en Prometheus/Grafana para `dlq_message_count > 0` con severidad ALTA. Establecer un proceso de revisión de DLQ como parte del runbook de incidentes: inspeccionar el error, corregir la causa raíz, y reprocesar los mensajes desde la DLQ una vez que el sistema está sano.
