Capítulo 2: Eventos como Fuente de Verdad
Capítulo 2: Eventos como Fuente de Verdad
“Un evento bien diseñado cuenta una historia completa”
Anatomía de un Evento
Un evento de dominio debe responder: ¿Qué pasó, cuándo, y con qué datos?
Antes de ver la estructura, entendamos cada componente:
- eventId: Identificador único del evento (UUID). Permite detectar duplicados y rastrear eventos específicos.
- aggregateId: Identificador del agregado al que pertenece el evento. Todos los eventos de un pedido comparten el mismo
aggregateId. - version: Número secuencial que indica el orden del evento dentro del stream del agregado. El primer evento es versión 0 o 1.
- correlationId: Identificador que conecta eventos relacionados a través de diferentes operaciones. Si crear un pedido dispara reservar inventario, ambos eventos comparten el
correlationId. - causationId: Identificador del evento que causó este evento. Permite rastrear cadenas de causa-efecto.
Estructura Base
// TypeScript
interface DomainEvent {
readonly eventId: string; // Identificador único
readonly type: string; // Tipo del evento
readonly aggregateId: string; // ID del agregado
readonly version: number; // Versión del agregado
readonly occurredAt: Date; // Cuándo ocurrió
readonly metadata: EventMetadata; // Información contextual
readonly payload: unknown; // Datos del evento
}
interface EventMetadata {
correlationId: string; // Traza de la operación
causationId: string; // Evento que lo causó
userId?: string; // Quién lo provocó
}
// Go
type DomainEvent struct {
EventID string `json:"eventId"`
Type string `json:"type"`
AggregateID string `json:"aggregateId"`
Version int `json:"version"`
OccurredAt time.Time `json:"occurredAt"`
Metadata EventMetadata `json:"metadata"`
Payload json.RawMessage `json:"payload"`
}
type EventMetadata struct {
CorrelationID string `json:"correlationId"`
CausationID string `json:"causationId"`
UserID string `json:"userId,omitempty"`
}
# Python
from dataclasses import dataclass
from datetime import datetime
from typing import Any
@dataclass(frozen=True)
class EventMetadata:
correlation_id: str
causation_id: str
user_id: str | None = None
@dataclass(frozen=True)
class DomainEvent:
event_id: str
event_type: str
aggregate_id: str
version: int
occurred_at: datetime
metadata: EventMetadata
payload: dict[str, Any]
Naming Conventions
Regla de Oro: Pasado + Acción
Los eventos ya ocurrieron, usa tiempo pasado:
| ❌ Incorrecto | ✅ Correcto |
|---|---|
| CreateOrder | OrderCreated |
| UpdateInventory | InventoryUpdated |
| ProcessPayment | PaymentProcessed |
| ShipOrder | OrderShipped |
| CancelOrder | OrderCancelled |
Estructura del Nombre
{Sustantivo}{Verbo en Pasado}
Ejemplos para nuestro sistema de pedidos:
// Eventos del Order
type OrderEvents =
| 'OrderCreated'
| 'OrderItemAdded'
| 'OrderItemRemoved'
| 'OrderConfirmed'
| 'OrderPaid'
| 'OrderShipped'
| 'OrderDelivered'
| 'OrderCancelled'
| 'OrderRefunded';
// Eventos del Inventory
type InventoryEvents =
| 'ProductStockIncreased'
| 'ProductStockDecreased'
| 'ProductStockReserved'
| 'ProductStockReleased'
| 'LowStockAlertTriggered';
// Eventos del Customer
type CustomerEvents =
| 'CustomerRegistered'
| 'CustomerEmailVerified'
| 'CustomerAddressAdded'
| 'CustomerAddressUpdated'
| 'CustomerDeactivated';
Diseño de Payloads
El payload es el contenido específico del evento: los datos que describen qué exactamente ocurrió. Es la parte que varía entre diferentes tipos de eventos.
Principio: Incluye Todo lo Necesario
El payload debe contener toda la información para reconstruir el estado. Esto significa que si alguien lee solo este evento, debe entender completamente qué pasó sin necesidad de consultar otras fuentes:
// ❌ Malo: Información incompleta
interface OrderCreatedBad {
type: 'OrderCreated';
orderId: string;
// ¿Dónde están los items? ¿El cliente?
}
// ✅ Bueno: Información completa
interface OrderCreated {
type: 'OrderCreated';
orderId: string;
customerId: string;
items: Array<{
productId: string;
productName: string; // Desnormalizado
quantity: number;
unitPrice: number;
currency: string;
}>;
shippingAddress: Address;
billingAddress: Address;
subtotal: number;
tax: number;
total: number;
currency: string;
occurredAt: Date;
}
Desnormalizacion Intencional
La desnormalizacion significa duplicar datos en lugar de referenciarlos. En bases de datos relacionales tradicionales esto se evita, pero en Event Sourcing es deliberado y necesario.
¿Por qué? Porque los eventos son inmutables e históricos. Si un producto cambia de precio mañana, el evento de hoy debe recordar el precio de hoy.
En eventos, duplicar datos es correcto:
// El precio del producto puede cambiar,
// pero el evento preserva el precio al momento de la compra
interface OrderItemAdded {
type: 'OrderItemAdded';
orderId: string;
item: {
productId: string;
productName: string; // Desnormalizado del catálogo
productSku: string; // Desnormalizado
unitPrice: number; // Precio al momento de agregar
quantity: number;
};
occurredAt: Date;
}
Implementación Práctica
TypeScript: Sistema de Tipos para Eventos
// src/domain/events/base.ts
import { z } from 'zod';
import { v4 as uuid } from 'uuid';
const EventMetadataSchema = z.object({
correlationId: z.string().uuid(),
causationId: z.string().uuid(),
userId: z.string().uuid().optional()
});
export const createEventMetadata = (
correlationId?: string,
causationId?: string,
userId?: string
): z.infer<typeof EventMetadataSchema> => ({
correlationId: correlationId ?? uuid(),
causationId: causationId ?? uuid(),
userId
});
// Factoría de eventos tipada
export function createEvent<T extends string, P>(
type: T,
aggregateId: string,
version: number,
payload: P,
metadata?: Partial<z.infer<typeof EventMetadataSchema>>
) {
return {
eventId: uuid(),
type,
aggregateId,
version,
payload,
occurredAt: new Date(),
metadata: createEventMetadata(
metadata?.correlationId,
metadata?.causationId,
metadata?.userId
)
};
}
// src/domain/events/order-events.ts
import { z } from 'zod';
import { createEvent } from './base';
// Schemas de validación
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
state: z.string(),
zipCode: z.string(),
country: z.string()
});
const OrderItemSchema = z.object({
productId: z.string().uuid(),
productName: z.string(),
sku: z.string(),
quantity: z.number().int().positive(),
unitPrice: z.number().positive(),
currency: z.string().length(3)
});
export const OrderCreatedPayloadSchema = z.object({
customerId: z.string().uuid(),
items: z.array(OrderItemSchema).min(1),
shippingAddress: AddressSchema,
subtotal: z.number().positive(),
tax: z.number().nonnegative(),
total: z.number().positive()
});
export type OrderCreatedPayload = z.infer<typeof OrderCreatedPayloadSchema>;
// Factoría específica
export const createOrderCreated = (
orderId: string,
version: number,
payload: OrderCreatedPayload
) => createEvent('OrderCreated', orderId, version, payload);
Go: Eventos con Generics
// domain/events/base.go
package events
import (
"encoding/json"
"time"
"github.com/google/uuid"
)
type EventMetadata struct {
CorrelationID string `json:"correlationId"`
CausationID string `json:"causationId"`
UserID string `json:"userId,omitempty"`
}
type Event[T any] struct {
EventID string `json:"eventId"`
Type string `json:"type"`
AggregateID string `json:"aggregateId"`
Version int `json:"version"`
Payload T `json:"payload"`
Metadata EventMetadata `json:"metadata"`
OccurredAt time.Time `json:"occurredAt"`
}
func NewEvent[T any](
eventType string,
aggregateID string,
version int,
payload T,
) Event[T] {
return Event[T]{
EventID: uuid.New().String(),
Type: eventType,
AggregateID: aggregateID,
Version: version,
Payload: payload,
Metadata: EventMetadata{
CorrelationID: uuid.New().String(),
CausationID: uuid.New().String(),
},
OccurredAt: time.Now().UTC(),
}
}
// domain/events/order_events.go
package events
type Address struct {
Street string `json:"street"`
City string `json:"city"`
State string `json:"state"`
ZipCode string `json:"zipCode"`
Country string `json:"country"`
}
type OrderItem struct {
ProductID string `json:"productId"`
ProductName string `json:"productName"`
SKU string `json:"sku"`
Quantity int `json:"quantity"`
UnitPrice float64 `json:"unitPrice"`
Currency string `json:"currency"`
}
type OrderCreatedPayload struct {
CustomerID string `json:"customerId"`
Items []OrderItem `json:"items"`
ShippingAddress Address `json:"shippingAddress"`
Subtotal float64 `json:"subtotal"`
Tax float64 `json:"tax"`
Total float64 `json:"total"`
}
func NewOrderCreated(
orderID string,
version int,
payload OrderCreatedPayload,
) Event[OrderCreatedPayload] {
return NewEvent("OrderCreated", orderID, version, payload)
}
Python: Eventos con Pydantic
# domain/events/base.py
from datetime import datetime
from uuid import uuid4
from pydantic import BaseModel, Field
class EventMetadata(BaseModel):
correlation_id: str = Field(default_factory=lambda: str(uuid4()))
causation_id: str = Field(default_factory=lambda: str(uuid4()))
user_id: str | None = None
class DomainEvent(BaseModel):
event_id: str = Field(default_factory=lambda: str(uuid4()))
event_type: str
aggregate_id: str
version: int
metadata: EventMetadata = Field(default_factory=EventMetadata)
occurred_at: datetime = Field(default_factory=datetime.utcnow)
class Config:
frozen = True
# domain/events/order_events.py
from pydantic import BaseModel
from .base import DomainEvent
class Address(BaseModel):
street: str
city: str
state: str
zip_code: str
country: str
class OrderItem(BaseModel):
product_id: str
product_name: str
sku: str
quantity: int
unit_price: float
currency: str
class OrderCreatedPayload(BaseModel):
customer_id: str
items: list[OrderItem]
shipping_address: Address
subtotal: float
tax: float
total: float
class OrderCreated(DomainEvent):
event_type: str = "OrderCreated"
payload: OrderCreatedPayload
Testing de Eventos
// tests/events/order-events.test.ts
import { describe, it, expect } from 'vitest';
import { createOrderCreated, OrderCreatedPayloadSchema } from './order-events';
describe('OrderCreated Event', () => {
it('should create valid event with all required fields', () => {
const payload = {
customerId: '123e4567-e89b-12d3-a456-426614174000',
items: [{
productId: '123e4567-e89b-12d3-a456-426614174001',
productName: 'Widget',
sku: 'WDG-001',
quantity: 2,
unitPrice: 29.99,
currency: 'USD'
}],
shippingAddress: {
street: '123 Main St',
city: 'Springfield',
state: 'IL',
zipCode: '62701',
country: 'US'
},
subtotal: 59.98,
tax: 4.80,
total: 64.78
};
const event = createOrderCreated('order-123', 1, payload);
expect(event.type).toBe('OrderCreated');
expect(event.aggregateId).toBe('order-123');
expect(event.version).toBe(1);
expect(event.payload).toEqual(payload);
expect(event.eventId).toBeDefined();
expect(event.occurredAt).toBeInstanceOf(Date);
});
it('should reject invalid payload', () => {
const invalidPayload = {
customerId: 'not-a-uuid',
items: [] // Empty array
};
expect(() => OrderCreatedPayloadSchema.parse(invalidPayload))
.toThrow();
});
});
Resumen
- Los eventos usan tiempo pasado en su nombre
- Incluyen toda la información necesaria para reconstruir estado
- La desnormalización es intencional y correcta
- Usa schemas de validación para garantizar integridad
- Los metadatos permiten trazabilidad y debugging
Glosario
Payload
Definicion: El contenido especifico de un evento; los datos que describen exactamente que ocurrio.
Por que es importante: El payload debe ser auto-contenido y proporcionar toda la informacion necesaria para entender y procesar el evento sin consultar otras fuentes.
Ejemplo practico: En un evento OrderItemAdded, el payload incluye productId, productName, quantity, unitPrice - no solo el ID del producto, sino toda la informacion relevante al momento de agregar.
Metadata (Metadatos)
Definicion: Informacion contextual sobre el evento que no es parte del hecho de negocio en si: quien lo causo, cuando, como se relaciona con otros eventos.
Por que es importante: Permite debugging, auditorias de seguridad, y rastreo de operaciones distribuidas. Sin metadata, seria dificil responder “quien hizo esto” o “que operacion causo estos cambios”.
Ejemplo practico: { correlationId: "abc-123", causationId: "xyz-789", userId: "user-456", timestamp: "2024-01-15T10:30:00Z" } - nos dice que el usuario 456 genero este evento como parte de la operacion abc-123.
Correlation ID
Definicion: Identificador unico que conecta todos los eventos generados por una misma operacion de usuario o proceso de negocio.
Por que es importante: En sistemas distribuidos, una accion del usuario puede generar eventos en multiples servicios. El correlationId permite rastrear toda la cadena.
Ejemplo practico: El usuario hace clic en “Comprar”. Esto genera: OrderCreated (servicio de pedidos), StockReserved (servicio de inventario), PaymentInitiated (servicio de pagos). Todos comparten el mismo correlationId para poder rastrear la compra completa.
Causation ID
Definicion: Identificador del evento que directamente causo este evento.
Por que es importante: Permite reconstruir la cadena de causa-efecto entre eventos. Mientras correlationId agrupa, causationId establece jerarquia.
Ejemplo practico: OrderConfirmed tiene causationId del comando original. Cuando este evento dispara StockConfirmed, el causationId de este ultimo apunta a OrderConfirmed.
Desnormalizacion
Definicion: Practica de duplicar datos en lugar de referenciarlos por ID, contrario a la normalizacion de bases de datos relacionales.
Por que es importante: Los eventos son inmutables e historicos. Si referencias un producto por ID y el producto cambia, perderas el contexto historico. Duplicar garantiza que el evento capture el estado exacto al momento del hecho.
Ejemplo practico: En lugar de guardar { productId: "123" }, guardas { productId: "123", productName: "Widget Pro", unitPrice: 29.99 }. Si el precio cambia a $39.99 manana, el evento historico mantiene el precio correcto de $29.99.
Schema de Validacion
Definicion: Definicion formal de la estructura esperada de un evento, utilizada para validar que los datos son correctos antes de persistirlos.
Por que es importante: Los eventos son inmutables; no puedes corregir errores despues. Validar con schemas previene guardar datos corruptos o incompletos.
Ejemplo practico: Usando Zod en TypeScript: z.object({ orderId: z.string().uuid(), total: z.number().positive() }) rechazara eventos con orderId invalido o totales negativos.
Naming Convention (Convencion de Nombres)
Definicion: Reglas consistentes para nombrar eventos. En Event Sourcing: Sustantivo + Verbo en Pasado.
Por que es importante: Nombres consistentes hacen el codigo legible y expresan claramente la intencion. El tiempo pasado refuerza que los eventos son hechos que ya ocurrieron.
Ejemplo practico: OrderCreated (correcto) vs CreateOrder (incorrecto). El primero es un hecho consumado; el segundo suena como una accion por hacer (comando).
← Capítulo 1: Introducción | Capítulo 3: Agregados y Boundaries →