· nervico-team · desarrollo-software · 10 min read
Event-driven architecture: patrones y anti-patrones
Guia practica sobre event-driven architecture: patrones fundamentales, anti-patrones comunes, cuando usar eventos vs llamadas sincronas, y como implementar EDA sin perder el control.
LinkedIn procesa mas de 5 billones de eventos al dia a traves de su plataforma de mensajeria. Netflix usa event-driven architecture para coordinar la codificacion de video, las recomendaciones y la entrega de contenido a mas de 260 millones de suscriptores. Uber procesa millones de eventos por segundo para coordinar conductores, pasajeros, pagos y mapas en tiempo real.
Estos numeros son impresionantes, pero tambien enganosos. La mayoria de los proyectos no necesitan procesar billones de eventos. Sin embargo, la arquitectura orientada a eventos (EDA) resuelve problemas reales que aparecen en sistemas de cualquier tamano: desacoplamiento entre componentes, procesamiento asincrono, y la capacidad de reaccionar a cambios de estado de forma flexible.
El problema es que EDA mal implementada puede ser peor que no tenerla. Eventos perdidos, procesamiento duplicado, flujos imposibles de depurar, y una complejidad operativa que supera con creces los beneficios.
Esta guia cubre los patrones que funcionan, los anti-patrones que hay que evitar, y un framework de decision para saber cuando EDA tiene sentido en tu proyecto.
Que es Event-Driven Architecture
Conceptos fundamentales
En una arquitectura orientada a eventos, los componentes del sistema se comunican emitiendo y reaccionando a eventos. Un evento es una notificacion de que algo ha ocurrido: un usuario se ha registrado, un pedido se ha realizado, un pago se ha procesado.
Tres componentes basicos:
Productor: El componente que emite el evento. Publica el evento y se desentiende de lo que pase despues.
Broker: La infraestructura que transporta los eventos del productor a los consumidores. Kafka, RabbitMQ, Amazon SNS/SQS, Google Pub/Sub.
Consumidor: El componente que recibe y procesa el evento. Reacciona al evento ejecutando su propia logica.
Diferencia clave con la comunicacion sincrona: En una llamada sincrona (REST, gRPC), el emisor espera una respuesta. En una comunicacion por eventos, el productor no espera nada. Emite el evento y continua. El consumidor lo procesara cuando pueda.
Tipos de eventos
Eventos de notificacion: Informan de que algo ha ocurrido pero no contienen todos los datos necesarios para procesarlo. El consumidor necesita hacer una llamada adicional para obtener los detalles.
Ejemplo: OrderPlaced { orderId: "123" } — El consumidor necesita llamar al servicio de pedidos para obtener los detalles.
Eventos que llevan estado (event-carried state transfer): Contienen toda la informacion necesaria para que el consumidor procese el evento sin hacer llamadas adicionales.
Ejemplo: OrderPlaced { orderId: "123", items: [...], total: 299.00, shippingAddress: {...} }
Eventos de dominio: Representan hechos del negocio significativos. Son parte del lenguaje ubicuo del dominio.
La recomendacion general: usa eventos que llevan estado cuando sea posible. Reduce el acoplamiento y la latencia.
Patrones fundamentales
Publish-Subscribe
El patron mas basico. Un productor publica un evento en un topic, y todos los consumidores suscritos a ese topic lo reciben.
Como funciona:
- El servicio de pedidos publica OrderPlaced en el topic “orders”
- El servicio de notificaciones (suscrito al topic) envia un email al cliente
- El servicio de inventario (suscrito al topic) reserva los productos
- El servicio de analitica (suscrito al topic) registra la metrica
Ventajas:
- Desacoplamiento total. El productor no conoce a los consumidores
- Extensibilidad: anadir un nuevo consumidor no requiere cambiar el productor
- Cada consumidor procesa el evento a su propio ritmo
Desventajas:
- No hay garantia de orden de procesamiento entre consumidores
- Dificil saber si todos los consumidores procesaron el evento correctamente
- Puede crear dependencias ocultas que son dificiles de rastrear
Event Sourcing
En lugar de almacenar el estado actual de una entidad, almacenas la secuencia de eventos que la llevaron a ese estado.
Ejemplo: En lugar de almacenar “el saldo de la cuenta es 1.500 euros”, almacenas:
CuentaCreada { saldo: 0 }DepositoRealizado { cantidad: 2.000 }RetiroRealizado { cantidad: 500 }
El estado actual se calcula reproduciendo todos los eventos.
Cuando tiene sentido:
- Sistemas financieros donde necesitas un audit trail completo
- Sistemas donde necesitas poder reconstruir el estado en cualquier punto del tiempo
- Sistemas de colaboracion donde multiples usuarios modifican los mismos datos concurrentemente
Cuando no tiene sentido:
- CRUDs simples donde el estado actual es todo lo que necesitas
- Sistemas con requisitos de consulta complejos (las consultas sobre event stores son dificiles)
- Equipos sin experiencia en event sourcing (la curva de aprendizaje es significativa)
Complejidad real: Event sourcing anade una complejidad considerable. Necesitas manejar versionado de eventos, proyecciones para consultas, snapshots para rendimiento, y un modelo mental diferente al desarrollo tradicional. No lo implementes a menos que tengas razones claras.
CQRS (Command Query Responsibility Segregation)
Separa las operaciones de escritura (commands) de las operaciones de lectura (queries) en modelos diferentes.
Como funciona:
- Modelo de escritura: optimizado para procesar comandos y mantener la consistencia
- Modelo de lectura: optimizado para responder consultas rapidamente
- Los cambios en el modelo de escritura se propagan al modelo de lectura a traves de eventos
Ejemplo practico:
- Cuando un usuario hace un pedido, el comando se procesa en una base de datos normalizada (PostgreSQL)
- Un evento OrderPlaced actualiza una vista materializada en Elasticsearch, optimizada para busquedas y listados
- Las consultas del frontend van directamente a Elasticsearch
Ventajas:
- Cada modelo esta optimizado para su proposito
- Puedes escalar lectura y escritura de forma independiente
- Permite multiples representaciones de los mismos datos
Desventajas:
- Consistencia eventual entre el modelo de escritura y lectura
- Duplicacion de datos
- Complejidad operativa significativa
- Mas infraestructura que mantener
Regla de oro: CQRS es apropiado cuando los patrones de lectura y escritura son fundamentalmente diferentes en volumen, complejidad o requisitos de rendimiento. Si tus lecturas y escrituras son similares, CQRS es sobreingenieria.
Saga Pattern
Las sagas coordinan transacciones distribuidas a traves de multiples servicios usando eventos.
Ejemplo: Un proceso de compra involucra:
- Reservar inventario
- Procesar pago
- Confirmar envio
Si el pago falla despues de reservar el inventario, necesitas compensar: liberar la reserva de inventario.
Dos enfoques:
Coreografia: Cada servicio escucha eventos y decide que hacer. No hay coordinador central. El servicio de inventario emite InventoryReserved, el servicio de pagos escucha y emite PaymentProcessed o PaymentFailed, y el servicio de inventario escucha PaymentFailed para liberar la reserva.
Orquestacion: Un servicio coordinador (saga orchestrator) dirige el flujo. Envia comandos a cada servicio y decide que hacer segun las respuestas.
Coreografia vs orquestacion:
| Aspecto | Coreografia | Orquestacion |
|---|---|---|
| Complejidad | Distribuida, dificil de seguir | Centralizada, mas facil de entender |
| Acoplamiento | Bajo | Medio (el orquestador conoce a todos) |
| Puntos de fallo | Multiples | Centralizado |
| Mejor para | Flujos simples (2-3 pasos) | Flujos complejos (4+ pasos) |
Outbox Pattern
Garantiza que un evento se publica si y solo si la transaccion de base de datos se completa. Resuelve el problema del dual write: como actualizar la base de datos Y publicar un evento de forma atomica.
El problema: Si actualizas la base de datos y luego publicas el evento, puede ocurrir que la base de datos se actualice pero el evento no se publique (si el broker esta caido). O viceversa.
La solucion:
- En la misma transaccion de base de datos, escribes los datos Y un registro en una tabla “outbox”
- Un proceso separado (CDC con Debezium, o un poller) lee la tabla outbox y publica los eventos
- Una vez publicado, marca el registro como procesado
Cuando usarlo: Siempre que necesites garantizar que un cambio de estado en la base de datos se comunica de forma fiable a otros servicios.
Anti-patrones que debes evitar
Event Soup
Publicar eventos para todo sin un diseno claro. El resultado es un sistema donde cientos de eventos fluyen en todas direcciones y nadie entiende la relacion entre ellos.
Senales de alarma:
- No puedes dibujar el flujo de eventos entre servicios en 5 minutos
- Los nombres de los eventos no siguen una convencion clara
- Multiples servicios reaccionan al mismo evento de formas que se contradicen
- Nadie sabe que pasaria si se eliminara un evento
Solucion: Documenta cada evento: quien lo produce, quien lo consume, que datos lleva, y por que existe. Si no puedes justificar la existencia de un evento, eliminalo.
Event-Driven CRUD
Usar eventos para operaciones que serian mas simples como llamadas sincronas directas. Si el servicio A necesita crear un registro en el servicio B y esperar confirmacion, una llamada REST es mas simple y fiable que publicar un evento CreateRecord y esperar un evento RecordCreated.
Regla practica: Si el productor necesita la respuesta del consumidor para continuar, no uses eventos. Usa una llamada sincrona.
Eventos como API
Disenar eventos pensando en los consumidores especificos en lugar del dominio. Esto crea un acoplamiento sutil: si un consumidor necesita un campo nuevo, se anade al evento, lo que fuerza a todos los demas consumidores a manejar el cambio.
Solucion: Los eventos representan hechos del dominio, no necesidades de consumidores. Si un consumidor necesita datos adicionales, puede combinar multiples eventos o hacer una llamada directa para obtenerlos.
Falta de idempotencia
No manejar la posibilidad de recibir el mismo evento dos veces. En sistemas distribuidos, la entrega exactamente-una-vez es practicamente imposible. Necesitas diseno idempotente: procesar el mismo evento dos veces debe producir el mismo resultado que procesarlo una vez.
Implementacion practica:
- Cada evento tiene un ID unico
- El consumidor almacena los IDs de eventos procesados
- Antes de procesar un evento, verifica si ya se proceso
- Si ya se proceso, lo descarta silenciosamente
Eventos excesivamente grandes
Incluir todos los datos posibles en cada evento “por si acaso”. Eventos de 50KB o mas con datos que la mayoria de consumidores no necesitan.
Problema: Mas ancho de banda, mas almacenamiento, mas tiempo de serializacion/deserializacion, y cambios frecuentes en el schema del evento cada vez que cambia algun dato.
Solucion: Incluye solo los datos que son naturales para el evento. Datos que representan el hecho que ocurrio, no datos que algun consumidor podria necesitar.
Infraestructura: que tecnologia usar
Apache Kafka
Ideal para: Alto volumen de eventos, necesidad de replay, multiples consumidores que procesan a diferentes ritmos.
No ideal para: Proyectos pequenos, mensajeria point-to-point simple, cuando la latencia sub-milisegundo es critica.
Complejidad operativa: Alta. Kafka requiere conocimiento especializado para operar correctamente.
RabbitMQ
Ideal para: Mensajeria tradicional con patrones de enrutamiento complejos, colas de trabajo, comunicacion point-to-point.
No ideal para: Alto volumen de eventos con necesidad de replay, cuando multiples consumidores necesitan procesar los mismos eventos.
Complejidad operativa: Media. Mas simple que Kafka pero aun requiere atencion.
Amazon SNS/SQS
Ideal para: Proyectos en AWS que necesitan pub/sub simple sin gestionar infraestructura.
No ideal para: Cuando necesitas replay de eventos o procesamiento de streams.
Complejidad operativa: Baja. Servicio gestionado.
Tabla de decision
| Requisito | Kafka | RabbitMQ | SNS/SQS |
|---|---|---|---|
| Alto volumen | Excelente | Bueno | Bueno |
| Replay de eventos | Si | No nativo | No |
| Multiples consumidores | Excelente | Bueno | Bueno |
| Operaciones simples | Complejo | Medio | Simple |
| Latencia | Baja (ms) | Muy baja (sub-ms) | Media |
| Coste operativo | Alto | Medio | Bajo |
Cuando usar (y cuando no) Event-Driven Architecture
Usa EDA cuando
- Necesitas desacoplar servicios que evolucionan a ritmos diferentes
- Tienes procesos que involucran multiples servicios y pueden ejecutarse de forma asincrona
- Necesitas capacidad de replay (reconstruir estado a partir de eventos pasados)
- Multiples consumidores necesitan reaccionar al mismo hecho de negocio
- Necesitas escalar productores y consumidores de forma independiente
No uses EDA cuando
- La comunicacion es inherentemente sincrona (el usuario espera una respuesta)
- El flujo es simple y lineal (A llama a B, B responde)
- Tu equipo no tiene experiencia con sistemas distribuidos
- El volumen de eventos es bajo y no hay necesidad de desacoplamiento
- La consistencia fuerte es un requisito no negociable
Observabilidad en sistemas event-driven
El problema de la trazabilidad
En un sistema sincrono, puedes seguir una peticion de principio a fin con un trace ID. En un sistema event-driven, un evento puede disparar una cadena de eventos que se ramifica en multiples flujos.
Solucion: Correlation IDs. Cada evento lleva un ID de correlacion que se propaga a todos los eventos derivados. Herramientas como Jaeger, Zipkin, o AWS X-Ray pueden visualizar la cadena completa.
Monitorizacion critica
- Lag de consumidores: Cuantos eventos hay pendientes de procesar. Si crece, el consumidor no da abasto.
- Eventos fallidos: Eventos que no se pudieron procesar. Necesitan dead letter queues y alertas.
- Latencia end-to-end: Tiempo desde que se produce un evento hasta que todos los consumidores lo procesan.
- Throughput: Eventos procesados por segundo. Necesitas saber la capacidad maxima del sistema.
Conclusion
Event-driven architecture es una herramienta poderosa para construir sistemas desacoplados, escalables y flexibles. Pero su complejidad no es trivial, y aplicarla donde no es necesaria anade coste sin beneficio.
Tres principios para usar EDA de forma practica:
- Empieza sincrono, evoluciona a asincrono. Las llamadas directas son mas simples de implementar, depurar y monitorizar. Introduce eventos cuando tengas razones claras.
- Disena para fallos. Los eventos se pueden perder, duplicar o llegar fuera de orden. Tu sistema debe manejar todos estos casos.
- Observabilidad desde el dia uno. Sin trazabilidad, un sistema event-driven se convierte en una caja negra imposible de depurar.
Si necesitas ayuda para disenar la arquitectura de tus proyectos de software a medida, en NERVICO tenemos experiencia implementando sistemas event-driven que funcionan en produccion, no solo en diagramas.