CQRS — Cheat Sheet

Tag: cqrs, architettura, pattern, cqs, event-sourcing, ddd

Livello: intermedio

TL;DR: Pattern che separa i modelli di lettura (Query) da quelli di scrittura (Command), permettendo di scalarli, ottimizzarli e modellarli in modo indipendente.

Concetti chiave

Command: Cambia lo stato del sistema (scrittura), non ritorna dati (solo ack/nack o ID generato), esegue validazione e logica di business.

Query: Legge lo stato del sistema (lettura), non lo modifica MAI (side-effect free), ritorna DTO/ViewModel pronti per l'UI.

Separazione logica: I command e le query usano handler separati (es. CreateUserCommandHandler vs GetUserQueryHandler), anche se insistono sullo stesso database.

Separazione fisica (Avanzata): Database di scrittura separato da uno o più database di lettura (es. SQL per scrittura, NoSQL/Search Engine per lettura).

Eventual Consistency: Se i DB sono separati fisicamente, il DB di lettura viene aggiornato in modo asincrono (tramite code/eventi) e potrebbe essere temporaneamente disallineato.

Sinergia con Event Sourcing: CQRS brilla con l'Event Sourcing: il write model salva eventi (Command), i read model (Projections) consumano gli eventi per creare viste ottimizzate.

Task-Based UI: I command si sposano con interfacce utente basate su azioni specifiche ("PromuoviDipendente") invece di generici CRUD ("AggiornaDipendente").

Domande tipiche e risposte pronte

Q: Perché non usare un semplice CRUD invece di CQRS?R: Perché nei sistemi complessi le letture (veloci, denormalizzate) hanno esigenze di scala e modellazione diverse dalle scritture (transazionali, regole di business). CRUD mescola questi vincoli e diventa un collo di bottiglia.

Q: C'è sempre bisogno di due database fisici in CQRS?R: Assolutamente no. Il pattern nasce come separazione *logica* dei modelli (classi/interfacce separate). La separazione fisica è un'ottimizzazione architetturale successiva e opzionale.

Q: Come gestisci l'eventual consistency se l'utente crea un record e poi fa subito refresh?R: Uso tecniche lato UI (aggiornamento ottimistico in cache locale), polling, oppure faccio restituire al Command un ID/token che la Query attende prima di risolvere la lettura.

Insidie e errori comuni

Overengineering: Applicare CQRS (specialmente con DB separati) a domini semplici (anemici) dove un CRUD basta e avanza, aumentando inutilmente la complessità.

Command che restituiscono dati complessi: Un Command dovrebbe restituire al massimo un ID o uno stato di successo, non l'entità aggiornata (violazione del principio).

Ignorare l'eventual consistency: Non preparare la UI e il Product Owner al fatto che, in architetture CQRS distribuite, il dato appena scritto potrebbe non essere immediatamente visibile.

Snippet e comandiTypeScript

// Esempio Command
class CreateOrderCommand {
  constructor(public customerId: string, public items: any[]) {}
}
// Command Handler
class CreateOrderHandler {
  async handle(command: CreateOrderCommand): Promise<string> {
    // 1. Valida regole di business
    // 2. Salva a DB
    // 3. Ritorna OrderID
    return "order-123";
  }
}

// Esempio Query
class GetOrderSummaryQuery {
  constructor(public orderId: string) {}
}
// Query Handler
class GetOrderSummaryHandler {
  async handle(query: GetOrderSummaryQuery): Promise<OrderSummaryDTO> {
    // Query diretta e veloce al DB di lettura, senza caricare il domain model
    return db.query("SELECT * FROM read_orders WHERE id = ?", query.orderId);
  }
}

Da ripassare

[ ] Meccanismi di sincronizzazione asincrona (Projections) tra Write DB e Read DB.

[ ] Saga pattern e Outbox pattern in contesti CQRS distribuiti.

Jarvis Interviews
Jarvis