CQRS (Command Query Responsibility Segregation) è un pattern architetturale che suggerisce di separare in modo netto le operazioni di scrittura (command) da quelle di lettura (query) su un sistema. Invece di avere un unico modello di dominio e un unico percorso di accesso ai dati, si definiscono percorsi ottimizzati e spesso modelli differenti per leggere e per modificare lo stato dell'applicazione.
Perché nasce CQRS
In molte applicazioni tradizionali (approccio CRUD) lo stesso modello viene usato per:
- Mostrare dati all'utente (lettura);
- Modificare e salvare dati (scrittura);
- Gestire regole di business, validazioni, sicurezza.
Questo approccio funziona bene finché il dominio è semplice. Quando la complessità cresce, emergono diversi problemi:
- Modelli gonfi e difficili da mantenere: un singolo modello deve soddisfare esigenze molto diverse.
- Query complicate e lente: si finisce per scrivere query molto complesse per soddisfare le esigenze di reporting e UI.
- Difficoltà a scalare: lettura e scrittura hanno spesso requisiti di scalabilità differenti, ma vengono trattate allo stesso modo.
CQRS propone di affrontare questi problemi separando nettamente il modello e il percorso di lettura da quelli di scrittura.
I principi di base di CQRS
1. Separazione tra Command e Query
Il principio di base è: i metodi che modificano lo stato (command) non restituiscono dati di dominio, mentre i metodi che leggono dati (query) non modificano lo stato.
- Command: eseguono azioni che cambiano lo stato del sistema (crea ordine, aggiorna indirizzo, annulla prenotazione...).
- Query: recuperano dati per l'utente o per altri sistemi (lista ordini, dettaglio cliente, report, dashboard...).
2. Modelli separati
Con CQRS è normale avere:
- Un modello di dominio ricco per la scrittura, centrato sulle regole di business.
- Uno o più modelli di lettura ottimizzati per le esigenze di visualizzazione e reporting.
Il modello di lettura può essere molto semplice (ad esempio DTO piatti) e mappato direttamente sulle esigenze della UI.
3. Canali e storage differenti (opzionale ma comune)
La separazione tra lettura e scrittura può arrivare fino al livello di storage:
- Database ottimizzato per la scrittura e la consistenza delle regole di business.
- Database o viste ottimizzate per la lettura (denormalizzate, materializzate, indicizzate per la UI).
Talvolta si usano tecnologie differenti per i due mondi: ad esempio un database relazionale per la scrittura e un database documentale o un motore di ricerca per la lettura.
Esempio concettuale
Immaginiamo un sistema di gestione ordini. In un approccio CRUD tradizionale potremmo avere un servizio con metodi come:
// Esempio semplificato in stile CRUD
public class OrderService
{
public Order GetOrder(int id) { /* ... */ }
public List<Order> GetOrdersForCustomer(int customerId) { /* ... */ }
public void SaveOrder(Order order) { /* ... */ }
public void UpdateOrderStatus(int orderId, OrderStatus status) { /* ... */ }
}
Con CQRS separiamo i percorsi:
- Un CommandHandler per gestire la richiesta di creare/modificare un ordine.
- Un QueryHandler per recuperare gli ordini in una forma pronta per la UI.
// Comando: crea un nuovo ordine
public record CreateOrderCommand(int CustomerId, List<OrderItemDto> Items);
// Gestore del comando
public class CreateOrderCommandHandler
{
public Task Handle(CreateOrderCommand command)
{
// 1. Carica il cliente e le informazioni necessarie
// 2. Applica le regole di business (limiti di credito, disponibilità prodotti, ecc.)
// 3. Crea l'aggregato Order e lo persiste
}
}
// Query: ottieni ordini per un cliente
public record GetOrdersForCustomerQuery(int CustomerId);
// Gestore della query
public class GetOrdersForCustomerQueryHandler
{
public Task<IReadOnlyList<CustomerOrderReadModel>> Handle(GetOrdersForCustomerQuery query)
{
// Esegue query su un modello di lettura (ad esempio viste denormalizzate)
}
}
CQRS e Event Sourcing
CQRS viene spesso (ma non necessariamente) abbinato a Event Sourcing. Con Event Sourcing lo stato degli oggetti di dominio non è salvato come snapshot finale, ma come sequenza di eventi immutabili che descrivono cosa è successo nel tempo (OrderCreated, ItemAddedToOrder, OrderShipped...).
In questo scenario, il flusso tipico è:
- Un comando viene ricevuto (es.
CreaOrdine). - Il comando viene applicato al modello di dominio.
- Il modello genera eventi di dominio (es.
OrdineCreato). - Gli eventi vengono salvati nello store eventi.
- Processi separati ascoltano gli eventi e aggiornano uno o più modelli di lettura.
La combinazione CQRS + Event Sourcing permette:
- Audit completo delle modifiche (è possibile ricostruire lo stato nel tempo).
- Facilità nel costruire proiezioni differenti per la lettura.
- Possibilità di reprocessare gli eventi per creare nuovi modelli di lettura.
Modelli di lettura e proiezioni
Un modello di lettura (read model) è una rappresentazione dei dati ottimizzata per una certa vista o use case. Viene spesso chiamato anche proiezione.
Esempi:
- Dashboard con conteggi aggregati per giorno/mese.
- Timeline degli eventi di un cliente.
- Elenco ordini pronto per essere mostrato in tabella con colonne, filtri e ordinamenti specifici.
In CQRS, i modelli di lettura vengono aggiornati reagendo agli eventi provenienti dal lato scrittura.
// Esempio di proiezione semplificata
public class OrdersProjection
{
// Metodo chiamato quando viene emesso un evento OrderCreated
public Task When(OrderCreated e)
{
// Inserisce una riga nella tabella OrdersReadModel
}
// Metodo chiamato quando viene emesso un evento OrderStatusChanged
public Task When(OrderStatusChanged e)
{
// Aggiorna lo stato nella tabella OrdersReadModel
}
}
Consistenza: immediata vs eventuale
Quando lettura e scrittura sono separate, è comune accettare una consistenza eventuale:
- Subito dopo un comando di scrittura, il modello di lettura potrebbe non essere ancora aggiornato.
- Dopo un breve tempo (millisecondi o secondi) i modelli di lettura vengono allineati tramite gli eventi.
È fondamentale progettare l'esperienza utente considerando questa eventuale latenza:
- Mostrare messaggi di stato (es. "Ordine in elaborazione").
- Gestire cache e refresh automatici.
- Limitare i casi in cui la consistenza deve essere immediata, ad esempio per vincoli forti di business.
Vantaggi di CQRS
- Maggiore chiarezza del codice: la separazione dei ruoli rende il codice più leggibile e comprensibile.
- Modelli ottimizzati: si possono modellare lettura e scrittura in modo indipendente sulle rispettive esigenze.
- Scalabilità: è possibile scalare indipendentemente lettura e scrittura, anche usando tecnologie diverse.
- Testabilità: command e query handler sono spesso facili da testare in isolamento.
- Flessibilità evolutiva: è possibile aggiungere nuovi modelli di lettura senza toccare il modello di scrittura.
Svantaggi e costi
CQRS introduce anche complessità aggiuntiva, quindi non è adatto a tutti i progetti. Alcuni svantaggi:
- Maggiore complessità architetturale: più componenti (handler, bus di messaggi, proiezioni, modelli di lettura...).
- Consistenza eventuale: serve maggiore attenzione nella UX e nella gestione dei casi limite.
- Integrazione e debug più complessi: la presenza di code di messaggi o processi asincroni rende più difficile seguire il flusso end-to-end.
- Non necessario per domini semplici: in molti casi un buon design orientato al dominio e un CRUD pulito sono sufficienti.
Quando ha senso usare CQRS
CQRS può essere una scelta sensata quando:
- Il dominio è complesso e ricco di regole di business.
- Requisiti di lettura e scrittura sono molto diversi (es. tantissime letture, poche scritture pesanti).
- Servono viste molto ottimizzate per la UI o per il reporting.
- È importante poter evolvere nel tempo le viste senza toccare il core di dominio.
- Si prevede di usare Event Sourcing o un modello a eventi.
Al contrario, CQRS potrebbe essere eccessivo se:
- L'applicazione è piccola o a breve vita.
- La complessità del dominio è bassa.
- Le performance attese sono gestibili con un normale approccio CRUD ben progettato.
Pattern e tecnologie spesso usati con CQRS
- Event Sourcing: per persistere eventi anziché stato aggregato.
- Message bus / code di messaggi: per propagare eventi e comandi tra componenti.
- DDD (Domain-Driven Design): per modellare il dominio in modo ricco sul lato scrittura.
- Database multipli: un DB per la scrittura, uno o più per la lettura.
Esempio più completo di pipeline CQRS
Immaginiamo il flusso di creazione di un ordine in un sistema CQRS + Event Sourcing:
- La UI invia un
CreateOrderCommandall'API. - L'API valida i dati di base e instrada il comando verso il
CreateOrderCommandHandler. - Il command handler carica eventuali dati necessari (cliente, prodotti...).
- Il modello di dominio applica le regole di business e genera un evento
OrderCreated. - L'evento viene salvato nello store eventi.
- Un processo di proiezione ascolta l'evento e aggiorna il modello di lettura (es. tabella OrdersReadModel).
- Quando l'utente chiede la lista degli ordini, una query legge direttamente da OrdersReadModel.
// Pseudo-codice semplificato della pipeline di comando
public class CreateOrderCommandHandler
{
private readonly IOrderRepository _repository;
private readonly IEventBus _eventBus;
public async Task Handle(CreateOrderCommand command)
{
var order = new Order(command.CustomerId);
foreach (var item in command.Items)
{
order.AddItem(item.ProductId, item.Quantity, item.UnitPrice);
}
// Il dominio potrebbe generare eventi interni
var events = order.GetUncommittedEvents();
// Persisto l'aggregato o gli eventi
await _repository.Save(order);
// Pubblico gli eventi sul bus
await _eventBus.Publish(events);
}
}
Linee guida pratiche
Alcuni suggerimenti per introdurre CQRS in modo pragmatico:
- Inizia semplice: separa logica di lettura e scrittura a livello di codice (handler, servizi) senza introdurre subito Event Sourcing o database multipli.
- Introdurre CQRS solo dove serve: applicalo ai bounded context o ai moduli più complessi, non per forza a tutto il sistema.
- Automatizza la sincronizzazione dei read model: se usi eventi, assicurati di avere strumenti per monitorare e riprocessare le proiezioni.
- Progetta la UX tenendo conto della consistenza eventuale: messaggi chiari, refresh dei dati, gestione di stati intermedi.
Conclusione
CQRS è un pattern potente per gestire domini complessi, migliorare la scalabilità e rendere il codice più chiaro separando responsabilità di lettura e scrittura. Tuttavia introduce complessità: va adottato solo quando porta un beneficio concreto e non come soluzione predefinita per qualsiasi applicazione.
Un approccio pragmatico è partire da una separazione logica di command e query all'interno del codice (ad esempio con handler dedicati) e solo successivamente, se necessario, evolvere verso modelli di lettura separati, Event Sourcing e infrastrutture più sofisticate.