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
- La idea central d'Event Sourcing
- L'Event Store (magatzem d'esdeveniments)
- Reconstrucció de l'estat (replay)
- CQRS: separar lectura i escriptura
- Projeccions i models de lectura
- Exemple complet d'esdeveniments d'un compte bancari
- Errors comuns i consells
- Exercicis i solucions
- Conclusió
- 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:
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 |
- 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_idagrupa tots els esdeveniments d'una mateixa entitat. Per reconstruir el compte 42, llegim tots els esdeveniments ambstream_id = 'cuenta-42'ordenats perversion.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
UPDATEniDELETEsobre aquesta taula; nomésINSERT.
- 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:
rehidratarparteix d'unCuentabuit i aplica cada esdeveniment seqüencialment. El resultat és l'estat actual.- El mètode
aplicarfa 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.
- 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| RMAvantatges 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.
- 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ò.
- 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
sealedgaranteix que elswitchd'aplicar(secció 3) cobreixi tots els casos possibles; si afegeixes un esdeveniment nou, el compilador t'obliga a tractar-lo. - El mètode
retirarvalida 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
- 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. - 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?
- Dissenya la taula de lectura desnormalitzada
movimientos_vistaque permeti mostrar l'extracte d'un compte (data, tipus, import, saldo resultant) sense reproduir esdeveniments en cada consulta.
Solucions
- Saldo = 0 + 200 − 70 + 30 = 160. Es parteix d'un compte buit i s'apliquen els esdeveniments en ordre:
CuentaAbiertafixa 0,DineroIngresado(200)suma a 200,DineroRetirado(70)baixa a 130,DineroIngresado(30)puja a 160. - 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.
- 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
- Què és l'Arquitectura d'Aplicacions?
- El Rol de l'Arquitecte de Programari
- Atributs de Qualitat i Requisits No Funcionals
- Decisions Arquitectòniques i Compromisos (Trade-offs)
- Documentació d'Arquitectura: Vistes i el Model C4
Mòdul 2: Principis i Tàctiques de Disseny
- Acoblament, Cohesió i Separació de Responsabilitats
- Principis SOLID Aplicats a l'Arquitectura
- DRY, KISS, YAGNI i Altres Principis de Disseny
- Tàctiques Arquitectòniques per als Atributs de Qualitat
- Gestió del Deute Tècnic
Mòdul 3: Estils i Patrons Arquitectònics
- Arquitectura Monolítica
- Arquitectura en Capes (N-Tier)
- Arquitectura Client-Servidor
- Arquitectura Hexagonal (Ports i Adaptadors)
- Arquitectura Neta i Ceba (Clean & Onion)
Mòdul 4: Arquitectures Distribuïdes i Microserveis
- Introducció als Sistemes Distribuïts
- Arquitectura de Microserveis
- Descomposició de Serveis i Bounded Contexts
- API Gateway, Service Discovery i Comunicació entre Serveis
- Patrons de Resiliència: Circuit Breaker, Retry i Bulkhead
- El Teorema CAP i la Consistència de Dades
Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria
- Fonaments de l'Arquitectura Orientada a Esdeveniments
- Missatgeria Asíncrona: Cues i Brokers
- Patrons d'Esdeveniments: Event Sourcing i CQRS
- Gestió de Transaccions Distribuïdes: Patró Saga
- Streaming de Dades en Temps Real
Mòdul 6: Disseny Dirigit pel Domini (DDD)
- Conceptes Fonamentals del DDD
- Disseny Estratègic: Bounded Contexts i Llenguatge Ubic
- Disseny Tàctic: Entitats, Agregats i Repositoris
- Mapatge de Contextos (Context Mapping)
Mòdul 7: Dades i Persistència
- Estratègies de Persistència: SQL vs NoSQL
- Patrons d'Accés a Dades: Repository, Unit of Work i DAO
- Base de Dades per Servei i Gestió de Dades Distribuïdes
- Cau i Estratègies d'Invalidació
Mòdul 8: Arquitectura al Núvol i Desplegament
- Fonaments del Cloud Computing (IaaS, PaaS, SaaS)
- Contenidors i Orquestració amb Docker i Kubernetes
- Arquitectura Serverless
- Patrons de Disseny Cloud-Native
- Infraestructura com a Codi (IaC)
Mòdul 9: Qualitat, Seguretat i Observabilitat
- Escalabilitat: Horitzontal vs Vertical i Balanceig de Càrrega
- Alta Disponibilitat i Tolerància a Fallades
- Seguretat per Disseny i Autenticació/Autorització
- Observabilitat: Logging, Mètriques i Traçabilitat
- Rendiment i Proves de Càrrega
