La plupart des applications lisent et écrivent les mêmes tables avec le même modèle. Cela fonctionne jusqu'à ce que les formes divergent : les écritures exigent des règles métier strictes sur une commande client à la fois, tandis que les lectures veulent des listes, des recherches et des totaux sur des milliers de commandes. CQRS sépare les deux, et l'event sourcing change ce que vous stockez au départ.
On les présente souvent ensemble, mais ce sont deux décisions distinctes. Cet article explique chacune, comment elles s'articulent, à quoi ressemble le diagramme d'un système qui combine les deux, et les coûts qui font regretter à beaucoup d'équipes de les avoir adoptées trop tôt.
CQRS en un paragraphe
Command Query Responsibility Segregation signifie utiliser un modèle pour modifier les données et un autre pour les lire. Les commandes (PlaceOrder, CancelOrder) vont à un côté écriture qui applique les règles. Les requêtes vont à un côté lecture façonné pour les écrans qui en ont besoin. L'article de Martin Fowler sur CQRS, qui attribue le pattern à Greg Young, contient aussi l'avertissement qu'il faut répéter : pour la plupart des systèmes, CQRS ajoute une complexité risquée. Utilisez-le là où lectures et écritures divergent vraiment, pas dans toute une application.
CQRS n'exige pas deux bases de données. Un modèle d'écriture et un ensemble de vues SQL sur les mêmes tables, c'est déjà du CQRS. Les stockages séparés viennent plus tard, quand la charge ou la forme des lectures l'exigent.
L'event sourcing en un paragraphe
L'event sourcing stocke les changements plutôt que l'état courant. Une commande client n'est pas une ligne avec une colonne de statut ; c'est un flux d'événements : OrderPlaced, PaymentAuthorized, OrderShipped. L'état courant est ce que vous obtenez en les rejouant. L'article de Fowler sur l'event sourcing décrit l'idée en détail. Vous obtenez gratuitement un historique d'audit complet, et vous pouvez construire à tout moment de nouvelles vues du passé, puisque rien n'a été écrasé.
Comment ils s'articulent
L'event sourcing impose presque CQRS. Rejouer des événements est un bon moyen de charger une commande client avant de la modifier, mais un très mauvais moyen de répondre à « montre-moi toutes les commandes ouvertes de plus de 100 dollars ». Le côté écriture travaille donc avec des flux d'événements, et le côté lecture maintient des modèles de lecture séparés, mis à jour à partir de ces événements.
Le déroulement :
- Une commande arrive au côté commande.
- Le handler charge l'agrégat (la commande client) en rejouant son flux, vérifie les règles métier et décide quels nouveaux événements émettre.
- Il ajoute les événements à l'event store avec la version attendue du flux. Si une autre commande a ajouté des événements entre-temps, l'ajout échoue et la commande est relancée. C'est du contrôle de concurrence optimiste, qui remplace les verrous de ligne.
- Des projecteurs s'abonnent au flux d'événements et mettent à jour les modèles de lecture : une table pour la liste des commandes, un index de recherche, des compteurs pour un tableau de bord.
- Les requêtes ne lisent que les modèles de lecture.
L'event store
Un event store a besoin de peu de fonctionnalités, mais il lui faut exactement celles-ci :
- Des flux en ajout seul, un par agrégat, avec un numéro de version par événement.
- Un ajout conditionnel sur la version attendue, de façon atomique.
- La lecture d'un flux depuis le début ou depuis une version.
- Des abonnements à tous les nouveaux événements dans l'ordre, avec une position qu'un projecteur peut enregistrer pour reprendre.
Les event stores dédiés fournissent tout cela, tout comme une table relationnelle avec une contrainte d'unicité sur l'ID de flux et la version, plus une outbox ou un change feed pour les abonnements.
Modèles de lecture et cohérence à terme
Les projections s'exécutent après l'enregistrement de l'événement, donc les modèles de lecture ont un retard de quelques millisecondes ou, en cas d'arriéré, bien plus. Concevez en conséquence :
- Après une commande, renvoyez la nouvelle version et laissez le client attendre que le modèle de lecture ait rattrapé son retard, ou affichez le résultat à partir de la réponse de la commande.
- Donnez à chaque projecteur sa propre position, pour qu'un index de recherche lent ne retarde pas la liste des commandes.
- Rendez les projections idempotentes. Les projecteurs redémarrent et relisent des événements ; appliquer OrderShipped deux fois ne doit pas compter deux expéditions.
La contrepartie, c'est que les modèles de lecture sont jetables. Un nouvel écran demande une nouvelle forme : écrivez un projecteur et rejouez l'historique dedans. Un bug de projection : corrigez-le, supprimez le modèle de lecture et reconstruisez-le.
Un exemple concret : un nouveau rapport
Un mois après le lancement, la finance demande le chiffre d'affaires par catégorie de produit et par jour, historique compris. Dans un système CRUD, vous écririez une migration et un script de backfill, et l'historique antérieur au changement aurait peut-être disparu. Avec l'event sourcing, vous écrivez un projecteur qui écoute OrderPlaced et OrderRefunded, vous le placez au début du journal d'événements et vous le laissez rejouer. Une fois à jour, le rapport couvre toutes les commandes depuis le lancement, et il se tient à jour tout seul ensuite.
Le diagramme d'architecture
Contenu du diagramme
- User
- Command side
- Event store
- Event stream
- Projectors
- Read model: order list
- Read model: search index
- Read model: daily totals
- Query side
- PlaceOrder
- append
- subscription
- GET /orders
Comment le lire :
- Rangée du haut, le chemin d'écriture : la commande PlaceOrder de l'utilisateur va au côté commande, qui ajoute des événements à l'event store. Un abonnement alimente le flux d'événements.
- À droite : les projecteurs consomment le flux et tiennent à jour trois modèles de lecture, chacun dans le stockage adapté à ses requêtes.
- En bas à gauche, le chemin de lecture : les requêtes vont au côté requête, qui ne lit que les modèles de lecture et ne contient aucune règle métier.
- Le bloc Markdown montre un flux avec ses versions, ce qui rend la vérification de la version attendue facile à expliquer.
Les coûts
- L'évolution du schéma ne s'arrête jamais. Les événements sont stockés pour toujours, donc les anciennes versions doivent rester lisibles. Prévoyez l'upcasting (convertir les anciens événements au format actuel à la lecture) dès le premier jour.
- Supprimer des données est difficile. Les réglementations qui exigent l'effacement entrent en conflit avec des journaux immuables ; gardez les données personnelles hors des événements, ou chiffrez-les par personne et supprimez la clé.
- Les longs flux deviennent lents à charger. Des snapshots de l'état de l'agrégat toutes les quelques centaines d'événements règlent le problème.
- Le débogage se déplace. Les bugs apparaissent comme des modèles de lecture faux, et la correction est un rejeu, qui demande de l'outillage.
- Les compétences de l'équipe. La modélisation par événements est une autre façon de penser. Un seul domaine cœur en event sourcing suffit largement ; le reste du système peut rester en CRUD.
Ce qu'il faut mettre sur le diagramme
- Deux chemins clairement séparés pour les commandes et les requêtes.
- L'event store comme source de vérité, et les modèles de lecture dessinés comme dérivés.
- Chaque projecteur et son modèle de lecture, pour que le lecteur voie quelles vues peuvent prendre du retard.
- Un flux d'exemple, avec ses versions, pour ancrer la discussion.
Les mécanismes des événements et des consommateurs sont traités dans l'article sur l'architecture événementielle, et le pattern transactional outbox explique comment publier des événements de façon fiable quand vous n'utilisez pas d'event store dédié. Le diagramme d'architecture Kafka couvre le flux lui-même.
Le construire dans Archboard
- Connectez-vous avec Google ou GitHub et créez un diagramme.
- Ajoutez une Personne et des nœuds Conteneur du pack Modèle C4 pour le côté commande, les projecteurs et le côté requête.
- Ajoutez une Base de données pour l'event store et des nœuds Base de données pour les modèles de lecture, ainsi que Kafka et Redis du pack Données pour le flux d'événements et les compteurs.
- Dessinez le chemin d'écriture en haut et le chemin de lecture en dessous, avec les projecteurs qui alimentent les modèles de lecture depuis la droite.
- Ajoutez un bloc Markdown du pack Base avec un exemple de flux d'événements.
L'exemple utilise 10 nœuds de diagramme et reste dans les limites de l'offre Free.
FAQ
CQRS et event sourcing doivent-ils être utilisés ensemble ? Non. CQRS fonctionne avec une base de données classique, et l'event sourcing peut alimenter un seul modèle de lecture. L'event sourcing sans aucune forme de CQRS est rare, car les requêtes sur des flux rejoués sont lentes.
Quand ne pas utiliser CQRS ? Quand lectures et écritures utilisent la même forme de données, ce qui est le cas de la plupart des écrans CRUD. Appliquez-le aux parties du système où elles divergent.
Comment les modèles de lecture restent-ils cohérents avec l'event store ? À terme. Les projecteurs traitent les événements dans l'ordre et enregistrent leur position ; après une panne, ils reprennent à partir d'elle. Les clients doivent tolérer un court délai.
Comment modifier plus tard la structure d'un événement ? Versionnez les événements et convertissez les anciennes versions à la lecture (upcasting). Ne modifiez jamais des événements stockés en place.
Qu'est-ce qu'un snapshot en event sourcing ? Une copie enregistrée de l'état d'un agrégat à une version donnée : pour le charger, on lit le snapshot puis les événements suivants, au lieu de tout le flux.