← Volver al listado de tecnologías

Capítulo 2: Eventos como Fuente de Verdad

Por: SiempreListo
event-sourcingeventosdiseñoddd

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:

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
CreateOrderOrderCreated
UpdateInventoryInventoryUpdated
ProcessPaymentPaymentProcessed
ShipOrderOrderShipped
CancelOrderOrderCancelled

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

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 →