# Día 25: CQRS y Event Sourcing: Modelos de Lectura y Estado como Historia

> Separar comandos y consultas, construir proyecciones desde eventos, guardar el estado como secuencia de eventos y saber cuándo no merece la pena

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 25 de 30
- Fase 6: Mensajería Asíncrona con Kafka, RabbitMQ, EDA y CQRS
- Dedicación: 4h
- Profundidad: visión de conjunto
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/cqrs-event-sourcing/

## Al terminar este día sabrás

- Separar el modelo de escritura y el de lectura con CQRS cuando aporta valor.
- Construir proyecciones que se puedan borrar y reconstruir desde los eventos.
- Reconstruir un agregado a partir de sus eventos y guardarlos con concurrencia optimista.
- Evolucionar eventos ya guardados con versionado y snapshots, y saber cuándo no usar estos patrones.


## Conceptos

### 1. CQRS: Command Query Responsibility Segregation

CQRS separa el modelo de datos en dos lados: el lado de **Comando** (escritura) optimizado para validar y persistir cambios de estado, y el lado de **Consulta** (lectura) optimizado para devolver datos desnormalizados y rápidos de leer. Los comandos modifican el estado y publican eventos; las consultas leen vistas materializadas actualizadas por esos eventos.

**Cómo funciona por dentro.**

CQRS separa las operaciones que cambian el estado (comandos) de las que lo leen (consultas), usando modelos distintos para cada lado. El modelo de escritura se centra en validar reglas de negocio e invariantes; el de lectura, en servir consultas rápidas con la forma exacta que necesita cada pantalla o API, a menudo desnormalizado.

No implica obligatoriamente dos bases de datos. El nivel más sencillo es el mismo almacenamiento con dos modelos en el código (entidades ricas para escribir, proyecciones o vistas SQL para leer). El nivel avanzado usa almacenes separados (PostgreSQL para escribir, Elasticsearch o una tabla desnormalizada para leer) sincronizados mediante eventos, con consistencia eventual.

Tiene sentido cuando lecturas y escrituras son muy asimétricas en volumen o forma: un panel que agrega datos de muchas tablas, búsquedas de texto completo o lecturas que se cuentan por miles frente a pocas escrituras.

*Comando, consulta y proyección actualizada por eventos*

```java
// Lado de escritura: comando con validación de invariantes
public record AsignarTarea(long tareaId, String responsable) {}

@Service
class ComandosTarea {
    @Transactional
    public void manejar(AsignarTarea cmd) {
        var tarea = repo.findById(cmd.tareaId()).orElseThrow();
        tarea.asignarA(cmd.responsable());                    // reglas del dominio
        eventos.publishEvent(new TareaAsignada(cmd.tareaId(), cmd.responsable()));
    }
}

// Lado de lectura: tabla desnormalizada pensada para el panel
public record ResumenResponsable(String responsable, int pendientes, int completadasSemana) {}

@Component
class ProyeccionPanel {
    private final JdbcClient jdbc;
    ProyeccionPanel(JdbcClient jdbc) { this.jdbc = jdbc; }

    @TransactionalEventListener
    void on(TareaAsignada e) {
        jdbc.sql("""
            INSERT INTO panel_responsable (responsable, pendientes) VALUES (:r, 1)
            ON CONFLICT (responsable) DO UPDATE SET pendientes = panel_responsable.pendientes + 1
            """).param("r", e.responsable()).update();
    }

    public List<ResumenResponsable> panel() {                // consulta trivial y rápida
        return jdbc.sql("SELECT * FROM panel_responsable ORDER BY pendientes DESC")
            .query(ResumenResponsable.class).list();
    }
}
```

- **En una línea:** `CommandHandler -> eventStore.save(evento) -> EventListener -> readModelRepository.update(vistaConsulta)`
- **Por qué importa:** Permite optimizar escrituras y lecturas de forma independiente: la base de datos de escritura puede ser relacional normalizada y la de lectura puede ser un Elasticsearch, Redis o vista desnormalizada en PostgreSQL.
- **Error típico:** Aplicar CQRS a un CRUD simple donde las operaciones de lectura y escritura tienen las mismas necesidades de rendimiento. CQRS añade complejidad significativa que solo se justifica cuando hay asimetría entre lecturas y escrituras.
- **Java 17 frente a 21:** Patrón arquitectónico independiente de versión.
- **Se conecta con:** Event Sourcing, Vistas Materializadas, Escalabilidad Asimétrica

### 2. Proyecciones: Modelos de Lectura Construidos desde Eventos

Una proyección escucha eventos y mantiene una vista pensada para una consulta concreta: un resumen de pedidos por cliente, un panel de ventas por día. Es desechable: se puede borrar y reconstruir reproduciendo los eventos desde el principio.

**Cómo funciona por dentro.**

Una proyección es un consumidor de eventos cuyo único trabajo es mantener una vista para leer. Recibe `PedidoConfirmado`, `PedidoCancelado`, `LineaAnadida`… y actualiza una tabla con la forma exacta que necesita una pantalla o un informe: sin joins ni cálculos en cada consulta. Puede haber tantas proyecciones como consultas distintas, cada una en el almacenamiento que mejor le venga (una tabla SQL, un índice de búsqueda, una caché).

Lo que las hace valiosas es que son desechables. Como toda la información está en los eventos, una proyección se puede borrar y reconstruir reproduciéndolos desde el principio. Eso permite corregir un bug en la vista, añadir una columna o crear un informe nuevo sobre datos históricos. Para que funcione, la proyección tiene que ser idempotente (el mismo evento dos veces no la altera) y recordar hasta qué evento ha procesado, para continuar donde lo dejó.

El precio es la consistencia eventual: entre el comando y la actualización de la vista pasa un tiempo. La interfaz debe contar con ello, por ejemplo devolviendo el resultado del comando directamente en lugar de releerlo de la proyección.

*Proyección idempotente y reconstruible*

```java
@Component
class ResumenClienteProyeccion {
    private final JdbcTemplate jdbc;

    ResumenClienteProyeccion(JdbcTemplate jdbc) { this.jdbc = jdbc; }

    @Transactional
    public void aplicar(EventoPedido evento, long posicion) {
        // Idempotencia: si esta posición ya se procesó, no se hace nada
        int avanza = jdbc.update("""
            update proyeccion_estado set posicion = ?
            where nombre = 'resumen_cliente' and posicion < ?""", posicion, posicion);
        if (avanza == 0) return;

        switch (evento) {
            case PedidoConfirmado(var pedidoId, var clienteId, var total, var cuando) -> jdbc.update("""
                insert into resumen_cliente (cliente_id, pedidos, total, ultimo_pedido)
                values (?, 1, ?, ?)
                on conflict (cliente_id) do update set
                    pedidos = resumen_cliente.pedidos + 1,
                    total = resumen_cliente.total + excluded.total,
                    ultimo_pedido = excluded.ultimo_pedido""", clienteId, total, cuando);
            case PedidoCancelado(var pedidoId, var clienteId, var total) -> jdbc.update("""
                update resumen_cliente set pedidos = pedidos - 1, total = total - ?
                where cliente_id = ?""", total, clienteId);
            default -> { }                           // eventos que esta vista no necesita
        }
    }

    // Reconstruir: vaciar la vista y reproducir todos los eventos desde el principio
    public void reconstruir(EventStore store) {
        jdbc.update("truncate resumen_cliente");
        jdbc.update("update proyeccion_estado set posicion = 0 where nombre = 'resumen_cliente'");
        store.todosDesde(0).forEach(e -> aplicar(e.evento(), e.posicion()));
    }
}
```

- **En una línea:** `resumen_cliente(cliente_id, pedidos, total, ultimo_pedido)` actualizada al recibir `PedidoConfirmado` y `PedidoCancelado`.
- **Por qué importa:** Cada pantalla lee una tabla con la forma exacta que necesita, sin joins ni cálculos al vuelo, y se pueden crear vistas nuevas sobre datos históricos.
- **Error típico:** Escribir en la proyección desde el lado de comandos «para que esté al día»: deja de ser reconstruible y aparecen dos fuentes de verdad.
- **Java 17 frente a 21:** Igual en Java 17 y 21.
- **Se conecta con:** CQRS, Consistencia eventual, Consumidores idempotentes

### 3. Event Sourcing: El Estado como Secuencia de Eventos

En vez de guardar el estado actual de una entidad en una fila de base de datos, Event Sourcing persiste la secuencia completa de eventos que han modificado esa entidad. El estado actual se reconstruye "reproduciendo" (replay) todos los eventos desde el primero. Es como el extracto bancario: no guardas solo el saldo final, sino cada transacción individual.

**Cómo funciona por dentro.**

Con Event Sourcing no guardas el estado actual de una entidad, sino la secuencia completa de eventos que le ocurrieron. El estado se obtiene aplicando los eventos en orden desde el principio (un pliegue o fold). La tabla de eventos es la fuente de verdad y solo admite inserciones.

Ventajas: un historial de auditoría completo y fiable por diseño, la posibilidad de reconstruir el estado en cualquier momento del pasado y de crear nuevas proyecciones de lectura reprocesando todos los eventos. Para agregados con miles de eventos, se guardan instantáneas (snapshots) periódicas y se aplica solo lo posterior.

Inconvenientes: es un cambio de mentalidad grande, las consultas siempre van por proyecciones (casi obliga a CQRS), y versionar eventos es delicado porque los antiguos no se pueden modificar: hay que convertirlos al leerlos (upcasting). Encaja en dominios donde el historial es el negocio: contabilidad, logística, sistemas regulados.

*Agregado que se reconstruye a partir de eventos*

```java
sealed interface EventoCuenta permits CuentaAbierta, DineroIngresado, DineroRetirado {}
record CuentaAbierta(String id, String titular) implements EventoCuenta {}
record DineroIngresado(String id, BigDecimal importe) implements EventoCuenta {}
record DineroRetirado(String id, BigDecimal importe) implements EventoCuenta {}

final class Cuenta {
    private String id;
    private BigDecimal saldo = BigDecimal.ZERO;
    private final List<EventoCuenta> pendientes = new ArrayList<>();

    static Cuenta reconstruir(List<EventoCuenta> historial) {
        var cuenta = new Cuenta();
        historial.forEach(cuenta::aplicar);                 // estado = fold(eventos)
        return cuenta;
    }

    void retirar(BigDecimal importe) {
        if (saldo.compareTo(importe) < 0) throw new SaldoInsuficienteException(id);
        registrar(new DineroRetirado(id, importe));         // decide y registra, no muta directamente
    }

    private void registrar(EventoCuenta e) { aplicar(e); pendientes.add(e); }

    private void aplicar(EventoCuenta e) {
        switch (e) {
            case CuentaAbierta a -> id = a.id();
            case DineroIngresado i -> saldo = saldo.add(i.importe());
            case DineroRetirado r -> saldo = saldo.subtract(r.importe());
        }
    }

    List<EventoCuenta> eventosPendientes() { return List.copyOf(pendientes); }
}
```

- **En una línea:** `List<EventoPedido> eventos = eventStore.findByAggregateId(pedidoId); Pedido pedido = Pedido.reconstruct(eventos);`
- **Por qué importa:** Proporciona un audit trail completo e inmutable, permite reconstruir el estado en cualquier punto temporal, y facilita la depuración de errores de negocio.
- **Error típico:** Implementar Event Sourcing sin mecanismo de Snapshots; con millones de eventos, reconstruir una entidad desde el primer evento tarda minutos.
- **Java 17 frente a 21:** Patrón independiente de versión.
- **Se conecta con:** Kafka como Event Store, Snapshots, Proyecciones

### 4. Event Store: Solo Añadir y Concurrencia Optimista

El event store guarda los eventos de cada agregado en orden, con un número de versión, y solo admite añadir. Al guardar se indica la versión esperada: si otro proceso añadió eventos entretanto, la escritura falla y el comando se reintenta con el estado actualizado.

**Cómo funciona por dentro.**

Un event store es, en esencia, una tabla (o una base de datos especializada como EventStoreDB) donde cada fila es un evento: el id del agregado, su número de versión dentro de ese agregado, el tipo, el contenido y la fecha. Solo se añade: nada se actualiza ni se borra. Para cargar un agregado se leen sus eventos en orden de versión y se aplican uno tras otro.

La concurrencia se resuelve de forma optimista. Un comando carga el agregado (por ejemplo, en la versión 7), decide y genera nuevos eventos, y al guardarlos indica que espera estar en la versión 7. Si mientras tanto otro comando añadió la versión 8, la clave `(agregado_id, version)` hace fallar la inserción: el comando recarga el agregado y vuelve a decidir con el estado real. No hay bloqueos y ninguna decisión se toma sobre datos obsoletos.

Como la historia es inmutable, los errores no se corrigen editando eventos: se añade un evento que compensa (`ImporteCorregido`, `PedidoReabierto`). Así queda constancia de qué pasó y cuándo se corrigió, y todas las proyecciones se pueden reconstruir con el mismo resultado.

*Guardar eventos con la versión esperada*

```java
// create table eventos (
//     agregado_id  uuid        not null,
//     version      int         not null,
//     tipo         text        not null,
//     contenido    jsonb       not null,
//     ocurrido_en  timestamptz not null,
//     posicion     bigserial,                      -- orden global para las proyecciones
//     primary key (agregado_id, version)
// );

public class EventStoreJdbc implements EventStore {
    private final JdbcTemplate jdbc;
    private final SerializadorEventos serializador;

    public EventStoreJdbc(JdbcTemplate jdbc, SerializadorEventos serializador) {
        this.jdbc = jdbc;
        this.serializador = serializador;
    }

    @Override
    @Transactional
    public void anadir(UUID agregadoId, int versionEsperada, List<EventoPedido> nuevos) {
        int version = versionEsperada;
        try {
            for (EventoPedido e : nuevos) {
                version++;
                jdbc.update("""
                    insert into eventos (agregado_id, version, tipo, contenido, ocurrido_en)
                    values (?, ?, ?, ?::jsonb, now())""",
                    agregadoId, version, e.getClass().getSimpleName(), serializador.aJson(e));
            }
        } catch (DuplicateKeyException conflicto) {
            // Otro comando escribió antes: se deshace todo y el llamador recarga y reintenta
            throw new ConcurrenciaException(agregadoId, versionEsperada);
        }
    }
}
```

- **En una línea:** Clave primaria `(agregado_id, version)` en la tabla de eventos: la segunda escritura con la misma versión falla.
- **Por qué importa:** Evita que dos comandos simultáneos decidan sobre un estado obsoleto, sin bloquear el agregado.
- **Error típico:** Corregir un error editando o borrando un evento guardado: se rompe la historia y las proyecciones dejan de cuadrar. Se añade un evento compensatorio.
- **Java 17 frente a 21:** Igual en Java 17 y 21.
- **Se conecta con:** Concurrencia optimista, Agregados, EventStoreDB y tablas append-only

### 5. Snapshots y Versionado de Eventos

Cuando un agregado acumula miles de eventos, un snapshot guarda su estado cada N eventos para no reproducirlos todos. Y como los eventos guardados no se modifican, cuando cambia su forma se versionan y se transforman al leerlos (upcasting).

**Cómo funciona por dentro.**

Reconstruir un agregado leyendo todos sus eventos es barato con decenas y caro con decenas de miles. Un snapshot guarda el estado del agregado en una versión concreta; para cargarlo se lee el último snapshot y solo los eventos posteriores. Es una optimización, no una fuente de verdad: se puede borrar y regenerar desde los eventos, y por eso nunca se eliminan los eventos que resume. La mayoría de agregados bien diseñados tienen pocos eventos y no lo necesitan.

El versionado es el problema más serio. Los eventos guardados no se pueden modificar, pero el código cambia: un campo nuevo obligatorio, uno que cambia de nombre, un evento que se divide en dos. La técnica habitual es el upcasting: cada evento guardado lleva la versión de su esquema y, al leerlo, una cadena de transformaciones convierte las versiones antiguas a la actual antes de aplicarlas. Así el dominio solo conoce la última versión.

Todo esto es complejidad que hay que justificar. CQRS y Event Sourcing encajan cuando la historia tiene valor de negocio (auditoría, finanzas, «¿cómo hemos llegado aquí?») o hay muchas formas de leer los mismos datos. En un CRUD de perfiles de usuario solo añaden coste.

*Upcaster de PedidoCreado v1 a v2*

```java
// v1 (guardado durante años): sin moneda
record PedidoCreadoV1(UUID pedidoId, UUID clienteId, BigDecimal total) {}

// v2 (actual): la moneda es obligatoria
record PedidoCreado(UUID pedidoId, UUID clienteId, BigDecimal total, String moneda) implements EventoPedido {}

final class Upcaster {
    // Convierte cualquier versión guardada a la actual antes de aplicarla al agregado
    static EventoPedido actualizar(EventoGuardado guardado, SerializadorEventos json) {
        return switch (guardado.tipo()) {
            case "PedidoCreado" -> guardado.versionEsquema() == 1
                ? desdeV1(json.leer(guardado.contenido(), PedidoCreadoV1.class))
                : json.leer(guardado.contenido(), PedidoCreado.class);
            default -> json.leerEvento(guardado.tipo(), guardado.contenido());
        };
    }

    private static PedidoCreado desdeV1(PedidoCreadoV1 v1) {
        return new PedidoCreado(v1.pedidoId(), v1.clienteId(), v1.total(), "EUR");   // valor acordado con negocio
    }
}
```

- **En una línea:** Snapshot cada 100 eventos; un `PedidoCreado` v1 sin moneda se convierte al leerlo en v2 con moneda `EUR`.
- **Por qué importa:** Mantiene rápida la carga con historias largas y permite evolucionar el modelo sin reescribir el pasado.
- **Error típico:** Tratar el snapshot como fuente de verdad y borrar los eventos anteriores: si el snapshot tiene un bug, ya no se puede recalcular.
- **Java 17 frente a 21:** Igual en Java 17 y 21.
- **Se conecta con:** Rendimiento, Esquemas de eventos, Compatibilidad hacia atrás


## Sesión de 90 minutos

### Bloque 1: CQRS y Proyecciones (0 - 25 min)

- **Qué estudiar:** Separación entre comandos y consultas, y proyecciones alimentadas por eventos.
- **Pregunta a responder:** ¿Qué consulta de tu proyecto se beneficiaría de un modelo de lectura propio?
- **Entregable:** Diagrama del flujo comando → evento → proyección → consulta para el panel de pedidos.

### Bloque 2: Event Sourcing (25 - 50 min)

- **Qué estudiar:** Agregado reconstruido a partir de eventos y método `aplicar` con `switch` exhaustivo.
- **Pregunta a responder:** ¿Qué información pierdes si solo guardas el estado actual?
- **Entregable:** Agregado `Pedido` que se reconstruye desde una lista de eventos, con tests.

### Bloque 3: Event Store, Concurrencia y Versionado (50 - 80 min)

- **Qué estudiar:** Versión esperada al añadir eventos, snapshots y upcasting.
- **Pregunta a responder:** ¿Qué pasa si dos comandos añaden eventos al mismo agregado a la vez?
- **Entregable:** Tabla de eventos con clave `(agregado_id, version)` y un test que provoca el conflicto.

### Cierre de Consolidación (80 - 90 min)

- **Qué estudiar:** Repaso de la rúbrica del tutor.
- **Pregunta a responder:** Explicar cuándo NO usarías CQRS ni Event Sourcing.
- **Entregable:** Checklist de decisión para CQRS y Event Sourcing en tus notas.


## Explícalo desde cero

### Event Sourcing explicado con un extracto bancario

- **Qué entender:** El banco no guarda solo tu saldo: guarda cada movimiento, y el saldo se calcula sumándolos. Event Sourcing hace lo mismo con cualquier entidad: el estado actual es el resultado de aplicar todos sus eventos en orden.
- **Qué suele explicarse mal:** Decir que Event Sourcing es «guardar un log de auditoría». En un log, el estado vive en otra tabla y el log es un añadido; en Event Sourcing, los eventos son la única fuente de verdad.
- **Ejemplo sencillo:** Si el banco detecta un cargo erróneo, no borra la línea del extracto: añade una devolución. Así se corrige también un evento.
- **Señal de alerta:** Si tu solución para un error en los datos es editar un evento ya guardado.


## Preguntas

### Nivel 1 (Básico): ¿Qué se gana al separar el modelo de escritura y el de lectura en CQRS?

Cada lado se optimiza para lo suyo: el de escritura valida reglas y protege invariantes con un modelo normalizado; el de lectura sirve consultas rápidas con vistas desnormalizadas a medida de cada pantalla, y puede escalar por separado.

**Respuesta razonada:** Porque escribir y leer tienen necesidades opuestas. El lado de escritura protege invariantes: valida, aplica reglas y guarda en un modelo normalizado pensado para que los datos no se contradigan. El lado de lectura quiere devolver rápido exactamente lo que pide una pantalla, y eso suele significar datos duplicados y desnormalizados.

Al separarlos, cada lado se diseña, se optimiza y se escala por su cuenta: puedes tener muchas réplicas de lectura, varias vistas para usos distintos y un modelo de escritura pequeño y estricto. El coste es la consistencia eventual entre ambos y más piezas que mantener, así que solo compensa cuando lecturas y escrituras divergen de verdad.

### Nivel 2 (Intermedio): ¿Cuándo NO se debe aplicar CQRS?

CQRS añade complejidad de sincronización entre modelos de escritura y lectura, introduce consistencia eventual, y duplica la lógica de persistencia. En un CRUD simple con pocas lecturas/escrituras simétricas, la complejidad añadida no se justifica.

**Respuesta razonada:** Cuando el dominio es un CRUD sencillo donde leer y escribir tienen la misma forma, cuando el sistema es pequeño, cuando el equipo no tiene experiencia con consistencia eventual o cuando no hay una asimetría real entre lecturas y escrituras. En esos casos CQRS duplica modelos, mapeos y código de sincronización sin beneficio.

Si además separas almacenes, introduces consistencia eventual: el usuario guarda algo y no lo ve en la lista inmediatamente, con la complejidad de interfaz que eso conlleva. CQRS se aplica por partes, solo en el contexto del sistema donde hace falta, nunca como arquitectura por defecto de toda la aplicación.

### Nivel 3 (Entrevista Senior): Tienes millones de eventos `PedidoCreado` guardados y ahora el evento necesita un campo `moneda` obligatorio. ¿Cómo lo cambias?

Sin tocar los eventos guardados: se crea una versión 2 del evento, los nuevos se escriben en v2 y al leer los antiguos un upcaster los convierte con un valor por defecto acordado. Si alguna proyección necesita el dato, se reconstruye.

**Respuesta razonada:** No se tocan los eventos guardados: son la historia, y las proyecciones dependen de ellos. Se define una nueva versión del evento (`PedidoCreado` v2 con `moneda`), los eventos nuevos se escriben ya en v2 y, al leer, un upcaster convierte los v1 en v2 con un valor por defecto acordado con negocio (por ejemplo, `EUR` si la tienda solo vendía en euros).

El dominio solo maneja la versión actual, y si alguna proyección necesita el dato nuevo para los eventos antiguos, se reconstruye. Si el valor no se puede deducir de forma segura, se publican eventos de corrección para los casos afectados en lugar de inventar el dato.


## Reto del tutor

**Reto:** Un arquitecto propone implementar Event Sourcing para el módulo de gestión de usuarios de la empresa (CRUD de altas, bajas y modificaciones de perfil). El equipo tiene 2 desarrolladores juniors. ¿Es una buena decisión? Argumenta como un CTO técnico.

- **Lo que está bien:** Reconoce que Event Sourcing tiene valor en ciertos contextos.
- **Respuesta superficial:** Decir que "Event Sourcing es bueno porque es moderno".
- **Lo que falta de fondo:** No evaluar el coste de complejidad frente al beneficio real. La gestión de usuarios es un CRUD puro con pocas escrituras, sin necesidad de audit trail complejo ni reproducción de estados.
- **Respuesta senior:** No es una decisión acertada para ese módulo: (1) La gestión de usuarios es un CRUD simple con pocas escrituras concurrentes y sin necesidad de reconstruir estados históricos; (2) Event Sourcing añade complejidad significativa (event store, snapshots, proyecciones, consistencia eventual) que un equipo de 2 juniors no dominará y generará bugs difíciles de diagnosticar; (3) El audit trail se puede lograr con una simple tabla de auditoría (`Hibernate Envers` o triggers de BD) con una fracción de la complejidad. Event Sourcing brilla en dominios con alta concurrencia, reglas de negocio complejas con muchas transiciones de estado (carritos de compra, reservas de vuelo, transacciones financieras) y necesidad real de reproducción temporal del estado.


## Katas

### Kata 25.1: Pedido con Event Sourcing

Modela `sealed interface EventoPedido` con `PedidoCreado`, `LineaAnadida`, `PedidoConfirmado` y `PedidoCancelado`. Implementa `Pedido.desde(List<EventoPedido>)` aplicando cada evento con un `switch` sin `default` y comprueba con tests que el total y el estado salen de los eventos.

### Kata 25.2: Proyección reconstruible

Construye una proyección `ResumenCliente` (número de pedidos y total gastado) que se actualiza con cada evento. Escribe un test que verifique que reconstruirla desde cero con todos los eventos da el mismo resultado que actualizarla evento a evento.


## Reglas de oro

- No apliques CQRS ni Event Sourcing a CRUDs simples; reserva estos patrones para dominios con alta complejidad de estado.
- Los eventos guardados no se editan: los errores se corrigen con un evento compensatorio.
- Las proyecciones se pueden borrar y reconstruir desde los eventos: diseña para poder hacerlo.
- Al guardar eventos, comprueba siempre la versión esperada del agregado.


## Java 17 frente a Java 21

| Característica | Java 17 | Java 21 |
| --- | --- | --- |
| Aplicar eventos al agregado | `instanceof` en cadena o patrón visitante | `switch` exhaustivo con record patterns |


## Proyecto guiado «Pedidos»: Panel de la tienda con CQRS

Separar las lecturas del panel de las escrituras de pedidos con un modelo de lectura propio.

**Tareas:**

- Crea la tabla `resumen_pedidos` (por día y por cliente) que solo actualiza una proyección a partir de los eventos.
- Haz que la proyección recuerde el último evento procesado y que reprocesar sea seguro.
- Añade un comando de administración que borre la proyección y la reconstruya desde los eventos.

**Está listo cuando:**

- El panel lee solo de `resumen_pedidos`, nunca de las tablas de escritura.
- Reconstruir la proyección desde cero da exactamente los mismos números que la actualización incremental.


## Fuentes oficiales

- [Martin Fowler: CQRS](https://martinfowler.com/bliki/CQRS.html)
- [Martin Fowler: Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html)
- [Patrón Event Sourcing](https://microservices.io/patterns/data/event-sourcing.html)

