Implementare CQRS in Go: una guida pratica per un'architettura scalabile
Implementa CQRS in Go senza inutili formalismi
CQRS è uno di quei pattern che viene spesso sopravvalutato, complicato eccessivamente e talvolta diagnosticato erroneamente come la cura per la noia del buon vecchio CRUD.
La versione utile è molto più semplice: separa il codice che modifica lo stato dal codice che legge lo stato, e lascia che ciascun lato evolva in base al proprio compito. Martin Fowler descrive CQRS come l’uso di un modello diverso per aggiornare le informazioni rispetto a quello utilizzato per leggerle, avvertendo allo stesso tempo che per la maggior parte dei sistemi aggiunge una complessità rischiosa. Microsoft sottolinea lo stesso concetto fondamentale in termini più operativi: separa i modelli di lettura e scrittura in modo che ciascuno possa essere ottimizzato in modo indipendente.

Se lavori con Go, questa idea si adatta in modo insolitamente bene al linguaggio. Go è bravo nei confini espliciti, nelle interfacce piccole, nei tipi di dati semplici e nei pacchetti orientati ai casi d’uso. Questo rende il CQRS di base in Go molto meno teatrale di quanto spesso appaia nelle presentazioni alle conferenze. Non hai bisogno di event sourcing, Kafka o tre database per iniziare. Anzi, sia le linee guida CQRS di Microsoft che gli esempi Go di Three Dots Labs mostrano che un’implementazione semplice può condividere lo stesso archivio sottostante, con handler di comandi e query separati aggiunti per primi e infrastrutture più sofisticate introdotte solo quando il problema lo richiede effettivamente.
Cosa significa realmente CQRS
Nel suo nucleo, CQRS traccia una linea netta tra comandi e query. Una query legge i dati e non dovrebbe modificare lo stato del sistema. Un comando cambia lo stato e non dovrebbe restituire dati di dominio come risultato principale. Three Dots Labs formulano questo concetto in termini pratici Go: le query restituiscono dati e i comandi apportano modifiche, con gli errori che costituiscono un risultato normale dei comandi. Questo è il movimento di base. Tutto il resto è opzionale.
Un malinteso comune è che CQRS significhi automaticamente database separati, proiezioni asincrone o event sourcing. Questo non è vero. La guida ai pattern di Microsoft tratta esplicitamente gli archivi dati separati come la forma più avanzata, non come quella predefinita, e Three Dots Labs mostrano un’implementazione Go in cui le query leggono dallo stesso database delle scritture perché ciò è sufficiente per il sistema in questione. Se il tuo articolo insegna chiaramente una sola cosa, che sia questa: CQRS è principalmente una scelta di modellazione e struttura dell’applicazione, non un pacchetto obbligatorio di sistemi distribuiti.
L’altro dettaglio importante è la denominazione. I comandi dovrebbero modellare l’intento di business, non le mutazioni di archiviazione. L’esempio di Microsoft contrappone “Prenota camera d’hotel” a “Imposta ReservationStatus su Riservato”, e Three Dots Labs raccomandano nomi vicini al modo in cui gli esperti di dominio parlano, come “ScheduleTraining” (PianificaAddestramento) o “CancelTraining” (AnnullaAddestramento) piuttosto che verbi generici come “Create” (Crea) e “Delete” (Elimina). In Go, questa disciplina di denominazione paga perché i nomi dei comandi diventano spesso nomi di tipi, nomi degli handler e confini dei pacchetti.
Perché i team si rivolgono a questo pattern
CQRS diventa attraente quando un singolo modello CRUD inizia a svolgere troppe funzioni in modo scadente. Le linee guida di Microsoft elencano i soliti punti di pressione: le rappresentazioni di lettura e scrittura degli stessi dati divergono, gli aggiornamenti concorrenti creano contesa sui blocchi, le prestazioni di lettura soffrono per la complessità delle query e le entità condivise trasformano le regole di sicurezza in un groviglio. In altre parole, il problema non è che CRUD sia moralmente sbagliato. Il problema è che un unico modello è costretto a soddisfare preoccupazioni incompatibili contemporaneamente.
Questo è particolarmente comune nei prodotti tecnici. Le scritture tendono a preoccuparsi di validazione, invarianti, transazioni e regole di business. Le letture tendono a preoccuparsi di filtri, join, aggregazione, caching, ordinamento e di servire esattamente la forma di cui una pagina o un API ha bisogno. CQRS permette al lato scrittura di rimanere rigoroso e orientato al dominio, mentre il lato lettura rimane pragmatico e orientato ai DTO. Microsoft raccomanda esplicitamente un modello di scrittura focalizzato sulla validazione e sulla consistenza, e un modello di lettura focalizzato su DTO o proiezioni ottimizzate per presentazione e reattività.
C’è anche un vantaggio a livello di team. Three Dots Labs sostengono che dividere comandi e query migliora il disaccoppiamento, rende il flusso di esecuzione più chiaro e accelera l’onboarding perché gli sviluppatori possono ispezionare una piccola lista di comandi e query disponibili invece di inseguire la logica attraverso strati di servizi casuali. Microsoft nota similmente che CQRS è particolarmente utile in ambienti collaborativi dove più utenti aggiornano gli stessi dati e i comandi hanno bisogno di sufficiente granularità per prevenire o risolvere conflitti.
Il mio parere leggermente opinabile è questo: la maggior parte dei team adotta CQRS troppo tardi, dopo che un “servizio” si è già trasformato in un monolite dal centro morbido. Ma molti team lo adottano anche troppo presto, principalmente perché il diagramma dell’architettura sembrava costoso e quindi serio. Il momento giusto è quando letture e scritture stanno chiaramente divergendo per forma, velocità o regole, non quando la tua app per le cose da fare ha ambizioni.
I vantaggi e il costo
Il CQRS di base ha vantaggi reali anche prima di aggiungere qualsiasi messaging o archivi separati. Ti offre modelli di comando più piccoli, modelli di query più piccoli, casi d’uso più chiari e luoghi più ovvi per applicare preoccupazioni trasversali come logging e strumentazione. Three Dots Labs evidenziano esplicitamente una migliore organizzazione del codice, disaccoppiamento e modelli più semplici come vittorie immediate, mentre Microservices.io evidenzia modelli di comando e query più semplici e il supporto per viste di lettura denormalizzate e scalabili.
Una volta che il problema lo giustifica, CQRS apre anche la porta a un’ottimizzazione più forte del lato lettura. Le linee guida di Microsoft notano che i modelli di lettura separati possono usare DTO, proiezioni, repliche di sola lettura o persino una tecnologia di archiviazione completamente diversa. Indica anche le viste materializzate come un modo per evitare join pesanti e percorsi di query carichi di ORM. Se stai valutando quale livello di accesso ai dati usare sul lato scrittura, Confronto ORM Go per PostgreSQL copre i compromessi tra GORM, Ent, Bun e sqlc in termini pratici. È lì che CQRS inizia a dare frutti operativamente, non solo strutturalmente.
Il costo è altrettanto reale. L’avvertimento di Fowler è ancora il punto di partenza corretto: per la maggior parte dei sistemi CQRS aggiunge complessità rischiosa. Microsoft lista la complessità aumentata e la consistenza eventuale come considerazioni centrali, mentre Microservices.io aggiunge la potenziale duplicazione del codice e il ritardo di replicazione nelle viste di lettura. Se dividi gli archivi, erediti anche il compito di mantenerli sincronizzati, di solito attraverso eventi, senza affidarti a una transazione distribuita ordinata tra il tuo database e il broker.
L’event sourcing non elimina quel costo; ne cambia la forma. Le linee guida CQRS di Microsoft dicono che l’event sourcing può rendere l’archivio eventi l’unica fonte di verità e permetterti di ricostruire viste materializzate riproducendo la cronologia, mentre Event Horizon punta alla tracciabilità e al logging di audit come benefici maggiori. Ma Microsoft avverte anche che la generazione delle viste, la riproduzione e la gestione degli eventi aggiungono più complessità progettuale, e suggerisce snapshot per ridurre i costi di riproduzione. Questo è perché preferisco spiegare l’event sourcing come “CQRS più una seconda decisione difficile”, non come biglietto d’ingresso.
Una regola pratica utile da tenere a mente è che il CQRS di base è economico mentre il CQRS distribuito è costoso, e confondere le due conversazioni è uno dei modi più comuni in cui i team finiscono con molta più complessità di quanto il problema abbia mai richiesto.
Una semplice implementazione CQRS in Go
Un primo passo sensato in Go è mantenere un unico database e dividere solo il livello dell’applicazione. I comandi possiedono le regole di business e la persistenza. Le query restituiscono modelli di lettura modellati per i chiamanti. Questo è esattamente il tipo di CQRS di base che Three Dots Labs raccomandano prima di passare a bus asincroni o archivi di lettura separati.
Inizia con i comandi
package blog
import (
"context"
"errors"
"time"
)
type PublishPostCommand struct {
Title string
Slug string
BodyMD string
Author string
}
type PostRepository interface {
NextID(ctx context.Context) (string, error)
Save(ctx context.Context, post Post) error
}
type Post struct {
ID string
Title string
Slug string
BodyMD string
Author string
PublishedAt time.Time
}
type PublishPostHandler struct {
Repo PostRepository
Now func() time.Time
}
func (h PublishPostHandler) Handle(ctx context.Context, cmd PublishPostCommand) error {
if cmd.Title == "" || cmd.Slug == "" || cmd.BodyMD == "" {
return errors.New("title, slug, and body are required")
}
id, err := h.Repo.NextID(ctx)
if err != nil {
return err
}
post := Post{
ID: id,
Title: cmd.Title,
Slug: cmd.Slug,
BodyMD: cmd.BodyMD,
Author: cmd.Author,
PublishedAt: h.Now(),
}
return h.Repo.Save(ctx, post)
}
Questo handler non cerca di servire una pagina, modellare una risposta a lista o ottimizzare SQL per una griglia di card. Si limita ad applicare l’intento e a persistere un aggregato valido. Questo è il lato comando che svolge un lavoro bene.
Aggiungi le query
package blog
import "context"
type PostView struct {
ID string
Title string
Slug string
Author string
PublishedAt string
Excerpt string
}
type LatestPostsQuery struct {
Limit int
}
type PostReadModel interface {
Latest(ctx context.Context, limit int) ([]PostView, error)
BySlug(ctx context.Context, slug string) (PostView, error)
}
type LatestPostsHandler struct {
ReadModel PostReadModel
}
func (h LatestPostsHandler) Handle(ctx context.Context, q LatestPostsQuery) ([]PostView, error) {
limit := q.Limit
if limit <= 0 {
limit = 10
}
return h.ReadModel.Latest(ctx, limit)
}
type GetPostBySlugQuery struct {
Slug string
}
type GetPostBySlugHandler struct {
ReadModel PostReadModel
}
func (h GetPostBySlugHandler) Handle(ctx context.Context, q GetPostBySlugQuery) (PostView, error) {
return h.ReadModel.BySlug(ctx, q.Slug)
}
Nota come il lato lettura restituisce un PostView, non il modello di scrittura. Questo rispecchia la raccomandazione di Microsoft che il modello di lettura sia ottimizzato per DTO e presentazione, mentre il modello di scrittura è ottimizzato per l’integrità transazionale e le regole di dominio.
Collega tutto come un’applicazione Go, non come un santuario
package app
import "your/module/internal/blog"
type Application struct {
Commands Commands
Queries Queries
}
type Commands struct {
PublishPost blog.PublishPostHandler
}
type Queries struct {
LatestPosts blog.LatestPostsHandler
GetPostBySlug blog.GetPostBySlugHandler
}
Questa struttura non è accidentale. Three Dots Labs usano un pattern molto simile in Wild Workouts: un tipo Application che espone Commands e Queries, con handler concreti collegati da pacchetti separati app/command e app/query. Il loro codice di composizione del servizio importa quei pacchetti separatamente e costruisce un unico oggetto applicazione da essi. È un modo pulito e “Go-ish” per rendere il confine evidente senza il Dramma dei Framework. Se il tuo grafo di dipendenze diventa complesso mano a mano che gli handler moltiplicano, Dependency Injection in Go copre Wire, Dig e pattern di iniezione tramite costruttore che si compongono naturalmente con questa struttura basata su handler.
Se in seguito hai bisogno di comandi asincroni, eventi cross-service o un indice di ricerca denormalizzato, puoi aggiungerli da questa base. Three Dots Labs presentano esplicitamente i bus di comandi asincroni e i database di query separati come ottimizzazioni successive, non come punto di partenza.
Librerie Go da conoscere
L’ecosistema CQRS di Go è più ristretto di quello .NET, che onestamente è una benedizione. Puoi surveyare le opzioni reali in un pomeriggio e evitare di adottare tre astrazioni di cui non hai bisogno.
Watermill
Watermill è la scelta moderna più chiara quando vuoi CQRS più messaging. Il suo componente CQRS è un’API ad alto livello che ti permette di lavorare con struct Go piuttosto che messaggi grezzi, e i suoi blocchi costruttivi includono un EventBus, EventProcessor, CommandBus e CommandProcessor. La documentazione copre anche gruppi di handler eventi per l’elaborazione ordinata su topic condivisi, un esempio di modello di lettura e metadati di marshaling personalizzati. Al di fuori dello strato CQRS, Watermill supporta un’ampia gamma di back-end pub/sub tra cui RabbitMQ, Kafka, NATS Jetstream, Redis Streams, Google Cloud Pub/Sub, SQL, HTTP e altri. Pkg.go.dev marca Watermill come pronto per la produzione con un’API pubblica stabile dalla v1.0.0, e la versione del modulo pubblicata attualmente è v1.5.2, con GitHub che elenca quel rilascio il 13 maggio.
commandBus, err := cqrs.NewCommandBusWithConfig(pub, cfg)
eventBus, err := cqrs.NewEventBusWithConfig(pub, cfg)
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(router, cfg)
eventProcessor, err := cqrs.NewEventProcessorWithConfig(router, cfg)
Usa Watermill quando comandi ed eventi devono attraversare i confini del processo, quando vuoi che le semantiche di retry e ridistribuzione siano di prima classe, o quando sai che il tuo servizio “semplice” è già a metà strada verso la realtà event-driven. Il rovescio della medaglia è che ora stai avendo conversazioni su broker, topic, ordinamento e idempotenza che tu lo voglia o meno. Questo non è un difetto di Watermill. Questo è il costo dello spazio dei problemi.
Event Horizon
Event Horizon è un toolkit CQRS e event sourcing per Go. I suoi manutentori lo descrivono come usato in sistemi di produzione, ma notano anche che l’API non è definitiva. Il toolkit fornisce helper per registrazione di aggregati, comandi ed eventi, implementazioni ufficiali di archivi eventi per varianti in memoria e MongoDB, supporto per proiezioni e repository, ed esempi che includono un’applicazione basata sul pattern outbox. Il flusso di rilascio è ancora attivo, con GitHub che mostra v0.17.0 il 16 giugno e rilasci precedenti che aggiungono funzionalità come snapshot, proiezioni retryable, pianificazione persistente dei comandi e il pattern outbox.
eh.RegisterAggregate(func(id uuid.UUID) eh.Aggregate {
return &InvoiceAggregate{ID: id}
})
eh.RegisterCommand(func() eh.Command {
return &CreateInvoiceCommand{}
})
Event Horizon ha più senso quando l’event sourcing è il punto, non un’estensione futura opzionale. Se vuoi stream audit-friendly, cronologia riproducibile, proiezioni e un modello centrato sull’archivio eventi, è un’opzione seria. Se vuoi solo servizi di applicazione più puliti in un monolite, è probabilmente più meccanismo di quanto tu ne abbia bisogno. La nota “l’API non è definitiva” significa anche che dovresti budgetare un po’ più di adattamento nel tempo rispetto a quanto faresti con Watermill.
Go-MediatR
Go-MediatR non è un framework CQRS completo, ma è utile per CQRS in-process. Il suo README lo descrive come un’implementazione del pattern mediatore usata con CQRS, con dispatch richiesta/risposta per comandi e query, dispatch notifiche per eventi e comportamenti di pipeline per preoccupazioni trasversali. Il progetto ha anche rilasci taggati, con GitHub che elenca v1.4.0 come ultimo rilascio ed evidenziando la registrazione degli handler thread-safe e miglioramenti correlati alla concorrenza.
resp, err := mediatr.Send[*CreateProductCommand, *CreateProductResponse](ctx, cmd)
post, err := mediatr.Send[*GetPostBySlugQuery, *PostView](ctx, query)
Questo è un buon fit se vuoi comandi e query basati su handler, ma non un broker, motore di proiezione o archivio eventi. È particolarmente amichevole per i team provenienti da MediatR in .NET. Il compromesso è ugualmente chiaro: devi ancora progettare la tua persistenza, la strategia di aggiornamento del modello di lettura e la storia di integrazione fuori-processo. In altre parole, ti dà il confine dell’applicazione, non l’intera architettura.
Framework più vecchi e materiale di riferimento
Ci sono librerie CQRS Go più vecchie che sono ancora istruttive, ma le tratterei come materiale di riferimento prima che come default per nuovi progetti.
jetbasrawi/go.cqrs si descrive come un’implementazione di riferimento CQRS Go con applicazioni campione basate sui principi di Greg Young. Tuttavia, pkg.go.dev mostra nessun go.mod valido, nessuna versione taggata e nessuna versione stabile, mentre GitHub mostra nessun rilascio e i metadati del pacchetto sono stati pubblicati 7.4 anni fa. Questa è una storia utile, non un segnale forte per un’adozione di produzione fresca nel 2026.
andrewwebber/cqrs è simile: fornisce event sourcing, emissione e elaborazione comandi, pubblicazione eventi e generazione del modello di lettura dagli eventi pubblicati, ma anche i metadati del pacchetto sono stati pubblicati 7.4 anni fa. Lo leggerei assolutamente se vuoi capire come le librerie CQRS Go precedenti affrontavano il problema. Sarei cauto nel renderlo la base di una nuova codebase a meno che tu non sia felice di diventare manutentore part-time del tuo stack architettonico.
Un layout pratico per progetti Go
Un layout CQRS Go tipico dovrebbe rendere i casi d’uso evidenti, non seppellirli sotto astrazioni generiche. Wild Workouts è un buon riferimento qui. Il repository separa i contesti limitati sotto internal, mantiene comandi e query in pacchetti dell’applicazione distinti, e li collega a un tipo Application che espone Commands e Queries. La composizione del servizio riunisce adattatori, handler e dipendenze esplicitamente. I pattern qui descritti si allineano con la guida più ampia in Go Project Structure: Practices & Patterns, che copre il set più ampio di decisioni di layout che i team affrontano man mano che le codebase Go crescono.
Un layout pragmatico è simile a questo:
internal/
blog/
app/
app.go
command/
publish_post.go
unpublish_post.go
query/
get_post_by_slug.go
latest_posts.go
domain/
post.go
slug.go
adapters/
postgres/
post_repository.go
post_read_model.go
ports/
http/
handler.go
service/
application.go
Questo layout ha alcuni vantaggi.
Primo, gli handler di comandi e query vivono vicino ai casi d’uso che implementano. Questo rende più difficile nascondere il comportamento di business nei repository o in handler nominati dopo gli strati di trasporto. Three Dots Labs lo fanno direttamente in Wild Workouts, dove app/command e app/query sono pacchetti separati e l’Application di livello superiore raggruppa gli handler per responsabilità.
Secondo, il pacchetto dominio può rimanere focalizzato su invarianti e comportamento, mentre il lato query è libero di restituire DTO e proiezioni. Questo si allinea con le linee guida di Microsoft sul modello di scrittura e lettura e evita l’anti-pattern CQRS comune in cui il lato query è costretto a tornare attraverso oggetti di dominio solo per purezza ideologica.
Terzo, questa struttura scala dal CQRS più piccolo utile a varianti più pesanti. Puoi mantenere un unico database PostgreSQL e due implementazioni di repository oggi, poi aggiungere un indice di ricerca o una proiezione di lettura event-driven in seguito senza dover riscrivere l’intera forma dell’applicazione. Three Dots Labs descrivono esplicitamente quella progressione dal CQRS di base ai bus di comandi asincroni e agli archivi di query separati solo quando il sistema ne ha bisogno.
Quando CQRS si adatta e quando no
CQRS ha senso quando letture e scritture sono problemi veramente diversi. Microsoft lo raccomanda per carichi di lavoro dove i modelli di lettura e scrittura hanno bisogno di ottimizzazione indipendente, dove più utenti collaborano sugli stessi dati, e dove una separazione chiara aiuta con prestazioni, scalabilità e sicurezza. Microservices.io aggiunge un altro fit classico: viste denormalizzate ad alte prestazioni costruite da eventi di dominio o proiezioni materializzate. Three Dots Labs puntano anche a logica di business complessa, manutenibilità ed estensione futura verso comandi asincroni o archivi di lettura specializzati come ragioni forti per adottarlo in Go.
In pratica, questo spesso significa sistemi con regole di dominio ricche, modelli di lettura costosi, viste di reporting che non mappano bene sugli aggregati, o microservizi che pubblicano eventi e costruiscono proiezioni altrove. In quei contesti, il Pattern Saga per transazioni distribuite appare spesso accanto a CQRS come meccanismo di coordinamento per operazioni di business multi-step che attraversano i confini dei servizi. Si adatta anche a prodotti dove il lato scrittura deve essere rigoroso e auditabile mentre il lato lettura deve essere veloce e modellato per il consumo UI o API. Se stai già parlando di proiezioni, repliche o ricostruzione di viste dagli eventi, sei probabilmente nel territorio CQRS che tu usi l’etichetta o meno.
CQRS non ha senso quando il tuo servizio è un editor di dati diretto. Fowler dice apertamente che per la maggior parte dei sistemi CQRS aggiunge complessità rischiosa, e Three Dots Labs dicono che i servizi CRUD semplici che ricevono e restituiscono essenzialmente gli stessi dati non sono un buon fit. Nel loro esempio Wild Workouts, un servizio utenti più semplice non usa Clean Architecture e CQRS perché i pattern non pagherebbero il loro affitto lì.
Questa è la parte vale la pena dirlo chiaramente in un blog tecnico: CQRS non è un badge di maturità ma un compromesso deliberato, e ha senso solo quando hai effettivamente bisogno di ciò che ti dà. Se il tuo pannello admin scrive righe e legge le stesse righe indietro, non separare il modello solo perché puoi. Se i tuoi handler di comandi sono principalmente “imposta campo X sul record Y”, non hai un problema CQRS. Hai un’applicazione normale, e questo è software perfettamente rispettabile.
Pensieri finali
Il miglior modo per implementare CQRS in Go è iniziare con la versione noiosa. Separa gli handler di comando dagli handler di query. Lascia che i comandi modellino l’intento di business. Lascia che le query restituiscano modelli di lettura. Mantieni lo stesso database se è tutto ciò di cui hai bisogno. Poi, solo quando il sistema ti costringe, aggiungi bus asincroni, proiezioni, archivi separati o event sourcing. Questa progressione è coerente con l’avvertimento di Fowler sulla complessità, le linee guida CQRS a fasi di Microsoft e gli esempi Go pragmatici di Three Dots Labs.
Se hai bisogno di una libreria, Watermill è la scelta general-purpose più forte per CQRS guidato da messaggi in Go, Event Horizon è convincente quando l’event sourcing è il centro di gravità, e Go-MediatR è un tocco leggero buono quando hai bisogno solo di dispatch di comandi e query in-process. Tutto il resto dovrebbe guadagnare il suo posto molto attentamente. Per una mappa più ampia di struttura del codice, integrazione e pattern di accesso ai dati in sistemi Go di produzione, la guida App Architecture è un compagno utile.
Quello, alla fine, è la risposta più “Go-like” a CQRS: usa il pattern, non il costume.