Mise en œuvre de CQRS en Go : Un guide pratique pour une architecture évolutive

Mettre en œuvre CQRS en Go sans formalités superflues

Sommaire

CQRS est l’un de ces modèles qui est souvent survendu, surcomplexifié et parfois mal diagnostiqué comme un remède à l’ennui du CRUD traditionnel.

La version utile est beaucoup plus simple : séparer le code qui modifie l’état du code qui lit l’état, puis laisser chaque côté évoluer selon sa propre fonction. Martin Fowler décrit CQRS comme l’utilisation d’un modèle différent pour mettre à jour les informations par rapport à celui utilisé pour les lire, tout en avertissant que pour la plupart des systèmes, cela ajoute une complexité risquée. Microsoft fait le même point central en termes plus opérationnels : séparer les modèles de lecture et d’écriture afin que chacun puisse être optimisé indépendamment.

CQRS en Go — commandes et requêtes comme des chemins séparés à travers un hub de gopher Go

Si vous travaillez en Go, cette idée correspond unusually bien au langage. Go est bon pour les limites explicites, les petites interfaces, les types de données simples et les packages orientés cas d’utilisation. Cela rend le CQRS de base en Go beaucoup moins théâtral qu’il n’apparaît souvent dans les diapositives de conférences. Vous n’avez pas besoin de la source d’événements (event sourcing), de Kafka ou de trois bases de données pour commencer. En fait, les directives CQRS de Microsoft et les exemples Go de Three Dots Labs montrent qu’une implémentation simple peut partager le même magasin sous-jacent, avec des gestionnaires de commandes et de requêtes ajoutés séparément et une infrastructure plus sophistiquée introduite uniquement lorsque le problème le demande réellement.

Ce que signifie réellement CQRS

Au cœur, CQRS trace une ligne dure entre les commandes et les requêtes. Une requête lit des données et ne doit pas modifier l’état du système. Une commande change l’état et ne doit pas retourner de données métier comme résultat principal. Three Dots Labs formulent cela en termes pratiques Go : les requêtes retournent des données et les commandes effectuent des changements, les erreurs étant un résultat normal de commande. C’est le mouvement de base. Tout le reste est optionnel.

Un malentendu courant est que CQRS signifie automatiquement des bases de données séparées, des projections asynchrones ou une source d’événements. Ce n’est pas vrai. Le guide de motifs de Microsoft traite explicitement les magasins de données séparés comme la forme plus avancée, et non la valeur par défaut, et Three Dots Labs montrent une implémentation Go où les requêtes lisent depuis la même base de données que les écritures car cela est suffisant pour le système en question. Si votre article n’enseigne qu’une seule chose clairement, que ce soit ceci : CQRS est principalement un choix de modélisation et de structure d’application, et non un paquetage obligatoire de systèmes distribués.

L’autre détail important est la nomenclature. Les commandes doivent modéliser l’intention métier, et non les mutations de stockage. L’exemple de Microsoft contraste « Réserver une chambre d’hôtel » avec « Définir ReservationStatus sur Réservé », et Three Dots Labs recommandent des noms proches de la façon dont les experts du domaine parlent, tels que « PlanifierFormation » ou « AnnulerFormation » plutôt que des verbes génériques comme « Créer » et « Supprimer ». En Go, cette discipline de nommage paie ses dividendes car les noms des commandes deviennent souvent des noms de type, des noms de gestionnaires et des limites de package.

Pourquoi les équipes s’y tournent

CQRS devient attrayant lorsqu’un modèle CRUD unique commence à mal faire trop de tâches. Les directives de Microsoft listent les points de pression habituels : les représentations en lecture et en écriture des mêmes données divergent, les mises à jour concurrentes créent des conflits de verrouillage, les performances de lecture souffrent sous la complexité des requêtes et les entités partagées transforment les règles de sécurité en un enchevêtrement. En d’autres termes, le problème n’est pas que le CRUD est moralement mauvais. Le problème est qu’un seul modèle est forcé de satisfaire des préoccupations incompatibles simultanément.

C’est particulièrement courant dans les produits techniques. Les écritures ont tendance à se soucier de la validation, des invariants, des transactions et des règles métier. Les lectures ont tendance à se soucier des filtres, des jointures, de l’agrégation, de la mise en cache, du tri et de servir exactement la forme dont une page ou une API a besoin. CQRS permet au côté écriture de rester strict et orienté domaine, tandis que le côté lecture reste pragmatique et orienté DTO. Microsoft recommande explicitement un modèle d’écriture axé sur la validation et la cohérence, et un modèle de lecture axé sur les DTO ou les projections optimisées pour la présentation et la réactivité.

Il y a aussi un avantage au niveau de l’équipe. Three Dots Labs soutiennent que la séparation des commandes et des requêtes améliore le découplage, rend le flux d’exécution plus clair et accélère l’intégration car les développeurs peuvent inspecter une petite liste de commandes et de requêtes disponibles plutôt que de traquer la logique à travers des couches de service aléatoires. Microsoft note également que CQRS est particulièrement utile dans les environnements collaboratifs où plusieurs utilisateurs mettent à jour les mêmes données et où les commandes ont besoin d’une granularité suffisante pour prévenir ou résoudre les conflits.

Mon opinion légèrement biaisée est la suivante : la plupart des équipes adoptent CQRS trop tard, après qu’un « service » est déjà devenu un monolithe à cœur mou. Mais beaucoup d’équipes l’adoptent aussi trop tôt, principalement parce que le diagramme d’architecture semblait coûteux et donc sérieux. Le bon moment est lorsque les lectures et les writings dérivent clairement l’un de l’autre en forme, vitesse ou règles, et non lorsque votre application de liste de tâches a des aspirations.

Les avantages et le coût

Le CQRS de base a de vrais avantages même avant d’ajouter tout messagerie ou magasins séparés. Il vous donne des modèles de commande plus petits, des modèles de requête plus petits, des cas d’utilisation plus clairs et des emplacements plus évidents pour appliquer des préoccupations transversales comme la journalisation et l’instrumentation. Three Dots Labs soulignent explicitement une meilleure organisation du code, le découplage et des modèles plus simples comme des gains immédiats, tandis que Microservices.io met en avant des modèles de commande et de requête plus simples et le support pour des vues de lecture dénormalisées et évolutives.

Une fois que le problème le justifie, CQRS ouvre également la porte à une optimisation plus forte du côté lecture. Les directives de Microsoft notent que les modèles de lecture séparés peuvent utiliser des DTO, des projections, des répliques en lecture seule ou même une technologie de stockage complètement différente. Il pointe également vers les vues matérialisées comme un moyen d’éviter les jointures lourdes et les chemins de requête lourds en ORM. Si vous évaluez quelle couche d’accès aux données utiliser du côté écriture, Comparaison des ORM Go pour PostgreSQL couvre les compromis entre GORM, Ent, Bun et sqlc en termes pratiques. C’est là que CQRS commence à payer ses dividendes opérationnellement, et pas seulement structurellement.

Le coût est tout aussi réel. L’avertissement de Fowler reste le point de départ correct : pour la plupart des systèmes, CQRS ajoute une complexité risquée. Microsoft liste la complexité accrue et la cohérence éventuelle comme des considérations centrales, tandis que Microservices.io ajoute la duplication potentielle de code et le délai de réplication dans les vues de lecture. Si vous séparez les magasins, vous héritez également de la tâche de les garder synchronisés, généralement via des événements, sans vous fier à une transaction distribuée propre entre votre base de données et votre courtier.

La source d’événements (event sourcing) ne supprime pas ce coût ; elle en change la forme. Les directives CQRS de Microsoft disent que la source d’événements peut faire du magasin d’événements la seule source de vérité et vous permettre de reconstruire des vues matérialisées en rejouant l’historique, tandis qu’Event Horizon pointe vers la traçabilité et la journalisation d’audit comme des avantages majeurs. Mais Microsoft avertit également que la génération de vues, le rejeu et le traitement des événements ajoutent plus de complexité de conception, et suggère des instantanés (snapshots) pour réduire les coûts de rejeu. C’est pourquoi je préfère expliquer la source d’événements comme « CQRS plus une deuxième décision difficile », et non comme le ticket d’entrée.

Une règle générale utile à garder à l’esprit est que le CQRS de base est bon marché tandis que le CQRS distribué est coûteux, et confondre les deux conversations est l’un des moyens les plus courants pour que les équipes se retrouvent avec beaucoup plus de complexité que le problème n’a jamais exigé.

Une implémentation CQRS simple en Go

Une première étape sensée en Go est de garder une seule base de données et de ne séparer que la couche d’application. Les commandes détiennent les règles métier et la persistance. Les requêtes retournent des modèles de lecture façonnés pour les appelants. C’est exactement le genre de CQRS de base que Three Dots Labs recommandent avant de passer aux buses asynchrones ou aux magasins de lecture séparés.

Commencez par les commandes

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)
}

Ce gestionnaire n’essaie pas de servir une page, de façonner une réponse de liste ou d’optimiser SQL pour une grille de cartes. Il applique simplement l’intention et persiste un agrégat valide. C’est le côté commande qui fait bien un seul travail.

Ajoutez des requêtes

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)
}

Remarquez que le côté lecture retourne un PostView, et non le modèle d’écriture. Cela reflète la recommandation de Microsoft selon laquelle le modèle de lecture doit être optimisé pour les DTO et la présentation, tandis que le modèle d’écriture est ajusté pour l’intégrité transactionnelle et les règles métier.

Câblez-le comme une application Go, pas comme un sanctuaire

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
}

Cette forme n’est pas accidentelle. Three Dots Labs utilisent un motif très similaire dans Wild Workouts : un type Application exposant Commands et Queries, avec des gestionnaires concrets câblés depuis des packages app/command et app/query séparés. Leur code de composition de service importe ces packages séparément et construit un seul objet application à partir d’eux. C’est une façon propre, typiquement Go, de rendre la limite évidente sans « Cadre Dramatique ». Si votre graph de dépendances devient complexe à mesure que les gestionnaires se multiplient, Injection de dépendances en Go couvre les motifs Wire, Dig et d’injection de constructeurs qui se composent naturellement avec cette structure basée sur les gestionnaires.

Si vous avez plus tard besoin de commandes asynchrones, d’événements inter-services ou d’un index de recherche dénormalisé, vous pouvez les ajouter à partir de cette base. Three Dots Labs présentent explicitement les buses de commandes asynchrones et les bases de données de requêtes séparées comme des optimisations ultérieures, et non comme le point de départ.

Bibliothèques Go à connaître

L’écosystème CQRS Go est plus étroit que celui de .NET, ce qui est honnêtement une bénédiction. Vous pouvez examiner les options réelles en une après-midi et éviter d’adopter trois abstractions dont vous n’avez pas besoin.

Watermill

Watermill est le choix moderne le plus clair lorsque vous voulez CQRS plus messagerie. Son composant CQRS est une API de haut niveau qui vous permet de travailler avec des structs Go plutôt qu’avec des messages bruts, et ses briques de construction incluent un EventBus, EventProcessor, CommandBus et CommandProcessor. La documentation couvre également les groupes de gestionnaires d’événements pour un traitement ordonné sur des sujets partagés, un exemple de modèle de lecture et des métadonnées de marshalling personnalisées. En dehors de la couche CQRS, Watermill prend en charge une large gamme de backends pub/sub y compris RabbitMQ, Kafka, NATS Jetstream, Redis Streams, Google Cloud Pub/Sub, SQL, HTTP et d’autres. Pkg.go.dev marque Watermill comme prêt pour la production avec une API publique stable depuis la v1.0.0, et la version de module publiée actuelle est la v1.5.2, GitHub listant cette version le 13 mai.

commandBus, err := cqrs.NewCommandBusWithConfig(pub, cfg)
eventBus, err := cqrs.NewEventBusWithConfig(pub, cfg)
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(router, cfg)
eventProcessor, err := cqrs.NewEventProcessorWithConfig(router, cfg)

Utilisez Watermill lorsque les commandes et les événements doivent traverser les limites des processus, lorsque vous voulez que les sémantiques de nouvelle tentative et de redelivery soient de première classe, ou lorsque vous savez que votre service « simple » est déjà à mi-chemin de la réalité pilotée par les événements. L’inconvénient est que vous avez maintenant des conversations sur le courtier, le sujet, l’ordonnancement et l’idempotence que vous le vouliez ou non. Ce n’est pas un défaut de Watermill. C’est le coût de l’espace problème.

Event Horizon

Event Horizon est un outil CQRS et de source d’événements pour Go. Ses mainteneurs le décrivent comme utilisé dans des systèmes de production, mais notent également que l’API n’est pas finale. L’outil fournit des aides d’enregistrement d’agrégat, de commande et d’événement, des implémentations officielles de magasin d’événements pour les variantes mémoire et MongoDB, un support pour les projections et les référentiels, et des exemples incluant une application basée sur le motif de boîte de sortie (outbox pattern). Le flux de versions est toujours actif, GitHub montrant la v0.17.0 le 16 juin et des versions précédentes ajoutant des fonctionnalités telles que les instantanés, les projections réessayables, la planification persistante des commandes et le motif de boîte de sortie.

eh.RegisterAggregate(func(id uuid.UUID) eh.Aggregate {
	return &InvoiceAggregate{ID: id}
})

eh.RegisterCommand(func() eh.Command {
	return &CreateInvoiceCommand{}
})

Event Horizon a le plus de sens lorsque la source d’événements est le point, et non une extension future optionnelle. Si vous voulez des flux conviviaux à l’audit, un historique rejouable, des projections et un modèle centré sur le magasin d’événements, c’est une option sérieuse. Si vous voulez seulement des services d’application plus propres dans un monolithe, c’est probablement plus de mécanisme que vous n’en avez besoin. La note « API n’est pas finale » signifie également que vous devez prévoir un peu plus d’adaptation au fil du temps que vous ne le feriez avec Watermill.

Go-MediatR

Go-MediatR n’est pas un framework CQRS complet, mais il est utile pour le CQRS intra-processus. Son README le décrit comme une implémentation du motif médiateur utilisée avec CQRS, avec un dispatch requête/réponse pour les commandes et les requêtes, un dispatch de notification pour les événements et des comportements de pipeline pour les préoccupations transversales. Le projet a également des versions taguées, GitHub listant la v1.4.0 comme dernière version et soulignant l’enregistrement de gestionnaire sécurisé pour les threads et les améliorations liées à la concurrence.

resp, err := mediatr.Send[*CreateProductCommand, *CreateProductResponse](ctx, cmd)
post, err := mediatr.Send[*GetPostBySlugQuery, *PostView](ctx, query)

C’est un bon choix si vous voulez des commandes et des requêtes basées sur des gestionnaires, mais pas de courtier, moteur de projection ou magasin d’événements. Il est particulièrement convivial pour les équipes provenant de MediatR dans .NET. Le compromis est tout aussi clair : vous devez encore concevoir votre propre persistance, votre stratégie de rafraîchissement du modèle de lecture et votre histoire d’intégration extra-processus. En d’autres termes, il vous donne la limite d’application, et non toute l’architecture.

Frameworks plus anciens et matériel de référence

Il y a des bibliothèques CQRS Go plus anciennes qui sont toujours instructives, mais je les traiterais comme du matériel de référence avant de les traiter comme des valeurs par défaut pour un nouveau projet.

jetbasrawi/go.cqrs se décrit comme une implémentation de référence CQRS Go avec des applications d’exemple basées sur les principes de Greg Young. Cependant, pkg.go.dev ne montre aucun go.mod valide, aucune version taguée et aucune version stable, tandis que GitHub ne montre aucune version et les métadonnées du package ont été publiées il y a 7,4 ans. C’est une histoire utile, et non un signal fort pour une adoption en production fraîche en 2026.

andrewwebber/cqrs est similaire : il fournit la source d’événements, l’émission et le traitement de commandes, la publication d’événements et la génération de modèles de lecture à partir des événements publiés, mais les métadonnées du package ont également été publiées il y a 7,4 ans. Je le lirais absolument si vous voulez comprendre comment les bibliothèques CQRS Go antérieures abordaient le problème. Je serais prudent à en faire la fondation d’une nouvelle base de code à moins que vous soyez heureux de devenir mainteneur à temps partiel de votre propre pile d’architecture.

Une mise en page de projet Go pratique

Une mise en page CQRS Go typique devrait rendre les cas d’utilisation évidents, et non les enterrer sous des abstractions génériques. Wild Workouts est une bonne référence ici. Le dépôt sépare les contextes délimités sous internal, garde les commandes et les requêtes dans des packages d’application distincts et les câble dans un type Application exposant Commands et Queries. La composition de service rassemble explicitement les adaptateurs, les gestionnaires et les dépendances. Les motifs décrits ici s’alignent avec les directives plus larges dans Structure de projet Go : Pratiques & Motifs, qui couvre l’ensemble plus large des décisions de mise en page auxquelles les équipes font face à mesure que les bases de code Go grandissent.

Une mise en page pragmatique ressemble à ceci :

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

Cette mise en page a quelques avantages.

Premièrement, les gestionnaires de commandes et de requêtes vivent près des cas d’utilisation qu’ils implémentent. Cela rend plus difficile de cacher le comportement métier dans les référentiels ou les gestionnaires nommés d’après les couches de transport. Three Dots Labs font cela directement dans Wild Workouts, où app/command et app/query sont des packages séparés et l’Application de haut niveau regroupe les gestionnaires par responsabilité.

Deuxièmement, le package domaine peut rester focalisé sur les invariants et le comportement, tandis que le côté requête est libre de retourner des DTO et des projections. Cela s’aligne avec les directives de Microsoft sur les modèles d’écriture et de lecture et évite l’anti-pattern CQRS courant où le côté requête est forcé de revenir à travers les objets domaine juste pour la pureté idéologique.

Troisièmement, cette structure évolue du CQRS le plus petit utile aux variantes plus lourdes. Vous pouvez garder une seule base de données PostgreSQL et deux implémentations de référentiel aujourd’hui, puis ajouter un index de recherche ou une projection de lecture pilotée par événements plus tard sans avoir à réécrire toute la forme de l’application. Three Dots Labs décrivent explicitement cette progression du CQRS de base aux buses de commandes asynchrones et aux magasins de requêtes séparés uniquement lorsque le système en a besoin.

Quand CQRS convient et quand il ne convient pas

CQRS a du sens lorsque les lectures et les écritures sont de vrais problèmes différents. Microsoft le recommande pour les charges de travail où les modèles de lecture et d’écriture ont besoin d’une optimisation indépendante, où plusieurs utilisateurs collaborent sur les mêmes données et où une séparation claire aide avec les performances, l’évolutivité et la sécurité. Microservices.io ajoute un autre ajustement classique : des vues dénormalisées et haute performance construites à partir d’événements métier ou de projections matérialisées. Three Dots Labs pointent également vers la logique métier complexe, la maintenabilité et l’extension future vers des commandes asynchrones ou des magasins de lecture spécialisés comme de fortes raisons de l’adopter en Go.

En pratique, cela signifie souvent des systèmes avec des règles métier riches, des modèles de lecture coûteux, des vues de rapport qui ne se mappent pas proprement aux agrégats, ou des microservices qui publient des événements et construisent des projections ailleurs. Dans ces contextes, le Motif Saga pour les transactions distribuées apparaît souvent aux côtés de CQRS comme mécanisme de coordination pour les opérations métier multi-étapes qui traversent les limites des services. Il convient également aux produits où le côté écriture doit être strict et auditable tandis que le côté lecture doit être rapide et façonné pour la consommation UI ou API. Si vous parlez déjà de projections, de répliques ou de reconstruction de vues à partir d’événements, vous êtes probablement dans le territoire CQRS que vous utilisiez l’étiquette ou non.

CQRS n’a pas de sens lorsque votre service est un éditeur de données straightforward. Fowler dit ouvertement que pour la plupart des systèmes CQRS ajoute une complexité risquée, et Three Dots Labs disent que les services CRUD simples qui reçoivent et retournent essentiellement les mêmes données ne sont pas un bon ajustement. Dans leur propre exemple Wild Workouts, un service d’utilisateurs plus simple n’utilise pas l’Architecture Propre et CQRS car les motifs ne paieraient pas leur loyer là-bas.

C’est la partie qu’il vaut la peine de dire clairement dans un blog technique : CQRS n’est pas un badge de maturité mais un compromis délibéré, et il n’a de sens que lorsque vous avez réellement besoin de ce qu’il vous donne. Si votre panneau d’administration écrit des lignes et lit les mêmes lignes en retour, ne séparez pas le modèle juste parce que vous pouvez. Si vos gestionnaires de commandes sont principalement « définir le champ X sur l’enregistrement Y », vous n’avez pas un problème CQRS. Vous avez une application normale, et c’est un logiciel parfaitement respectable.

Pensées finales

La meilleure façon d’implémenter CQRS en Go est de commencer par la version ennuyeuse. Séparez les gestionnaires de commandes des gestionnaires de requêtes. Laissez les commandes modéliser l’intention métier. Laissez les requêtes retourner des modèles de lecture. Gardez la même base de données si c’est tout ce dont vous avez besoin. Ensuite, uniquement lorsque le système vous force la main, ajoutez des buses asynchrones, des projections, des magasins séparés ou la source d’événements. Cette progression est cohérente avec l’avertissement de Fowler sur la complexité, les directives CQRS étagiées de Microsoft et les exemples Go pragmatiques de Three Dots Labs.

Si vous avez besoin d’une bibliothèque, Watermill est le choix général le plus fort pour le CQRS piloté par messages en Go, Event Horizon est séduisant lorsque la source d’événements est le centre de gravité, et Go-MediatR est une bonne touche légère lorsque vous n’avez besoin que du dispatch de commandes et de requêtes intra-processus. Tout le reste devrait gagner sa place très soigneusement. Pour une carte plus large des motifs de structure de code, d’intégration et d’accès aux données dans les systèmes Go de production, le Guide App Architecture est un compagnon utile.

C’est, en fin de compte, la réponse la plus typiquement Go à CQRS : utiliser le motif, pas le costume.

S'abonner

Recevez de nouveaux articles sur les systèmes, l'infrastructure et l'ingénierie IA.