← Volver al listado de tecnologías

Capítulo 1: Introducción a Event Sourcing

Por: SiempreListo
event-sourcingarquitecturafundamentos

Capítulo 1: Introducción a Event Sourcing

“El estado actual es solo una proyección de todos los eventos que han ocurrido”

¿Qué es Event Sourcing?

Event Sourcing es un patrón arquitectónico donde el estado de una aplicación se determina por una secuencia de eventos. En lugar de almacenar solo el estado actual, almacenamos todos los eventos que llevaron a ese estado.

Analogía: La Cuenta Bancaria

Imagina tu cuenta bancaria:

Enfoque tradicional (CRUD):

Balance actual: $1,500

Enfoque Event Sourcing:

1. AccountOpened: $0
2. MoneyDeposited: +$2,000
3. MoneyWithdrawn: -$500
4. MoneyDeposited: +$1,000
5. MoneyWithdrawn: -$1,000
---
Balance calculado: $1,500

¿Por qué Event Sourcing?

Ventajas

VentajaDescripción
Auditoría completaHistorial inmutable de todo lo ocurrido
Debugging temporalReproduce el estado en cualquier punto del tiempo
Análisis de negocioLos eventos contienen intención, no solo datos
DesacoplamientoOtros sistemas reaccionan a eventos
RecuperaciónReconstruye el estado desde los eventos

Desventajas

DesventajaMitigación
Complejidad inicialFrameworks y librerías maduras
Consistencia eventualDiseño cuidadoso de agregados
Espacio de almacenamientoSnapshots y archivado
Curva de aprendizajeEste tutorial

Cuándo Usar Event Sourcing

Casos Ideales

Cuándo NO Usarlo

Conceptos Fundamentales

Antes de ver código, es importante entender los términos clave que usaremos a lo largo de todo el tutorial. Si estos conceptos son nuevos para ti, no te preocupes: los explicaremos en detalle.

graph LR
    A[Comando] --> B[Agregado]
    B --> C[Evento]
    C --> D[Event Store]
    D --> E[Proyección]
    E --> F[Read Model]

1. Eventos (Domain Events)

Un evento de dominio es un registro inmutable de algo que ocurrió en el sistema. A diferencia de los datos en una base de datos tradicional que pueden modificarse, un evento una vez creado nunca cambia.

Piensa en los eventos como las entradas de un diario: escribes lo que pasó y nunca lo borras ni lo modificas.

Hechos inmutables que ocurrieron en el pasado:

// TypeScript
interface OrderCreated {
  type: 'OrderCreated';
  orderId: string;
  customerId: string;
  items: OrderItem[];
  occurredAt: Date;
}

interface OrderShipped {
  type: 'OrderShipped';
  orderId: string;
  trackingNumber: string;
  occurredAt: Date;
}
// Go
type OrderCreated struct {
    Type       string    `json:"type"`
    OrderID    string    `json:"orderId"`
    CustomerID string    `json:"customerId"`
    Items      []Item    `json:"items"`
    OccurredAt time.Time `json:"occurredAt"`
}
# Python
@dataclass(frozen=True)
class OrderCreated:
    order_id: str
    customer_id: str
    items: list[OrderItem]
    occurred_at: datetime

2. Event Store

El Event Store es una base de datos especializada (o una tabla/colección en una base de datos tradicional) diseñada específicamente para almacenar eventos. Sus características principales son:

Base de datos optimizada para almacenar y recuperar eventos:

-- PostgreSQL
CREATE TABLE events (
    id BIGSERIAL PRIMARY KEY,
    stream_id VARCHAR(255) NOT NULL,
    version INT NOT NULL,
    type VARCHAR(255) NOT NULL,
    data JSONB NOT NULL,
    metadata JSONB,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(stream_id, version)
);

CREATE INDEX idx_events_stream ON events(stream_id, version);

3. Agregados (Aggregates)

Un agregado es un concepto de Domain-Driven Design (DDD) que representa un grupo de objetos relacionados que se tratan como una unidad para propósitos de cambios de datos.

En Event Sourcing, el agregado tiene una responsabilidad adicional crucial:

El array private events: DomainEvent[] que verás en el código es la lista de eventos que el agregado ha generado pero que aún no se han guardado en el Event Store. A estos se les llama “eventos no comprometidos” (uncommitted events).

Entidades que agrupan eventos relacionados:

class Order {
  private events: DomainEvent[] = [];

  constructor(
    private id: string,
    private status: OrderStatus,
    private items: OrderItem[]
  ) {}

  static create(id: string, customerId: string, items: OrderItem[]): Order {
    const order = new Order(id, 'pending', items);
    order.apply(new OrderCreated(id, customerId, items));
    return order;
  }

  ship(trackingNumber: string): void {
    if (this.status !== 'paid') {
      throw new Error('Order must be paid before shipping');
    }
    this.apply(new OrderShipped(this.id, trackingNumber));
  }

  private apply(event: DomainEvent): void {
    this.events.push(event);
    this.when(event);
  }

  private when(event: DomainEvent): void {
    switch (event.type) {
      case 'OrderCreated':
        this.status = 'pending';
        break;
      case 'OrderShipped':
        this.status = 'shipped';
        break;
    }
  }
}

4. Proyecciones (Projections)

Una proyección es un proceso que “escucha” los eventos y los transforma en una estructura de datos optimizada para consultas.

Imagina que tienes miles de eventos de pedidos. Buscar “todos los pedidos del cliente X” sería muy lento si tuvieras que recorrer todos los eventos. La proyección crea una tabla orders_view que mantiene actualizada con cada evento, permitiendo consultas rápidas.

Las proyecciones crean lo que llamamos Read Models (modelos de lectura): estructuras de datos optimizadas para responder preguntas específicas del negocio.

Transforman eventos en modelos de lectura optimizados:

class OrderProjection {
  async handle(event: DomainEvent): Promise<void> {
    switch (event.type) {
      case 'OrderCreated':
        await this.db.insert('orders_view', {
          id: event.orderId,
          customer_id: event.customerId,
          status: 'pending',
          total: this.calculateTotal(event.items),
          created_at: event.occurredAt
        });
        break;

      case 'OrderShipped':
        await this.db.update('orders_view',
          { id: event.orderId },
          { status: 'shipped', tracking: event.trackingNumber }
        );
        break;
    }
  }
}

Ejercicio Práctico

Configura tu entorno de desarrollo:

# Crear proyecto
mkdir orderflow-es && cd orderflow-es

# Inicializar con Bun
bun init -y

# Instalar dependencias
bun add zod uuid
bun add -d typescript @types/node vitest

# Crear estructura
mkdir -p src/{domain,infrastructure,application}

Crea tu primer evento:

// src/domain/events/order-events.ts
import { z } from 'zod';

export const OrderCreatedSchema = z.object({
  type: z.literal('OrderCreated'),
  orderId: z.string().uuid(),
  customerId: z.string().uuid(),
  items: z.array(z.object({
    productId: z.string(),
    quantity: z.number().positive(),
    price: z.number().positive()
  })),
  occurredAt: z.date()
});

export type OrderCreated = z.infer<typeof OrderCreatedSchema>;

Resumen

Glosario

Event Sourcing

Definicion: Patron arquitectonico donde el estado de una aplicacion se determina por una secuencia de eventos en lugar de almacenar solo el estado actual.

Por que es importante: Proporciona un historial completo e inmutable de todos los cambios, permitiendo auditorias, debugging temporal y reconstruccion del estado en cualquier punto del tiempo.

Ejemplo practico: En lugar de guardar balance: $1500 en una cuenta bancaria, guardas todos los depositos y retiros: Deposito +$2000, Retiro -$500. El balance se calcula sumando todos los eventos.


Evento de Dominio (Domain Event)

Definicion: Un registro inmutable de algo que ocurrio en el sistema, expresado en tiempo pasado (OrderCreated, PaymentReceived).

Por que es importante: Los eventos capturan no solo los datos sino tambien la intencion del negocio. “OrderCancelled” nos dice mas que simplemente ver un campo status: cancelled.

Ejemplo practico: OrderShipped { orderId: "123", trackingNumber: "FEDEX456", shippedAt: "2024-01-15" } - contiene toda la informacion del hecho que ocurrio.


Event Store

Definicion: Base de datos especializada (o tabla) disenada para almacenar eventos de forma append-only, ordenada y agrupada por streams.

Por que es importante: Es la “fuente de verdad” del sistema. Todos los eventos se persisten aqui y desde aqui se reconstruye el estado.

Ejemplo practico: Una tabla PostgreSQL con columnas stream_id, version, event_type, data, created_at donde cada fila es un evento inmutable.


Agregado (Aggregate)

Definicion: Cluster de objetos de dominio que se tratan como una unidad para cambios de datos. Tiene una “raiz” que es el punto de entrada.

Por que es importante: Define los limites de consistencia transaccional. Todo cambio dentro de un agregado es atomico y las reglas de negocio se validan dentro del agregado.

Ejemplo practico: Un Order (pedido) es un agregado que contiene OrderItems. No puedes modificar un item directamente; debes hacerlo a traves del Order que valida las reglas (como “no puedes agregar items a un pedido ya enviado”).


Proyeccion (Projection)

Definicion: Proceso que transforma eventos en estructuras de datos optimizadas para consultas especificas.

Por que es importante: Los eventos son excelentes para escritura pero ineficientes para lectura. Las proyecciones crean “vistas materializadas” que responden consultas rapidamente.

Ejemplo practico: Una proyeccion escucha eventos OrderCreated, OrderShipped, etc., y mantiene actualizada una tabla orders_view con columnas como id, customer_name, status, total para mostrar en un dashboard.


Read Model (Modelo de Lectura)

Definicion: Estructura de datos creada por una proyeccion, optimizada para responder consultas especificas del negocio.

Por que es importante: Permite separar las necesidades de escritura (eventos) de las necesidades de lectura (consultas), optimizando cada una independientemente.

Ejemplo practico: Una tabla customer_stats con total_orders, lifetime_value, last_purchase_date que se actualiza con cada evento de pedido, permitiendo consultas rapidas para el equipo de marketing.


CRUD

Definicion: Create, Read, Update, Delete - el patron tradicional de manipulacion de datos donde se modifica directamente el estado actual.

Por que es importante: Es el enfoque mas comun y sencillo, pero pierde el historial de cambios. Event Sourcing es una alternativa cuando necesitas trazabilidad completa.

Ejemplo practico: En CRUD, actualizas UPDATE orders SET status = 'shipped' WHERE id = 123. El estado anterior se pierde. En Event Sourcing, agregas un evento OrderShipped y el historial completo se preserva.


← Volver al Índice | Capítulo 2: Eventos como Fuente de Verdad →