Arquitectura orientada a eventos | Nicolás Garzón
Una arquitectura orientada a eventos organiza ciertas colaboraciones alrededor de hechos inmutables que ya ocurrieron. Reduce acoplamiento temporal, pero traslada complejidad a consistencia, orden, duplicación, contratos y observabilidad.
Un evento expresa un hecho relevante:
Texto
Copiar OrderConfirmed
StockReserved
PaymentAuthorizedNo es una orden. ConfirmOrder solicita una acción; OrderConfirmed afirma que ocurrió.
Texto
Copiar comando
→ puede aceptarse o rechazarse
evento
→ describe un hecho pasadoUna llamada directa obliga a productor y consumidor a estar disponibles al mismo tiempo. Cuando varias capacidades reaccionan a un hecho —notificaciones, reporting, auditoría— una cadena síncrona aumenta latencia y disponibilidad compuesta.
Texto
Copiar Orders
→ Notifications
→ Reporting
→ LoyaltyTexto
Copiar Orders publica OrderConfirmed
├── Notifications consume
├── Reporting consume
└── Loyalty consumeEl productor no coordina cada reacción.
Publica un hecho que posee. No debería publicar internals ni eventos de datos sin significado.
Almacena, enruta y entrega. Sus garantías dependen de configuración: retención, particiones, ack y retries.
Procesa de forma idempotente y maneja duplicados, retrasos y fallos.
Define contrato, versión y compatibilidad.
Lag, DLQ, tracing, replay y reconciliación son parte de la arquitectura.
JSON
Copiar {
"eventId" : "evt-123" ,
"type" : "OrderConfirmed" ,
"version" : 1 ,
"occurredAt" : "2026-07-25T04:00:00Z" ,
"tenantId" : "business-7" ,
"aggregateId" : "order-42" ,
"aggregateVersion" : 8 ,
"correlationId" : "checkout-99" ,
"causationId" : "cmd-77" ,
"data" : {
"total" : { "minorUnits" : 85000 , "currency" : "COP" }
}
} Cada campo responde a una necesidad: deduplicación, orden por agregado, trazabilidad y aislamiento.
Incluye poco y obliga a consultar al productor. Contrato pequeño, pero crea dependencia de disponibilidad y fan-in.
Incluye datos para actualizar una proyección local. Aumenta autonomía, pero expone más información y endurece compatibilidad.
Elige según frescura, privacidad, tamaño y necesidad de lectura local.
Cada consumidor reacciona y publica nuevos hechos.
Texto
Copiar OrderConfirmed
→ Inventory reserva
→ StockReserved
→ Fulfillment preparaNo existe coordinador central.
El proceso queda distribuido e implícito. Cambiar una secuencia exige comprender múltiples consumidores.
Un coordinador conserva estado y decide el siguiente paso.
Texto
Copiar CheckoutSaga
→ reservar stock
→ autorizar pago
→ confirmar pedidoEl orquestador puede concentrar demasiado conocimiento. Debe coordinar, no poseer reglas internas de cada contexto.
Guardar estado y publicar en operaciones separadas produce dual write:
Texto
Copiar DB commit exitoso
→ publicación falla
→ evento perdidoTransactional outbox guarda cambio y evento en la misma transacción. Un relay publica después.
Texto
Copiar transacción local
├── order = confirmed
└── outbox = OrderConfirmed
relay → brokerPuede publicar duplicados; consumidores siguen necesitando idempotencia.
Recibe evento.
Verifica schema, tenant y versión.
Comprueba si eventId ya fue procesado.
Aplica efecto local en transacción.
Registra inbox/event ID.
Confirma al broker.
Si falla antes del ack, el broker puede reenviar y la inbox evita repetir efecto.
El orden global es costoso. Normalmente se necesita por agregado o clave.
Texto
Copiar partition key = orderIdEl consumidor usa aggregateVersion para ignorar eventos antiguos o detectar huecos.
Después de OrderConfirmed, reporting puede tardar. Esto debe ser aceptable y visible.
La UX puede mostrar estados:
Texto
Copiar confirmed
notification pending
report projection delayedEventual no significa indefinido. Define SLO de convergencia y reconciliación.
Orders cambia estado y escribe outbox.
Relay publica OrderConfirmed.
Notifications envía confirmación.
Reporting actualiza métricas.
Delivery evalúa asignación.
Si Notifications falla, el pedido continúa confirmado. El mensaje se reintenta y puede terminar en DLQ. Una alerta y runbook permiten intervenir.
Representa un hecho interno y puede contener conceptos ricos.
Contrato estable para otros contextos. Puede traducir, reducir datos y versionar.
No publiques automáticamente todos los eventos internos.
Procesamiento idempotente o inbox.
Versiones por agregado y reglas de precedencia.
Retención, lag y autoscaling. El productor no debe esperar.
Intentos limitados, DLQ y diagnóstico. No bloquees una partición indefinidamente.
Los consumidores deben distinguir reconstrucción de efectos irreversibles. No reenvíes correos históricos durante un replay.
Minimiza payload, cifra y aplica retención. Un evento es una copia adicional.
Evento como comando disfrazado.
Publicar modelo interno completo.
Usar eventos para toda llamada local.
Asumir exactly-once global.
Ignorar replay.
No versionar schemas.
Coreografía sin visibilidad de proceso.
No definir fuente de verdad.
Varias reacciones independientes.
Procesamiento asíncrono.
Proyecciones y auditoría.
Integración entre contextos.
Tolerancia a retraso.
Operación local simple.
Resultado inmediato obligatorio.
Equipo sin capacidad para operar broker y flujos asíncronos.
Dominio que exige atomicidad local y no necesita distribución.
Contract tests.
Duplicados y desorden.
Replay.
Fallo del broker y relay.
Métricas de lag y DLQ.
Trazas con correlation/causation.
Reconciliación de proyecciones.
Un evento describe un hecho pasado.
El desacoplamiento temporal introduce consistencia eventual.
Outbox evita pérdida entre DB y broker.
Consumidores deben ser idempotentes.
Orden suele requerirse por entidad, no globalmente.
Operación y schema evolution son parte del diseño.
¿Por qué SendOrderEmail no es un evento?
¿Qué problema resuelve outbox y cuál no?
¿Cuándo elegirías event-carried state?
¿Cómo evitarías efectos durante replay?
¿Qué diferencia existe entre coreografía y orquestación?
Mensajería, colas y pub/sub profundiza en los mecanismos de entrega y capacidad que sostienen estos flujos.