Event-driven architecture: patrones y cuándo usarlos
Pub/sub, colas, streams, event sourcing y CQRS resuelven problemas distintos y se confunden constantemente. Guía práctica de cada patrón, su coste real y cuándo un sistema síncrono simple sigue siendo la mejor opción.
“Event-driven” se ha convertido en una etiqueta que se pega a casi cualquier sistema que use una cola de mensajes en algún punto, lo cual diluye el término hasta hacerlo casi inútil. Martin Fowler, en un artículo que sigue siendo la referencia más citada del tema, señalaba que bajo el paraguas de “arquitectura orientada a eventos” conviven al menos cuatro patrones con implicaciones muy distintas: notificación de eventos, transferencia de estado mediante eventos, event sourcing y CQRS. Tratarlos como si fueran lo mismo es la razón por la que tantos equipos adoptan “eventos” sin saber muy bien cuál de los cuatro problemas están resolviendo, y terminan pagando la complejidad del más pesado para conseguir el beneficio del más ligero.
Este artículo separa los patrones, explica qué problema resuelve cada uno, y —con la misma importancia— cuándo un sistema síncrono simple sigue siendo la respuesta correcta.
El primitivo del que parte casi todo: pub/sub
La forma más simple de arquitectura orientada a eventos es publish/subscribe: un servicio publica un evento (“pedido creado”) sin saber ni importarle quién lo consume, y cero, uno o varios servicios se suscriben a ese tipo de evento para reaccionar. El productor no conoce a los consumidores; el broker de mensajería absorbe esa coordinación.
graph LR A[Servicio Pedidos] -->|publica: OrderCreated| B[[Broker / Topic]] B --> C[Servicio Facturación] B --> D[Servicio Notificaciones] B --> E[Servicio Analítica]
Esto resuelve un problema muy concreto: cuando ocurre algo, varios sistemas distintos necesitan reaccionar, y el servicio que genera el evento no debería tener que conocer ni acoplarse a cada uno de ellos. Añadir un nuevo consumidor —por ejemplo, un servicio de fraude que también quiere enterarse de cada pedido nuevo— no exige tocar el servicio de pedidos en absoluto: solo suscribir un nuevo consumidor al mismo evento.
Cola, pub/sub y stream no son lo mismo
Es habitual usar estos tres términos como sinónimos y no lo son, y la diferencia importa a la hora de elegir herramienta:
- Cola (queue): cada mensaje se entrega a un único consumidor y luego se elimina. Sirve para repartir trabajo entre varios workers (por ejemplo, procesar imágenes subidas por usuarios).
- Pub/sub (topic): un mismo mensaje se reparte a todos los suscriptores activos en ese momento. Sirve para que varios sistemas independientes reaccionen al mismo evento.
- Stream de eventos (log): un registro durable, ordenado y repetible que distintos grupos de consumidores pueden leer —y releer— de forma independiente, cada uno a su propio ritmo y desde el punto que necesite. Apache Kafka es el ejemplo de referencia: consumir un mensaje no lo borra del log, lo que permite rebobinar el offset de un consumidor y reprocesar eventos pasados si hace falta corregir un bug o reconstruir un estado.
Elegir mal entre estas tres primitivas es una fuente de errores silenciosos: montar un sistema de auditoría o reconstrucción de estado sobre una cola tradicional falla en cuanto necesitas releer eventos antiguos, porque la cola ya los ha descartado.
Event sourcing: guardar el porqué, no solo el qué
En un modelo CRUD tradicional, la base de datos guarda el estado actual: “el pedido #4021 está en estado enviado”. Event sourcing invierte esa idea: en lugar de guardar el estado final, guarda la secuencia completa de eventos que llevaron hasta él —PedidoCreado, PedidoPagado, PedidoEnviado— y el estado actual se calcula reproduciendo esos eventos en orden. La base de datos deja de ser una fotografía y pasa a ser una película completa.
type PedidoEvento =
| { tipo: 'PedidoCreado'; pedidoId: string; items: Item[] }
| { tipo: 'PedidoPagado'; pedidoId: string; monto: number }
| { tipo: 'PedidoEnviado'; pedidoId: string; trackingId: string };
function reconstruirEstado(eventos: PedidoEvento[]): EstadoPedido {
return eventos.reduce((estado, evento) => {
switch (evento.tipo) {
case 'PedidoCreado':
return { ...estado, items: evento.items, status: 'creado' };
case 'PedidoPagado':
return { ...estado, status: 'pagado', montoPagado: evento.monto };
case 'PedidoEnviado':
return { ...estado, status: 'enviado', trackingId: evento.trackingId };
}
}, estadoInicial);
}
La ventaja real de este enfoque no es técnica, es de negocio: nunca pierdes el historial de cómo se llegó a un estado, lo cual es oro para auditoría financiera, disputas de pago o depuración de un bug que solo se manifiesta después de una secuencia concreta de eventos. La documentación de Microsoft sobre el patrón lo resume bien: event sourcing captura todos los cambios como una secuencia inmutable de eventos, lo que permite reconstruir el estado en cualquier punto del pasado y facilita la auditoría completa del sistema.
CQRS: separar cómo escribes de cómo lees
CQRS (Command Query Responsibility Segregation) parte de una observación simple: el modelo de datos óptimo para escribir no siempre es el óptimo para leer. Un pedido se escribe como una serie de validaciones de negocio (stock disponible, método de pago válido, dirección de envío correcta) pero se lee, la mayoría de las veces, como una vista ya desnormalizada para un dashboard o un listado. CQRS separa explícitamente ambos caminos: un modelo de escritura optimizado para las reglas de negocio y un modelo de lectura optimizado para consultas rápidas, sincronizados de forma asíncrona.
CQRS combina de forma natural con event sourcing —los eventos que produce el lado de escritura alimentan proyecciones que actualizan el modelo de lectura— pero no lo exige: se puede aplicar CQRS con un modelo de escritura CRUD normal y un modelo de lectura que simplemente sea una vista materializada de la misma base de datos.
El propio Martin Fowler, que popularizó el término, es tajante sobre sus límites: advierte que la mayoría de los casos con los que se ha encontrado no han ido bien, y que CQRS se ha convertido en una fuerza importante para meter a un sistema de software en serias dificultades cuando se aplica sin que la complejidad esté justificada. Su recomendación es aplicarlo solo en el contexto acotado (el bounded context, en términos de Domain-Driven Design) donde realmente aporta, no como estilo arquitectónico global del sistema.
Cuándo CQRS aporta y cuándo es ceremonia
CQRS tiene sentido cuando:
- Las cargas de lectura y escritura son muy asimétricas (miles de lecturas por cada escritura) y necesitan escalar y optimizarse por separado.
- El modelo de escritura tiene reglas de negocio complejas que ensuciarían un modelo de lectura simple si se mezclaran.
- Ya existe una razón real para tener event sourcing, y CQRS es la forma natural de proyectar esos eventos en vistas consultables.
CQRS es ceremonia cuando:
- Las lecturas son simples y el modelo de escritura no está bajo ninguna presión real. En ese caso, CQRS añade infraestructura (mínimo: almacén de eventos o de escritura, pipeline de proyección, almacén de lectura) sin ningún beneficio que lo compense.
- El equipo no tiene ya resuelta la consistencia eventual como concepto operativo: los datos leídos pueden no reflejar el cambio más reciente durante un margen de tiempo, y eso tiene que ser aceptable para el negocio, no una sorpresa que descubre un usuario.
Cuándo un sistema síncrono simple es la opción honesta
La arquitectura orientada a eventos tiene un coste que a menudo se subestima: la depuración deja de ser lineal. En un flujo síncrono, un stack trace te lleva directamente del síntoma a la causa. En un flujo de eventos, seguir “qué pasó” implica reconstruir mentalmente una cadena de publicaciones y consumos que pueden estar sucediendo en servicios distintos, en momentos distintos, sin que exista una traza única que los conecte a menos que hayas invertido explícitamente en correlación de eventos.
Un sistema síncrono simple —un servicio que llama directamente a otro y espera respuesta— sigue siendo mejor que forzar eventos cuando:
- El flujo es fundamentalmente secuencial y necesita el resultado inmediato. Si el usuario necesita saber en la misma petición si el pago se aprobó, forzar ese flujo a través de eventos asíncronos solo añade latencia y complejidad para simular después una respuesta síncrona.
- Solo hay un consumidor del resultado. Si únicamente un servicio necesita enterarse de algo, pub/sub no aporta nada frente a una llamada directa; solo añade un broker de mensajería como punto adicional de fallo.
- El equipo no puede permitirse aún la inversión en observabilidad distribuida. Sin trazas correlacionadas entre productor y consumidores, depurar un sistema basado en eventos se convierte en arqueología de logs.
- La consistencia inmediata es un requisito real del negocio, no una preferencia. Si dos operaciones deben ser atómicas de verdad (todo o nada, ahora mismo), forzarlas a través de eventos asíncronos con consistencia eventual introduce una clase de bugs —condiciones de carrera, estados intermedios visibles— que no existían con una transacción síncrona.
Recomendación práctica
Antes de introducir cualquier patrón de esta familia, responde a tres preguntas:
- ¿Cuántos consumidores reales necesitan enterarse de este evento hoy? Si la respuesta es “uno, y puede que nunca haya un segundo”, una llamada directa es más simple y no menos correcta.
- ¿Necesito el historial completo de cómo llegué a un estado, o solo el estado actual? Si solo necesitas el estado actual, event sourcing es la herramienta equivocada por mucho que “se vea más robusta” en el papel.
- ¿Mi problema es de acoplamiento (varios consumidores reaccionando a lo mismo) o de rendimiento en lectura (muchas más lecturas que escrituras)? El primero pide pub/sub. El segundo, si es lo bastante severo, puede justificar CQRS. Rara vez ambos problemas aparecen a la vez, y tratarlos como si lo hicieran es la forma más común de sobrediseñar un sistema orientado a eventos.
La arquitectura orientada a eventos no es un estilo que se adopta de una vez para todo el sistema. Es una caja de herramientas con piezas de coste muy distinto, cada una resolviendo un problema distinto, y la habilidad real está en usar solo la pieza que el problema concreto exige.
Artículos relacionados
Patrones de resiliencia: circuit breakers, retries y timeouts explicados
Un servicio lento en el punto equivocado puede arrastrar a todo el sistema si nadie corta la llamada a tiempo. Timeouts, retries con backoff y jitter, y circuit breakers explicados con criterio de producción, no de diapositiva.
Bun vs Node.js vs Deno en 2026: qué runtime elegir y por qué
Tres runtimes de JavaScript, tres filosofías distintas. Esto es lo que dicen los datos (varios, no uno solo) y cuándo cada uno gana de verdad.
Type stripping en Node.js: ejecutar TypeScript sin compilar, explicado
Node.js puede ejecutar archivos .ts directamente desde hace ya un tiempo. No hace type-checking ni soporta todo TypeScript: esto es lo que de verdad ocurre por dentro.