Fins ara hem fet servir els esdeveniments com a mecanisme de comunicació entre serveis. En aquesta lliçó fem un pas més i els convertim en el mecanisme d'emmagatzematge. L'Event Sourcing proposa una idea radical: en lloc de desar l'estat actual d'una entitat (la típica fila en una taula que se sobreescriu), desem la seqüència completa d'esdeveniments que l'han portat fins a aquest estat. L'estat actual deixa de ser la dada primària i passa a ser una conseqüència que es deriva dels esdeveniments.

Aquest patró sol anar de bracet amb CQRS (Command Query Responsibility Segregation), que separa el model que escriu (comandes) del model que llegeix (consultes). Junts permeten auditoria total, escalat independent de lectures i escriptures, i la capacitat de "viatjar en el temps". És un enfocament potent però exigent, així que també aprendràs quan no fer-lo servir.

Contingut

  1. La idea central d'Event Sourcing
  2. L'Event Store (magatzem d'esdeveniments)
  3. Reconstrucció de l'estat (replay)
  4. CQRS: separar lectura i escriptura
  5. Projeccions i models de lectura
  6. Exemple complet d'esdeveniments d'un compte bancari
  7. Errors comuns i consells
  8. Exercicis i solucions
  9. Conclusió

  1. La idea central d'Event Sourcing

En el model tradicional (CRUD) una transferència bancària es tradueix en UPDATE cuentas SET saldo = 50 WHERE id = 1. El valor anterior (100) es perd per sempre.

Amb Event Sourcing desem els fets:

1. CuentaAbierta(saldoInicial=0)
2. DineroIngresado(importe=100)
3. DineroRetirado(importe=50)

El saldo actual (50) no s'emmagatzema: es calcula sumant els esdeveniments. Avantatges immediats:

  • Auditoria perfecta: tens l'historial íntegre de per què l'estat és el que és.
  • Depuració temporal: pots reconstruir l'estat en qualsevol moment del passat.
  • Noves vistes a posteriori: si demà necessites una nova projecció, la generes reproduint els esdeveniments ja existents.
Aspecte CRUD tradicional Event Sourcing
Què es desa Estat actual (se sobreescriu) Tots els esdeveniments (s'afegeixen)
Historial Es perd Complet i immutable
Operació principal UPDATE / DELETE APPEND (només afegir)
Auditoria Requereix taules extra Intrínseca
Complexitat Baixa Alta

  1. L'Event Store (magatzem d'esdeveniments)

L'Event Store és la base de dades on es persisteixen els esdeveniments. La seva característica fonamental és que és append-only: els esdeveniments mai no es modifiquen ni s'esborren, només s'afegeixen al final. Cada esdeveniment s'associa a un stream (normalment, un per entitat/agregat).

-- Estructura mínima d'un event store relacional
CREATE TABLE eventos (
    id            BIGSERIAL PRIMARY KEY,   -- ordre global d'inserció
    stream_id     VARCHAR(64) NOT NULL,    -- a quina entitat pertany (p.ex. cuenta-42)
    version       INT NOT NULL,            -- núm. de seqüència dins del stream
    tipo          VARCHAR(100) NOT NULL,   -- "DineroIngresado", etc.
    datos         JSONB NOT NULL,          -- payload de l'esdeveniment
    ocurrido_en   TIMESTAMP NOT NULL,
    UNIQUE (stream_id, version)            -- control de concurrència optimista
);

Explicació de cada columna:

  • stream_id agrupa tots els esdeveniments d'una mateixa entitat. Per reconstruir el compte 42, llegim tots els esdeveniments amb stream_id = 'cuenta-42' ordenats per version.
  • version és el número d'ordre dins del stream. La restricció UNIQUE (stream_id, version) implementa concurrència optimista: si dos processos intenten escriure la versió 5 alhora, un fallarà, evitant que es trepitgin.
  • datos (JSONB a PostgreSQL) desa el contingut de l'esdeveniment de forma flexible.
  • Mai no hi ha UPDATE ni DELETE sobre aquesta taula; només INSERT.

  1. Reconstrucció de l'estat (replay)

Per obtenir l'estat actual d'una entitat, llegim els seus esdeveniments en ordre i els apliquem un a un sobre un estat inicial buit. A això se'n diu replay o rehidratació.

public class Cuenta {
    private String id;
    private BigDecimal saldo = BigDecimal.ZERO;
    private boolean abierta = false;

    // Reconstrueix el compte reproduint el seu historial d'esdeveniments
    public static Cuenta rehidratar(List<EventoCuenta> historial) {
        Cuenta cuenta = new Cuenta();
        for (EventoCuenta e : historial) {
            cuenta.aplicar(e); // apliquem cada esdeveniment en ordre
        }
        return cuenta;
    }

    // Cada tipus d'esdeveniment modifica l'estat de forma determinista
    private void aplicar(EventoCuenta e) {
        switch (e) {
            case CuentaAbierta ca -> {
                this.id = ca.cuentaId();
                this.saldo = ca.saldoInicial();
                this.abierta = true;
            }
            case DineroIngresado di -> this.saldo = this.saldo.add(di.importe());
            case DineroRetirado dr  -> this.saldo = this.saldo.subtract(dr.importe());
        }
    }
}

Punts clau:

  • rehidratar parteix d'un Cuenta buit i aplica cada esdeveniment seqüencialment. El resultat és l'estat actual.
  • El mètode aplicar fa servir pattern matching sobre tipus segellats (Java 21): cada esdeveniment sap com muta l'estat. És totalment determinista: els mateixos esdeveniments produeixen sempre el mateix estat.
  • Optimització (snapshots): reproduir milers d'esdeveniments en cada lectura és costós. La solució són els snapshots: cada N esdeveniments es desa una "foto" de l'estat, i en rehidratar es parteix de l'últim snapshot i només es reprodueixen els esdeveniments posteriors.

  1. CQRS: separar lectura i escriptura

CQRS separa les operacions en dos models diferents:

  • Costat d'escriptura (Command): rep comandes, valida regles de negoci i genera esdeveniments. Optimitzat per a la consistència.
  • Costat de lectura (Query): serveix consultes des de models de dades preparats per llegir ràpid (desnormalitzats). Optimitzat per al rendiment.
flowchart LR
    U[Usuari] -->|Comanda| W[Model d'Escriptura]
    W -->|genera| ES[(Event Store)]
    ES -->|esdeveniments| PR[Projector]
    PR -->|actualitza| RM[(Model de Lectura<br/>desnormalitzat)]
    U -->|Consulta| RM

Avantatges de separar tots dos costats:

  • Escalat independent: normalment hi ha moltes més lectures que escriptures; pots replicar el model de lectura sense tocar el d'escriptura.
  • Models òptims: el d'escriptura pot ser un agregat normalitzat; el de lectura, una taula plana llesta per pintar una pantalla concreta.

Important: CQRS no obliga a fer servir Event Sourcing, ni a la inversa. Però combinats encaixen de forma natural: els esdeveniments del write side alimenten les projeccions del read side.

  1. Projeccions i models de lectura

Una projecció és un consumidor d'esdeveniments que manté actualitzat un model de lectura. Cada vegada que arriba un esdeveniment nou, la projecció actualitza la seva vista.

// Projecció que manté un resum de saldos per a llistats ràpids
@Component
public class ProyeccionSaldos {

    private final JdbcTemplate jdbc;

    public ProyeccionSaldos(JdbcTemplate jdbc) { this.jdbc = jdbc; }

    @EventListener
    public void on(DineroIngresado e) {
        // Actualitza la taula de lectura desnormalitzada
        jdbc.update("UPDATE saldos_vista SET saldo = saldo + ? WHERE cuenta_id = ?",
                e.importe(), e.cuentaId());
    }

    @EventListener
    public void on(DineroRetirado e) {
        jdbc.update("UPDATE saldos_vista SET saldo = saldo - ? WHERE cuenta_id = ?",
                e.importe(), e.cuentaId());
    }
}

Explicació:

  • saldos_vista és una taula desnormalitzada pensada només per llegir (per exemple, mostrar un llistat de comptes amb el seu saldo a l'instant, sense reproduir esdeveniments).
  • Cada mètode reacciona a un tipus d'esdeveniment i actualitza la vista. El model de lectura és, en el fons, una memòria cau derivada de l'event store: si es corromp, es pot regenerar reproduint tots els esdeveniments des de zero.
  • Això introdueix consistència eventual: després d'una escriptura, la vista pot trigar mil·lisegons a reflectir-la. Cal dissenyar l'experiència d'usuari comptant amb això.

  1. Exemple complet d'esdeveniments d'un compte bancari

Vegem els esdeveniments modelats com a tipus segellats de Java:

// Interfície segellada: només aquests tipus poden ser esdeveniments de compte
public sealed interface EventoCuenta
        permits CuentaAbierta, DineroIngresado, DineroRetirado {
    String cuentaId();
    Instant ocurridoEn();
}

public record CuentaAbierta(String cuentaId, BigDecimal saldoInicial,
                            Instant ocurridoEn) implements EventoCuenta {}

public record DineroIngresado(String cuentaId, BigDecimal importe,
                              Instant ocurridoEn) implements EventoCuenta {}

public record DineroRetirado(String cuentaId, BigDecimal importe,
                             Instant ocurridoEn) implements EventoCuenta {}

I el costat d'escriptura validant una regla de negoci abans d'emetre l'esdeveniment:

public class Cuenta {
    // ... estat i rehidratació d'abans ...

    // COMANDA: retirar diners. Valida i RETORNA l'esdeveniment resultant.
    public DineroRetirado retirar(BigDecimal importe) {
        if (!abierta) throw new IllegalStateException("Compte tancat");
        if (importe.compareTo(saldo) > 0)
            throw new SaldoInsuficienteException(id, importe, saldo);
        // La regla es compleix: generem l'esdeveniment (encara no muta l'estat)
        return new DineroRetirado(id, importe, Instant.now());
    }
}

Punts clau:

  • La interfície sealed garanteix que el switch d'aplicar (secció 3) cobreixi tots els casos possibles; si afegeixes un esdeveniment nou, el compilador t'obliga a tractar-lo.
  • El mètode retirar valida la regla "no pots retirar més del teu saldo" i, si passa, genera l'esdeveniment. L'estat s'actualitzarà quan aquest esdeveniment s'apliqui i es persisteixi. Així, escriptura i validació queden en l'agregat.

Errors Comuns i Consells

  • Aplicar Event Sourcing a tot el sistema. És complex. Reserva'l per als dominis on l'auditoria i l'historial aportin valor real (finances, assegurances, logística). Per a un CRUD de configuració, és exagerat.
  • Canviar el significat d'un esdeveniment ja desat. Els esdeveniments són immutables i eterns; els has de versionar (PedidoCreadoV2) i mantenir compatibilitat, no editar-los.
  • Oblidar els snapshots. Sense ells, les entitats amb milers d'esdeveniments es tornen lentes de rehidratar.
  • Esperar consistència immediata en les lectures. CQRS és eventualment consistent; informa l'usuari o fes servir estratègies de lectura després d'escriptura quan sigui crític.
  • Consell: comença només amb CQRS (sense Event Sourcing) si únicament necessites escalar lectures; afegeix Event Sourcing més endavant si necessites l'historial.

Exercicis

  1. Tens un compte amb els esdeveniments: CuentaAbierta(0), DineroIngresado(200), DineroRetirado(70), DineroIngresado(30). Quin és el saldo després de rehidratar? Explica el procés.
  2. Explica per què un esdeveniment ha d'expressar un fet ja validat i no una ordre. Què passaria si deséssim comandes a l'event store en lloc d'esdeveniments?
  3. Dissenya la taula de lectura desnormalitzada movimientos_vista que permeti mostrar l'extracte d'un compte (data, tipus, import, saldo resultant) sense reproduir esdeveniments en cada consulta.

Solucions

  1. Saldo = 0 + 200 − 70 + 30 = 160. Es parteix d'un compte buit i s'apliquen els esdeveniments en ordre: CuentaAbierta fixa 0, DineroIngresado(200) suma a 200, DineroRetirado(70) baixa a 130, DineroIngresado(30) puja a 160.
  2. Un esdeveniment és un fet consumat: ja va passar per les validacions, així que reproduir-lo mai no hauria de fallar. Si deséssim comandes, en reproduir l'historial hauríem de tornar a validar regles que podrien haver canviat, i una comanda podria rebutjar-se en reproduir-la, trencant la reconstrucció determinista de l'estat.
  3. Per exemple:
CREATE TABLE movimientos_vista (
    id            BIGSERIAL PRIMARY KEY,
    cuenta_id     VARCHAR(64) NOT NULL,
    fecha         TIMESTAMP NOT NULL,
    tipo          VARCHAR(20) NOT NULL,   -- INGRESO / RETIRADA
    importe       NUMERIC(15,2) NOT NULL,
    saldo_resultante NUMERIC(15,2) NOT NULL
);
CREATE INDEX idx_mov_cuenta ON movimientos_vista (cuenta_id, fecha);

La projecció omple saldo_resultante en cada esdeveniment, de manera que l'extracte es consulta amb un simple SELECT ... WHERE cuenta_id = ? ORDER BY fecha.

Conclusió

Event Sourcing converteix la seqüència d'esdeveniments en la font de veritat, oferint auditoria total i la capacitat de reconstruir qualsevol estat passat mitjançant replay, optimitzat amb snapshots. CQRS separa el model d'escriptura del de lectura, permetent escalar-los per separat i alimentar el costat de lectura amb projeccions. Vam veure que són potents però exigents i que convé aplicar-los amb criteri.

En la lliçó següent, "Gestió de Transaccions Distribuïdes: Patró Saga", abordarem un problema que apareix de forma natural en aquests sistemes: com mantenir la coherència d'una operació que abasta diversos serveis quan no podem fer servir una transacció ACID tradicional.

Curs d'Arquitectura d'Aplicacions

Mòdul 1: Fonaments de l'Arquitectura d'Aplicacions

Mòdul 2: Principis i Tàctiques de Disseny

Mòdul 3: Estils i Patrons Arquitectònics

Mòdul 4: Arquitectures Distribuïdes i Microserveis

Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria

Mòdul 6: Disseny Dirigit pel Domini (DDD)

Mòdul 7: Dades i Persistència

Mòdul 8: Arquitectura al Núvol i Desplegament

Mòdul 9: Qualitat, Seguretat i Observabilitat

Mòdul 10: Evolució, Governança i Casos Pràctics

© Copyright 2026. Tots els drets reservats