Arquitectura Dirigida por Eventos
La arquitectura dirigida por eventos (EDA) es un paradigma de diseño donde el flujo del programa está determinado por eventos. En lugar de que los servicios se llamen directamente entre sí (solicitud-respuesta), los servicios producen y consumen eventos asincrónicamente. Este desacoplamiento permite escalado independiente, mejor aislamiento de fallos y evolución del sistema más flexible.
¿Qué Es un Evento?
Un evento es un registro inmutable de algo que sucedió en el sistema. A diferencia de un comando (que le dice a un sistema qué hacer), un evento declara lo que ya ocurrió.
Eventos vs. Comandos vs. Consultas
| Concepto | Intención | Dirección | Ejemplo |
|---|---|---|---|
| Evento | Notificación de algo que sucedió | Uno a muchos (broadcast) | OrderPlaced, UserRegistered |
| Comando | Solicitud para realizar una acción | Uno a uno (dirigido) | PlaceOrder, RegisterUser |
| Consulta | Solicitud de información | Uno a uno (dirigido) | GetOrderStatus, GetUser |
// Event structure
interface DomainEvent {
eventId: string;
eventType: string; // e.g., "OrderPlaced"
aggregateId: string;
aggregateType: string; // e.g., "Order"
timestamp: Date;
version: number;
payload: Record<string, unknown>;
metadata: {
correlationId: string;
causationId: string;
userId?: string;
};
}
const orderPlacedEvent: DomainEvent = {
eventId: "evt_abc123",
eventType: "OrderPlaced",
aggregateId: "order_456",
aggregateType: "Order",
timestamp: new Date(),
version: 1,
payload: {
customerId: "cust_789",
items: [
{ productId: "prod_001", quantity: 2, price: 29.99 },
],
totalAmount: 59.98,
},
metadata: {
correlationId: "corr_xyz",
causationId: "cmd_place_order_111",
userId: "cust_789",
},
};Event Sourcing
Event sourcing es un patrón donde el estado de una entidad se deriva de una secuencia de eventos en lugar de almacenarse como un snapshot. En lugar de actualizar una fila en una base de datos, agregas un evento a un event store. El estado actual se reconstruye reproduciendo todos los eventos de esa entidad.
// Event-sourced Order aggregate
class Order {
private id: string = "";
private status: string = "";
private items: OrderItem[] = [];
private totalAmount: number = 0;
static fromEvents(events: DomainEvent[]): Order {
const order = new Order();
for (const event of events) {
order.apply(event);
}
return order;
}
private apply(event: DomainEvent): void {
switch (event.eventType) {
case "OrderCreated":
this.id = event.aggregateId;
this.status = "CREATED";
this.items = event.payload.items as OrderItem[];
this.totalAmount = event.payload.totalAmount as number;
break;
case "OrderPaid":
this.status = "PAID";
break;
case "OrderShipped":
this.status = "SHIPPED";
break;
case "OrderCancelled":
this.status = "CANCELLED";
break;
}
}
}
// Event Store interface
interface EventStore {
append(aggregateId: string, events: DomainEvent[], expectedVersion: number): Promise<void>;
getEvents(aggregateId: string, fromVersion?: number): Promise<DomainEvent[]>;
getEventsByType(eventType: string, fromTimestamp?: Date): Promise<DomainEvent[]>;
}Beneficios del Event Sourcing
- Pista de auditoría completa: Cada cambio se registra, habilitando reconstrucción completa del historial
- Consultas temporales: Responde preguntas como "¿cuál era el estado en el tiempo T?"
- Depuración: Reproduce eventos para reproducir cualquier estado histórico
- Flexibilidad: Crea nuevos modelos de lectura reproduciendo eventos a través de nuevas proyecciones
- Reproducción de eventos: Corrige errores y reconstruye estado reproduciendo handlers de eventos corregidos
CQRS (Command Query Responsibility Segregation)
CQRS separa las operaciones de lectura y escritura en modelos diferentes. El modelo de escritura maneja comandos y produce eventos. El modelo de lectura está optimizado para consultas y se construye consumiendo eventos.
// Write side: handles commands, emits events
class OrderCommandHandler {
async handle(command: PlaceOrderCommand): Promise<void> {
const customer = await customerRepo.findById(command.customerId);
if (!customer.isActive) throw new Error("Customer inactive");
const event: DomainEvent = {
eventId: generateId(),
eventType: "OrderPlaced",
aggregateId: generateOrderId(),
aggregateType: "Order",
timestamp: new Date(),
version: 1,
payload: {
customerId: command.customerId,
items: command.items,
totalAmount: command.items.reduce((sum, i) => sum + i.price * i.quantity, 0),
},
metadata: { correlationId: command.correlationId, causationId: command.commandId },
};
await eventStore.append(event.aggregateId, [event], 0);
await eventBus.publish(event);
}
}
// Read side: consumes events, builds query-optimized views
class OrderReadModelProjection {
async handleEvent(event: DomainEvent): Promise<void> {
switch (event.eventType) {
case "OrderPlaced":
await readDb.orders.insert({
orderId: event.aggregateId,
customerId: event.payload.customerId,
status: "PLACED",
totalAmount: event.payload.totalAmount,
itemCount: (event.payload.items as any[]).length,
createdAt: event.timestamp,
});
break;
case "OrderShipped":
await readDb.orders.update(
{ orderId: event.aggregateId },
{ status: "SHIPPED", shippedAt: event.timestamp }
);
break;
}
}
}Event Bus y Event Store
El event bus (o message broker) es la columna vertebral de un sistema dirigido por eventos. Recibe eventos de productores y los entrega a consumidores.
| Tecnología | Tipo | Mejor Para |
|---|---|---|
| Apache Kafka | Log distribuido / streaming de eventos | Alto rendimiento, reproducción de eventos, procesamiento de streams |
| RabbitMQ | Message broker | Enrutamiento complejo, colas de tareas, menor rendimiento |
| AWS SNS + SQS | Pub/sub gestionado + cola | Serverless, patrones fan-out, apps nativas de AWS |
| Redis Streams | Stream en memoria | Baja latencia, casos de uso más simples |
| EventStoreDB | Event store de propósito específico | Event sourcing con proyecciones integradas |
Beneficios y Desafíos
| Beneficios | Desafíos |
|---|---|
| Acoplamiento débil entre servicios | Consistencia eventual (no siempre aceptable) |
| Escalado independiente de productores y consumidores | Depuración es más difícil (rastrear eventos entre servicios) |
| Ajuste natural para flujos de trabajo asíncronos | Garantías de orden de eventos son complejas |
| Fácil agregar nuevos consumidores sin cambiar productores | Evolución de esquemas (cambiar formatos de eventos con el tiempo) |
| Mejor aislamiento de fallos | Idempotencia requerida (eventos pueden entregarse más de una vez) |
Cuándo Usar y Cuándo No
Usa Arquitectura Dirigida por Eventos Cuando:
- Múltiples servicios necesitan reaccionar al mismo evento de negocio
- Necesitas acoplamiento débil entre contextos delimitados
- Los flujos de trabajo son naturalmente asíncronos (procesamiento de pedidos, notificaciones)
- Necesitas una pista de auditoría completa de todos los cambios
- Quieres escalar independientemente las cargas de lectura y escritura (CQRS)
Evita Arquitectura Dirigida por Eventos Cuando:
- Necesitas respuestas síncronas, fuertemente consistentes (ej. "¿está disponible este nombre de usuario?")
- El sistema es lo suficientemente simple para llamadas directas servicio a servicio
- Tu equipo carece de experiencia con depuración de sistemas distribuidos
- El dominio es inherentemente solicitud-respuesta (aplicaciones CRUD)
- Los requisitos de latencia son extremadamente ajustados y cada milisegundo cuenta