CQRS et Event sourcing
Ces deux notions sont presque toujours présentées ensemble, et c’est la première chose à défaire. Ce sont deux décisions indépendantes, aux profils de coût radicalement différents : CQRS est bon marché et souvent déjà à moitié fait sans qu’on l’ait nommé ; l’event sourcing est l’une des décisions les plus chères de tout le catalogue, et elle est rarement justifiée.
| Sans event sourcing | Avec event sourcing | |
|---|---|---|
| Sans CQRS | Le cas ordinaire, et souvent le bon | Presque impraticable : plus aucune requête possible |
| Avec CQRS | Le cas utile, et de très loin le plus fréquent | Réservé aux domaines dont l’historique est le produit |
La seule dépendance réelle va dans un sens que l’on énonce rarement : l’event sourcing impose CQRS, puisqu’un flux d’événements ne se requête pas. L’inverse est faux — CQRS n’a aucun besoin d’event sourcing, et c’est la combinaison qu’il faut viser par défaut.
CQRS
D’où vient le besoin
Le problème est déjà posé, sans être nommé, dans trois pages précédentes.
Un agrégat est conçu pour écrire en respectant des invariants. Un dépôt dégénère en DAO à quarante méthodes dès qu’on lui demande de servir des écrans. Et l’hexagone n’a pas de bonne réponse pour une liste paginée, triée, filtrée sur six critères.
Ces trois symptômes ont une seule cause :
Un même modèle ne peut pas être simultanément optimal pour décider et pour afficher.
Décider demande des invariants, une frontière transactionnelle, un objet cohérent et petit. Afficher demande des jointures, de la dénormalisation, de la pagination, et exactement les champs de l’écran — ni plus, ni moins. Ce sont des exigences opposées, et vouloir les satisfaire avec le même modèle produit soit un agrégat obèse, soit un dépôt illisible.
Le principe
Command Query Responsibility Segregation — la formule est de Greg Young, dérivée du Command-Query Separation de Bertrand Meyer. Chez Meyer, la règle porte sur la méthode : une méthode change l’état ou renvoie une donnée, jamais les deux. CQRS hisse la même règle du niveau de la méthode à celui du modèle.
| Modèle d’écriture | Modèle de lecture | |
|---|---|---|
| Contient | Agrégats, invariants, règles | Des projections façonnées comme l’écran |
| Optimisé pour | Décider | Afficher |
| Accès | Par la racine d’agrégat | Directement, en lecture seule |
| Transaction | Oui | Aucune |
| Forme du résultat | Un agrégat | Une structure plate, sans comportement |
Warning
La confusion la plus répandue : CQRS ne veut pas dire « deux bases de données ». C’est la forme maximale, celle qui coûte le plus cher, et elle n’est nécessaire que dans une minorité de cas. La forme minimale, c’est deux chemins de code contre la même base — et elle apporte déjà l’essentiel du bénéfice.
La plupart des équipes qui disent avoir « fait du CQRS » et l’avoir regretté avaient sauté directement à la forme maximale.
Les cinq degrés
CQRS n’est pas binaire. C’est une échelle, et savoir où l’on se place est plus utile que le nom lui-même.
| Degré | Chemin de lecture | Coût | Cohérence |
|---|---|---|---|
| 0 | Le même modèle pour tout | Nul | Immédiate |
| 1 | Requêtes SQL dédiées renvoyant des structures d’affichage, même base | Très faible | Immédiate |
| 2 | Tables ou vues de lecture, mises à jour dans la même transaction | Faible | Immédiate |
| 3 | Projections mises à jour par événements, même base | Moyen | Différée |
| 4 | Base de lecture séparée — moteur de recherche, cache, réplique | Élevé | Différée |
Le degré 1 est presque toujours le bon, et c’est ce que « CQRS » devrait vouloir dire par défaut. Il ne demande aucune infrastructure, aucun événement, aucune cohérence différée : simplement d’admettre qu’une requête d’affichage n’a pas à passer par un agrégat.
Les degrés 3 et 4 franchissent une frontière qualitative — celle de la cohérence différée — et se décident sur un besoin nommé : volume de lecture qui écrase les écritures, recherche plein texte, tableaux de bord agrégés.
Le degré 1, en code
Deux chemins qui ne se ressemblent pas, et c’est le but.
Quatre observations sur ce code, qui sont tout l’intérêt du degré 1.
La requête joint quatre tables — ce qui serait odieux à obtenir par des dépôts d’agrégats, et qui est ici une ligne de SQL lisible.
LigneAgenda n’est pas un objet du domaine : pas de comportement, pas
d’invariant, pas d’identité. C’est une structure de transport, et c’est très bien.
Le chemin de lecture ne passe ni par un dépôt, ni par une entité, ni par une correspondance. En architecture hexagonale, c’est un second port de sortie — un port de requête — assumé comme tel, et non une entorse.
L’agrégat, déchargé de l’affichage, peut rester petit : c’est la règle 2 de la conception d’agrégats.
Ce que CQRS coûte
Deux chemins à maintenir. Ajouter un champ affiché touche l’écriture et la lecture. C’est le coût principal, et il est proportionnel au nombre d’écrans.
La tentation d’écrire par la lecture. Un UPDATE glissé dans le chemin de
requête contourne tous les invariants. C’est une règle à faire respecter
explicitement — le chemin de lecture n’a le droit qu’à des SELECT, et une revue de
code doit le vérifier.
Au-delà du degré 2, la cohérence différée devient visible. C’est un changement de nature, traité juste en dessous.
Le piège de l’utilisateur qui ne voit pas sa propre écriture
Dès le degré 3, un scénario se produit systématiquement et détruit la confiance : l’utilisateur crée quelque chose, est redirigé vers la liste, et ne le voit pas. La projection n’est pas encore à jour. Techniquement correct, humainement inacceptable.
Trois remèdes, du plus simple au plus lourd :
| Remède | Principe | Limite |
|---|---|---|
| Renvoyer le résultat de la commande | La réponse à l’écriture contient l’objet créé ; l’écran l’affiche sans relire | Ne couvre que l’écran qui suit immédiatement |
| Affichage optimiste | L’interface insère la ligne localement en attendant la projection | Divergence visible si l’écriture échoue |
| Lire ses propres écritures | Router les lectures de cet utilisateur vers la source d’écriture pendant quelques secondes | Complexité d’infrastructure réelle |
Le premier suffit dans la grande majorité des cas, et il est presque gratuit. Le choix doit être fait avant de passer au degré 3, jamais après le premier retour d’utilisateur.
Event sourcing
Le principe
L’état n’est pas stocké. Ce qui est stocké, c’est la suite ordonnée et immuable des événements qui l’ont produit. L’état courant est un calcul.
La différence de fond n’est pas technique. Un UPDATE solde = 150 conserve le
quoi et perd le pourquoi ; une suite ArgentDepose, ArgentRetire,
FraisPrélevés conserve l’intention de chaque changement.
Un agrégat événementiel
La structure est toujours la même, et repose sur une distinction qu’il ne faut jamais brouiller : décider peut refuser, appliquer ne refuse jamais.
Warning
La faute qui casse tout : mettre une règle dans appliquer. Un if solde < 0 then throw placé là rendrait impossible le rejeu de l’historique le jour où la règle
change — les événements passés, parfaitement légitimes à leur époque, seraient
refusés. appliquer ne juge pas, il constate. Toutes les règles vivent dans les
méthodes de décision.
Ce que l’event sourcing apporte réellement
Un historique complet et non falsifiable. La piste d’audit n’est pas une
fonctionnalité ajoutée : c’est la donnée elle-même. Aucun UPDATE ne peut effacer
une trace, parce qu’il n’y a pas d’UPDATE.
L’intention, pas seulement l’état. solde = 150 ne dit pas pourquoi.
RemiseCommercialeAccordee, AvoirApplique, FraisPreleves le disent.
La capacité de répondre à des questions non anticipées. C’est le bénéfice le plus sous-estimé : on peut construire aujourd’hui une projection qui répond à une question posée pour la première fois, en rejouant cinq ans de passé. Un stockage classique a jeté l’information.
Le débogage temporel. Reconstituer l’état exact d’un agrégat à l’instant d’un incident, et rejouer la suite.
Ce que l’event sourcing coûte
Cette section est volontairement brutale, parce que la notion est très survendue.
Le versionnement des événements — le vrai problème, et il est permanent. Les événements sont conservés pour toujours. Renommer un champ ou changer une structure signifie que le code doit rester capable de lire la version de 2019, indéfiniment. On y répond par des convertisseurs successifs ou un schéma tolérant, mais la dette ne s’éteint jamais. C’est la difficulté numéro un, et elle n’apparaît qu’au bout de deux ans.
Aucune requête possible. Il n’y a rien à interroger : WHERE statut = 'actif' n’a
pas de sens sur un flux. Toute lecture exige une projection — donc CQRS, obligatoire
et non optionnel.
Les instantanés. Un agrégat à cent mille événements ne peut pas être rejoué intégralement à chaque accès. Il faut des instantanés périodiques, donc leur génération, leur invalidation et leur versionnement.
La suppression est structurellement impossible. Le droit à l’effacement du RGPD se heurte de plein fouet à un journal immuable. La réponse usuelle est le chiffrement par sujet, dont on jette la clé — technique éprouvée, mais qui ajoute une gestion de clés à un système qui n’en voulait pas.
Les erreurs sont permanentes. Un défaut qui a émis de mauvais événements les laisse dans le journal pour toujours. On corrige par des événements de compensation, jamais en réparant le passé.
Les compétences sont rares. Peu de gens en ont fait, l’écosystème est mince, et une équipe qui apprend l’event sourcing sur un projet à échéance produira les six problèmes ci-dessus en même temps.
Quand c’est justifié
La liste est courte, et il faut s’y tenir.
- Une obligation d’audit ou réglementaire où l’historique est une exigence légale et non un confort.
- Un domaine qui est un journal d’événements par nature : comptabilité, grand livre, mouvements de stock, sinistres d’assurance, dossier médical. Le grand livre comptable est un event store vieux de six siècles.
- Un besoin réel de répondre demain à des questions inconnues aujourd’hui, sur le passé.
- Le débogage temporel comme exigence produit, pas comme confort d’équipe.
Warning
Et les raisons qui n’en sont pas.
« On voudra peut-être l’historique plus tard. » Une table d’audit alimentée par les événements de domaine donne 80 % du bénéfice pour 5 % du coût. C’est l’alternative à considérer d’abord, et presque toujours la bonne.
« Ça passe mieux à l’échelle. » Un journal en écriture seule a de bonnes propriétés d’écriture, et un coût de lecture qui les annule.
« Ça va avec les microservices. » Aucun rapport. Voir Monolithe, modulaire, microservices.
La variante raisonnable
Une possibilité que presque personne ne nomme, et qui est pourtant la bonne réponse dans la plupart des cas où l’event sourcing est tentant :
L’appliquer à un seul agrégat.
Rien n’oblige à événementialiser tout un système. Le grand livre, le dossier de sinistre, le portefeuille — l’agrégat dont l’historique est le produit — en event sourcing ; tout le reste en stockage classique. Le coût reste circonscrit, le bénéfice est là où il compte, et les six difficultés ci-dessus ne concernent qu’une partie du modèle.
C’est exactement la logique de dosage par sous-domaine appliquée ailleurs dans cette section : on ne choisit pas une technique pour un système, on la choisit pour un morceau.
Décider
L’ordre des questions, qui évite la quasi-totalité des mauvaises décisions.
- Avez-vous un problème de lecture ? Dépôt qui gonfle, écrans lents, agrégats chargés puis jetés à 95 %. → CQRS degré 1. La réponse est presque toujours oui, et il faut presque toujours s’arrêter là.
- Le volume ou la nature des lectures impose-t-il un stockage dédié ? Recherche plein texte, tableaux de bord agrégés, lectures qui écrasent les écritures. → degré 3 ou 4, en traitant d’abord la question de la cohérence différée visible.
- L’historique est-il une exigence légale, ou le produit lui-même ? → event sourcing sur les agrégats concernés uniquement.
- Sinon → ni l’un ni l’autre au-delà du degré 1.
Les pièges
Croire que CQRS impose deux bases. Le degré 1 se fait dans la même base, en une après-midi.
Faire de l’event sourcing pour avoir un historique. Une table d’audit suffit.
Faire de l’event sourcing sans CQRS. Impossible en pratique : il n’y a plus rien à requêter.
Des projections non reconstructibles. Si l’on ne peut pas supprimer une projection et la rebâtir depuis le début du journal, on a perdu le bénéfice principal de l’event sourcing — et on ne s’en apercevra que le jour où il faudra corriger un défaut de projection.
Des événements techniques dans le journal. CacheInvalide, LigneMiseAJour ne
sont pas des faits métier. Le journal se remplit de bruit qu’il faudra versionner
pour toujours.
Écrire par le chemin de lecture. Un UPDATE dans une requête contourne tous les
invariants.
Des événements conçus comme des structures de transport. Un événement est un fait
métier nommé au passé, pas un sac de champs modifiés. CommandeMiseAJour n’est pas un
événement, c’est un UPDATE déguisé.
Références
- https://martinfowler.com/bliki/CQRS.html
- https://martinfowler.com/eaaDev/EventSourcing.html
- Greg Young, CQRS Documents — https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
- Greg Young, Versioning in an Event Sourced System — https://leanpub.com/esversioning : le seul ouvrage sur la difficulté numéro un
- https://event-driven.io/ — Oskar Dudycz, le blog le plus honnête sur les coûts réels
- Vaughn Vernon, Implementing Domain-Driven Design, chapitre 4 — CQRS dans un contexte DDD