Event Sourcing e CQRS: quando la storia conta 🏛️
Event Sourcing e CQRS sono tra i pattern più citati — e più fraintesi — nell’ecosistema EDA. Di solito appaiono insieme, come se fossero un pacchetto unico. Non lo sono: si combinano bene, ma hanno motivazioni diverse e costi diversi.
Event Sourcing: il log è la fonte di verità 📜
flowchart LR
CMD[Command] --> AGG[Aggregate]
AGG -->|emette| E1[Evento A v1]
AGG -->|emette| E2[Evento B v1]
E1 --> STORE[(Event Store)]
E2 --> STORE
STORE --> PROJ["Proiezione<br/>(stato corrente)"]
STORE --> AUDIT[Audit log]
In un sistema tradizionale, salvi lo stato corrente dell’entità. Con Event Sourcing, salvi la sequenza di eventi che ha prodotto quello stato. Lo stato corrente è una proiezione della storia.
Esempio: OrdineCreato → ArticoloAggiunto → PagamentoAutorizzato → OrdineSpedito.
Lo stato dell’ordine si ricostruisce applicando gli eventi in sequenza.
Vantaggi concreti ✅
- Audit trail nativo: la storia è il dato — non servono tabelle di log aggiuntive.
- Temporal queries: ricostruisci lo stato in qualsiasi punto nel tempo.
- Replay e riproiezioni: se cambi la logica business, puoi ricostruire le read view dai dati originali.
- Debugging: sai esattamente cosa è successo, in che ordine e perché — senza dover inferire da uno snapshot.
Costi reali (quelli che non vedi nelle slide) 💸
- Event store: hai bisogno di uno storage ottimizzato per append (Kafka, EventStoreDB, o Postgres con table append-only dedicata).
- Snapshoting: senza snapshot, ricostruire lo stato di un’entità con 10.000 eventi è lento.
- Schema evolution: gli eventi sono immutabili, ma il tuo dominio non lo è. Versioning e trasformazioni sono obbligatori, non opzionali.
- Curva di apprendimento: il team deve cambiare modo di pensare. Non tutti trovano naturale “ragionare a eventi” invece di “CRUD su tabelle”.
Snapshoting: non ricominciare da zero ogni volta 📸
Quando la storia di un aggregato diventa lunga, riapplicare tutti gli eventi ad ogni accesso diventa costoso. La soluzione è il snapshot: una fotografia dello stato dell’aggregato salvata a intervalli regolari. Alla successiva lettura, si parte dall’ultimo snapshot e si applicano solo gli eventi successivi.
flowchart LR
SNAP["Snapshot<br/>(stato a T=1000)"] --> E1001[Evento 1001]
E1001 --> E1002[Evento 1002]
E1002 --> STATO[Stato corrente]
Strategia pratica: crea snapshot ogni N eventi o dopo un certo intervallo di tempo. Lo snapshot non sostituisce gli eventi originali — è un’ottimizzazione sulla lettura, non una cancellazione della storia.
CQRS: quando leggere e scrivere hanno bisogno di modelli diversi ⚖️
flowchart TD
C["Command<br/>(write side)"] --> AGG["Aggregate / Write Model"]
AGG --> ES[Event Store]
ES --> PROJ1["Proiezione A<br/>read model 1"]
ES --> PROJ2["Proiezione B<br/>read model 2"]
Q1[Query 1] --> PROJ1
Q2[Query 2] --> PROJ2
CQRS (Command Query Responsibility Segregation) separa il modello di scrittura (comandi → eventi) dal modello di lettura (proiezioni ottimizzate per le query).
- Il write model è normalizzato, coerente, orientato agli invarianti di business.
- I read model sono denormalizzati, ottimizzati per i pattern di query specifici.
Perché non usare un solo modello? 🧠
In sistemi semplici, è giusto usare un solo modello. CQRS ha senso quando:
- le query di lettura richiedono strutture radicalmente diverse da quelle di scrittura;
- la frequenza di lettura è molto più alta delle scritture e vuoi scalarle separatamente;
- hai bisogno di più “viste” degli stessi dati (dashboard, ricerca, notifiche) con esigenze diverse.
Consistenza eventuale nelle proiezioni 🕐
Le proiezioni vengono aggiornate asincronamente dopo la pubblicazione degli eventi. C’è sempre un lag tra la scrittura e la lettura aggiornata.
Conseguenze pratiche:
- l’interfaccia deve comunicare correttamente la consistenza eventuale (“operazione in corso”);
- pianificare il reprocessing delle proiezioni con idempotenza garantita;
- scegliere esplicitamente dove calcolare le denormalizzazioni (lato producer evento o lato consumer/projection).
Event Sourcing + CQRS: combo o alternative? 🤔
Spesso si vedono insieme, ma sono indipendenti:
| Event Sourcing | Senza Event Sourcing | |
|---|---|---|
| Con CQRS | Combo classica: eventi come write side, proiezioni come read model | Possibile: write su RDBMS, read model separati |
| Senza CQRS | Raro ma possibile: ES per audit/replay, stato letto dall’event store | Sistema tradizionale |
La combo ES + CQRS funziona molto bene quando:
- hai bisogno di audit/replay (motivazione ES);
- hai pattern di lettura diversi dalla struttura degli eventi (motivazione CQRS);
- il dominio è genuinamente event-centric e non CRUD mascherato.
Schema evolution sugli eventi immutabili 🧬
In Event Sourcing, gli eventi sono immutabili: non puoi modificare un evento già scritto. Questo rende la schema evolution più delicata rispetto a un sistema CRUD.
Strategie pratiche:
- Upcasting: quando leggi un vecchio evento, lo trasformi nel formato nuovo prima di processarlo. Implementato nel layer di lettura, non nello storage.
- Versioning esplicito:
OrdineCreato_v1,OrdineCreato_v2come tipi distinti, con handler separati per ogni versione. - Weak schema: usa un formato flessibile (JSON) e scrivi il layer di lettura in modo difensivo verso i campi opzionali.
Non esiste la soluzione “gratis”: scegli in base alla frequenza attesa di cambiamenti del dominio e al numero di consumer che leggono l’event store.
Quando NON usare ES/CQRS 🛑
Questi pattern non sono adatti a tutti i sistemi. Evitali se:
- il dominio è essenzialmente CRUD senza requisiti di audit o business logic complessa;
- il team non è pronto per la complessità operativa (event store, snapshot, reprocessing delle proiezioni);
- non hai requisiti di temporal queries, rebuild da zero o audit normativo;
- il sistema non raggiunge una scala in cui separare read e write vale il costo di ownership.
Prima di adottare ES+CQRS, chiediti: “Avrei bisogno di ricostruire lo stato da zero in futuro?” Se la risposta onesta è no, probabilmente un event log su Kafka + un buon read model è tutto ciò che serve.
Checklist prima di adottare ES/CQRS ✅
- Hai identificato requisiti reali di audit, replay o temporal queries?
- Hai scelto l’event store (Kafka, EventStoreDB, Postgres append-only)?
- Hai una strategia di snapshoting per aggregati a lunga vita?
- Il team ha compreso il cambio di paradigma rispetto al CRUD classico?
- Hai pianificato schema evolution e upcasting per gli eventi futuri?
- Hai definito il reprocessing delle proiezioni con idempotenza verificata?
- Hai misurato (o stimato) il lag delle proiezioni e comunicato le aspettative a prodotto e UX?
Prossimi passi 🚀
- Per contratti e schema evolution degli eventi: vedi la guida su event design.
- Per schema registry come strumento pratico per versioning e compatibilità: vedi la guida su Schema Registry.
- Per governance e catalogo: vedi la guida di governance.
- Per operatività, retry e DLQ: vedi la guida su operatività.