Pubblicato il 28 maggio 2026
ArchitectureSoftware DevelopmentData Patterns

CQRS ed Event Sourcing

Scopri come gestire flussi di dati complessi e ad alte prestazioni separando le letture dalle scritture con CQRS ed Event Sourcing.

Il Limite del Modello Unico di Dati

Nelle applicazioni tradizionali (spesso denominate architetture CRUD), utilizziamo lo stesso modello di dati sia per inserire e modificare le informazioni che per interrogarle. Solitamente si ha un database relazionale in cui una riga rappresenta lo "stato corrente" di un'entità.

Sebbene questo approccio funzioni benissimo per sistemi di piccola e media complessità, inizia a mostrare gravi limiti quando la scalabilità e la complessità di business aumentano:

  1. Conflitti di performance: Le query di lettura complesse (reportistica, dashboard) rallentano le transazioni di scrittura e viceversa.
  2. Ottimizzazione impossibile: Un modello ottimizzato per le regole di business (scrittura) difficilmente sarà ottimizzato per la visualizzazione dell'interfaccia utente (lettura), portando a JOIN mastodontiche o a DTO estremamente complessi.
  3. Perdita di informazioni storiche: Salvare solo lo stato corrente cancella l'intento dell'utente e la storia di come si è arrivati a quello stato.

Per risolvere queste problematiche, entrano in gioco due pattern architetturali straordinariamente potenti: CQRS ed Event Sourcing.


Cos'è il CQRS?

CQRS sta per Command Query Responsibility Segregation (Segregazione della Responsabilità di Comando e Query), un pattern formulato da Greg Young. Il principio fondamentale è disarmante nella sua semplicità: separa il percorso di scrittura (modifica dei dati) dal percorso di lettura (interrogazione dei dati).

graph LR
    UI[Interfaccia Utente] -->|Invia Command| C[Command Model - Scrittura]
    C -->|Aggiorna| DB_W[(Database Scrittura)]
    DB_W -->|Sincronizza/Proietta| DB_R[(Database Lettura)]
    UI -->|Invia Query| Q[Query Model - Lettura]
    Q -->|Legge da| DB_R

In un'architettura CQRS, dividiamo le operazioni in due categorie distinte:

1. I Comandi (Commands)

Rappresentano l'intenzione di modificare lo stato del sistema (es. InviaOrdine, RegistraUtente, ApplicaSconto).

  • Esprimono un intento di business preciso.
  • Possono essere rifiutati se violano le regole del dominio.
  • Non restituiscono dati all'interfaccia utente, tranne un'eventuale conferma di ricezione o un ID.

2. Le Query

Rappresentano la richiesta di visualizzare dei dati (es. OttieniDettaglioOrdine, ElencaProdottiAttivi).

  • Non modificano mai lo stato del sistema (sono operazioni idempotenti e prive di effetti collaterali).
  • Restituiscono DTO (Data Transfer Objects) pre-formattati per le specifiche esigenze della UI.

Separando questi flussi, possiamo persino utilizzare due database differenti: uno altamente normalizzato per le transazioni di scrittura (es. un database relazionale) ed uno denormalizzato ottimizzato per letture istantanee (es. un database NoSQL o un indice Elasticsearch).


Cos'è l'Event Sourcing?

Mentre il CQRS si occupa di separare i flussi di lettura e scrittura, l'Event Sourcing rivoluziona il modo in cui persistiamo le informazioni.

Invece di salvare lo "stato corrente" di un oggetto nel database (es. saldo attuale dell'utente = 150€), l'Event Sourcing prevede di memorizzare l'intera sequenza cronologica di eventi immutabili che hanno portato a quello stato.

L'analogia dell'estratto conto bancario

Pensa a come funziona una banca. La banca non memorizza semplicemente un singolo record con il tuo saldo corrente. Se lo facesse, non potrebbe mai spiegarti come hai speso i tuoi soldi o rilevare errori. La banca memorizza un elenco di transazioni immutabili (gli eventi):

  1. Conto Creato (+0€)
  2. Bonifico Ricevuto (+500€)
  3. Prelievo Effettuato (-100€)
  4. Pagamento Carta (-250€)

Il saldo corrente (150€) non è altro che la somma algebrica (la proiezione) di tutti questi eventi dal principio ad oggi.

Rehydration (Reidratazione)

Per ottenere lo stato corrente di un aggregato (ad esempio, per verificare se un utente ha abbastanza credito per fare un acquisto), l'applicazione esegue il processo di Rehydration: carica tutti gli eventi storici associati a quell'aggregato dall'Event Store (un database ottimizzato per append-only) e li applica uno dopo l'altro in memoria per ricostruire lo stato attuale.

// Esempio concettuale di Reidratazione
class ContoCorrente {
  private saldo: number = 0;

  applica(evento: Evento) {
    switch (evento.tipo) {
      case "ContoCreato":
        this.saldo = 0;
        break;
      case "DenaroDepositato":
        this.saldo += evento.dati.importo;
        break;
      case "DenaroPrelevato":
        this.saldo -= evento.dati.importo;
        break;
    }
  }
}

La Sinergia Perfetta: CQRS + Event Sourcing

Sebbene CQRS ed Event Sourcing possano essere usati in modo indipendente, la loro unione rappresenta uno dei pattern più eleganti per i sistemi distribuiti ed enterprise:

  1. Un Comando arriva al Command Model.
  2. Il Command Model esegue la reidratazione dell'aggregato dall'Event Store per verificare le regole di business.
  3. Se il comando è valido, l'aggregato genera uno o più Eventi di Dominio (es. DenaroPrelevato).
  4. Gli eventi vengono salvati nell'Event Store in modo transazionale.
  5. Un componente in background (chiamato Projector o Event Handler) intercetta questi nuovi eventi e aggiorna in modo asincrono il Read Model (il database di lettura ottimizzato).
  6. L'interfaccia utente interroga il Read Model tramite le Query, ottenendo risposte immediate ed efficienti.

I Vantaggi di questa Unione

  • Audit Log Nativo al 100%: Avendo memorizzato ogni singolo evento, hai a disposizione uno storico perfetto e incontestabile per scopi legali o di debug.
  • Time Travel (Viaggio nel Tempo): Puoi ricostruire lo stato del sistema in qualsiasi preciso istante del passato semplicemente fermando l'applicazione degli eventi a una certa data.
  • Flessibilità nei Read Model: Se le esigenze della UI cambiano e serve una nuova tabella di lettura, puoi creare un nuovo proiettore e far fare il "replay" di tutti gli eventi storici per popolare la nuova tabella da zero!
  • Scalabilità Estrema: Le scritture append-only sull'Event Store sono velocissime e prive di lock complessi. Le letture possono essere scalate all'infinito distribuendo copie del Read Model.

Le Sfide da Considerare

Questa architettura non è un "silver bullet" ed introduce una discreta complessità:

  • Consistenza Eventuale (Eventual Consistency): Poiché l'aggiornamento del Read Model avviene in modo asincrono, potrebbe esserci un leggero ritardo (generalmente frazioni di secondo) prima che una modifica appena salvata sia visibile in lettura.
  • Evoluzione degli Eventi (Versioning): Poiché gli eventi salvati nel database sono immutabili e durano per sempre, se i requisiti cambiano e cambia la struttura di un evento, dovrai gestire la retrocompatibilità (tramite pattern come l'Upcasting).
  • Curva di Apprendimento: Richiede una profonda comprensione della messaggistica, dei sistemi asincroni e del Domain-Driven Design.

Conclusione

L'accoppiata CQRS ed Event Sourcing rappresenta la soluzione ideale per domini complessi ad alta densità di transazioni e con rigide necessità di tracciamento storico (fintech, e-commerce su larga scala, logistica, sistemi sanitari).

Tuttavia, a causa dell'inevitabile complessità architetturale che introducono, è fondamentale applicarli solo laddove il valore di business superi ampiamente il costo di implementazione, lasciando le classiche architetture CRUD per le parti più semplici del sistema.

Condividi questo articolo