Event Sourcing vs CRUD: ¿cuándo usar cada uno?

7 min read

Un cliente de una fintech (empresa de tecnología financiera) llama a soporte: “Mi saldo dice $500 pero yo tenía $800 la semana pasada. ¿Qué pasó?”. El agente de soporte abre el sistema y encuentra… una fila en la base de datos que dice balance = 500. Nada más. La historia de cómo se llegó a ese número no existe.

Ese es el límite de CRUD (Create, Read, Update, Delete — Crear, Leer, Actualizar, Borrar): cada UPDATE sobrescribe el pasado. Para la mayoría de las aplicaciones eso está bien. Pero para dominios donde la historia importa — finanzas, salud, logística, auditoría — perder el “cómo llegamos aquí” puede ser un problema legal, no solo técnico.

Ahí es donde entra Event Sourcing (Abastecimiento de Eventos): en lugar de guardar el estado actual, guardamos cada cambio como un evento inmutable. El estado actual se calcula reproduciendo todos los eventos.


El diagrama


CRUD: el enfoque tradicional

En CRUD, la base de datos guarda el estado actual y solo el estado actual:


UPDATE accounts SET balance = 500 WHERE id = 'ACC-001';
-- El valor anterior ($800) desapareció para siempre

Ventajas: es simple, todos lo conocen, las herramientas lo soportan nativamente, y las consultas son directas. Para un catálogo de productos, un CRM (Customer Relationship Management — Gestión de Relaciones con Clientes) básico, o la configuración de usuarios, CRUD es la elección correcta.

Event Sourcing: la historia completa

En Event Sourcing, nunca actualizamos ni borramos. Solo agregamos eventos:


1. AccountOpened   { accountId: "ACC-001", initialBalance: 0 }
2. MoneyDeposited  { accountId: "ACC-001", amount: 800 }
3. MoneyWithdrawn  { accountId: "ACC-001", amount: 300 }

¿El saldo actual? Se calcula reproduciendo los eventos: 0 + 800 – 300 = $500. Y la pregunta del cliente de soporte ahora tiene respuesta completa: “El lunes retiró $300 desde la app móvil”.


Guía paso a paso

1 — Definir los eventos del dominio

Los eventos se nombran en pasado porque representan hechos que ya ocurrieron: MoneyDeposited, OrderShipped, UserRegistered. Un evento nunca se modifica ni se borra.

2 — Crear el Event Store

Es la lista ordenada de eventos por entidad (llamada “agregado” en DDD — Domain-Driven Design — Diseño Guiado por el Dominio). Puede ser una tabla en tu base de datos actual o una herramienta especializada como EventStoreDB.

3 — Implementar el replay

El estado actual de una entidad se reconstruye aplicando todos sus eventos en orden. Esta función debe ser determinística: mismos eventos, mismo estado, siempre.

4 — Agregar snapshots si es necesario

Si una cuenta tiene 50,000 eventos, reproducirlos todos en cada consulta es lento. Un snapshot (instantánea) guarda el estado calculado hasta cierto punto, y el replay solo procesa los eventos posteriores.

5 — Validar los comandos contra el estado actual

Antes de agregar un evento, reconstruí el estado y validá la operación. Por ejemplo: no se puede retirar $300 si el saldo reconstruido es $100.


El código

Implementemos una cuenta bancaria con Event Sourcing en Java puro. Usamos las anotaciones de Lombok @Getter y @AllArgsConstructor para mantener las clases de eventos limpias.


import lombok.AllArgsConstructor;
import lombok.Getter;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

// --- Eventos del dominio (inmutables, en pasado) ---

interface AccountEvent {
    String getAccountId();
    Instant getTimestamp();
}

@Getter
@AllArgsConstructor
class AccountOpened implements AccountEvent {
    private final String accountId;
    private final String ownerName;
    private final Instant timestamp;
}

@Getter
@AllArgsConstructor
class MoneyDeposited implements AccountEvent {
    private final String accountId;
    private final double amount;
    private final String channel; // "APP", "BRANCH", "TRANSFER"
    private final Instant timestamp;
}

@Getter
@AllArgsConstructor
class MoneyWithdrawn implements AccountEvent {
    private final String accountId;
    private final double amount;
    private final String channel;
    private final Instant timestamp;
}

// --- El Event Store ---

class EventStore {
    private final List<AccountEvent> events = new ArrayList<>();

    public void append(AccountEvent event) {
        events.add(event); // Solo agregamos. Nunca UPDATE, nunca DELETE.
        System.out.println("[EventStore] Evento guardado: " + event.getClass().getSimpleName());
    }

    public List<AccountEvent> getEventsFor(String accountId) {
        return events.stream()
                .filter(e -> e.getAccountId().equals(accountId))
                .toList();
    }
}

// --- El agregado: reconstruye su estado desde los eventos ---

@Getter
class BankAccount {
    private String accountId;
    private String ownerName;
    private double balance;

    // Replay: reconstruir el estado desde la historia
    public static BankAccount replay(List<AccountEvent> events) {
        BankAccount account = new BankAccount();
        for (AccountEvent event : events) {
            account.apply(event);
        }
        return account;
    }

    private void apply(AccountEvent event) {
        if (event instanceof AccountOpened opened) {
            this.accountId = opened.getAccountId();
            this.ownerName = opened.getOwnerName();
            this.balance = 0;
        } else if (event instanceof MoneyDeposited deposited) {
            this.balance += deposited.getAmount();
        } else if (event instanceof MoneyWithdrawn withdrawn) {
            this.balance -= withdrawn.getAmount();
        }
    }
}

// --- Servicio de comandos: valida contra el estado reconstruido ---

@AllArgsConstructor
class AccountCommandService {
    private final EventStore eventStore;

    public void withdraw(String accountId, double amount, String channel) {
        // 1. Reconstruir el estado actual
        BankAccount account = BankAccount.replay(eventStore.getEventsFor(accountId));

        // 2. Validar la regla de negocio
        if (account.getBalance() < amount) {
            throw new IllegalStateException("Fondos insuficientes. Saldo actual: $" + account.getBalance());
        }

        // 3. Registrar el evento
        eventStore.append(new MoneyWithdrawn(accountId, amount, channel, Instant.now()));
    }
}

// --- Main ---

public class EventSourcingDemo {

    public static void main(String[] args) {
        EventStore eventStore = new EventStore();
        AccountCommandService commandService = new AccountCommandService(eventStore);

        // La historia de la cuenta
        eventStore.append(new AccountOpened("ACC-001", "María Rodríguez", Instant.now()));
        eventStore.append(new MoneyDeposited("ACC-001", 800, "TRANSFER", Instant.now()));
        commandService.withdraw("ACC-001", 300, "APP");

        // Reconstruir el estado actual desde los eventos
        BankAccount account = BankAccount.replay(eventStore.getEventsFor("ACC-001"));
        System.out.println("\nSaldo actual de " + account.getOwnerName() + ": $" + account.getBalance());

        // La auditoría completa está disponible
        System.out.println("\n--- Historia completa de la cuenta ---");
        eventStore.getEventsFor("ACC-001").forEach(e ->
            System.out.println(e.getTimestamp() + " | " + e.getClass().getSimpleName()));

        // Las reglas de negocio se validan contra el estado reconstruido
        try {
            commandService.withdraw("ACC-001", 10000, "APP");
        } catch (IllegalStateException e) {
            System.out.println("\nOperación rechazada: " + e.getMessage());
        }
    }
}

Output esperado:


[EventStore] Evento guardado: AccountOpened
[EventStore] Evento guardado: MoneyDeposited
[EventStore] Evento guardado: MoneyWithdrawn

Saldo actual de María Rodríguez: $500.0

--- Historia completa de la cuenta ---
2026-07-19T... | AccountOpened
2026-07-19T... | MoneyDeposited
2026-07-19T... | MoneyWithdrawn

Operación rechazada: Fondos insuficientes. Saldo actual: $500.0

¿Cuándo usar cada uno?

CriterioCRUDEvent Sourcing
Auditoría obligatoriaRequiere tablas de logs extraViene incluida por diseño
Complejidad del equipoBaja — todos lo conocenAlta — requiere aprendizaje
Consultas al estado actualDirectas y rápidasRequieren replay o proyecciones
Análisis temporal (“¿cómo estaba el sistema el martes?”)Imposible sin backupsNatural — reproducir hasta esa fecha
Corrección de errores históricosUPDATE y se perdió la evidenciaEvento de compensación con rastro completo

Para tener en cuenta en producción

  • Empezá híbrido: no necesitás Event Sourcing en toda la aplicación. Aplicalo solo en los agregados donde la historia importa (pagos, órdenes) y usá CRUD para el resto (catálogos, configuración).
  • CQRS es el complemento natural: Command Query Responsibility Segregation (Segregación de Responsabilidades de Comandos y Consultas) separa la escritura (eventos) de la lectura (proyecciones optimizadas para consulta). Casi todos los sistemas con Event Sourcing serios lo usan.
  • El versionado de eventos es el verdadero reto: tus eventos de hace dos años deben poder reproducirse hoy, aunque la estructura haya cambiado. Definí una estrategia de versionado desde el día uno.
  • Cuidado con los datos personales: regulaciones como GDPR (General Data Protection Regulation — Reglamento General de Protección de Datos) exigen poder borrar datos de usuarios, y los eventos son inmutables. La técnica común es cifrar los datos personales y “olvidar” la llave.

Conclusión

CRUD y Event Sourcing no son competidores, son herramientas para problemas distintos. CRUD es la opción por defecto y está bien que así sea: es simple y suficiente para la mayoría de los casos. Event Sourcing brilla cuando el negocio pregunta no solo “¿cuál es el estado?” sino “¿cómo llegamos a este estado?”.

Si tu equipo de soporte alguna vez tuvo que decir “no sabemos qué pasó con esos datos”, ya tenés tu primer candidato para Event Sourcing.

Recordá suscribirte aquí para recibir los próximos posts directamente en tu correo.


Referencias:
Implementing Domain-Driven Design — Vaughn Vernon
Microservices Patterns — Chris Richardson
Martin Fowler — Event Sourcing: https://martinfowler.com/eaaDev/EventSourcing.html

Leave a Reply

Your email address will not be published. Required fields are marked *