Quand le CRUD classique ne suffit plus : introduction à CQRS et Event Sourcing
\nImaginez que vous construisez un système de réservation de billets. Tout est simple tant qu'il y a dix utilisateurs. Mais quand ils deviennent mille, puis un million, les problèmes commencent : les bases de données se bloquent, les requêtes entrent en conflit, et l'historique des modifications est impossible à reconstituer. Une situation familière ?
\nDans le monde du développement Go, où la performance et le parallélisme sont primordiaux, deux patrons architecturaux aident à résoudre ces problèmes : CQRS (Command Query Responsibility Segregation) et Event Sourcing. Aujourd'hui, nous les analysons en pratique — avec du code, des exemples et des scénarios d'utilisation réels.
\n\nQu'est-ce que CQRS en termes simples ?
\nCQRS est la séparation des responsabilités entre les commandes (écriture) et les requêtes (lecture). Dans le CRUD classique, un seul modèle de données gère à la fois la création et la lecture. Avec CQRS, nous créons deux modèles distincts :
\n- \n
- Modèle de commande — responsable de la modification de l'état (création, mise à jour, suppression) \n
- Modèle de requête — responsable uniquement de la lecture des données \n
Cela permet d'optimiser chaque côté indépendamment : l'écriture peut être strictement validée, tandis que la lecture peut être dénormalisée et rapide. En Go, cela est particulièrement efficace grâce aux goroutines et aux canaux.
\n\nExemple simple de CQRS en Go
\nCréons un système de base pour la gestion des commandes. Commençons par les commandes :
\n\ntype CreateOrderCommand struct {\n OrderID string\n UserID string\n ProductID string\n Amount float64\n}\n\ntype CommandHandler struct {\n repo *OrderRepository\n}\n\nfunc (h *CommandHandler) HandleCreateOrder(ctx context.Context, cmd CreateOrderCommand) error {\n order := Order{\n ID: cmd.OrderID,\n UserID: cmd.UserID,\n ProductID: cmd.ProductID,\n Amount: cmd.Amount,\n Status: "pending",\n CreatedAt: time.Now(),\n }\n return h.repo.Save(ctx, order)\n}\n\nCréons maintenant un modèle séparé pour la lecture :
\n\ntype OrderQuery struct {\n repo *ReadRepository\n}\n\ntype OrderView struct {\n OrderID string\n UserName string\n ProductName string\n Total float64\n Status string\n}\n\nfunc (q *OrderQuery) GetOrderSummary(ctx context.Context, orderID string) (*OrderView, error) {\n return q.repo.GetOrderView(ctx, orderID)\n}\n\nRemarquez que OrderView contient déjà des données dénormalisées (nom de l'utilisateur, nom du produit) provenant de différentes sources. Cela accélère la lecture des dizaines de fois.
Event Sourcing : enregistrer non pas l'état, mais les événements
\nEvent Sourcing change l'approche du stockage des données : au lieu de stocker l'état actuel d'un objet, nous stockons la séquence d'événements qui ont conduit à cet état. Cela offre un super-pouvoir : un historique complet des modifications et la possibilité de « rembobiner » l'état.
\n\nImplémentation d'Event Sourcing en Go
\nCommençons par les types d'événements de base :
\n\ntype Event struct {\n ID string\n AggregateID string\n Type string\n Data []byte\n Timestamp time.Time\n Version int\n}\n\ntype EventStore interface {\n Save(ctx context.Context, events []Event) error\n Load(ctx context.Context, aggregateID string) ([]Event, error)\n}\n\nImplémentons maintenant l'agrégat — l'objet qui restaure son état à partir des événements :
\n\ntype OrderAggregate struct {\n ID string\n Status string\n Items []OrderItem\n Version int\n}\n\nfunc (a *OrderAggregate) Apply(event Event) {\n switch event.Type {\n case "OrderCreated":\n var data OrderCreatedData\n json.Unmarshal(event.Data, &data)\n a.ID = data.OrderID\n a.Status = "created"\n case "OrderConfirmed":\n a.Status = "confirmed"\n case "OrderShipped":\n a.Stat