← Tous les cours / CQRS & Event Sourcing Cours
00 — DEUX IDÉES, PAS UNE

CQRS et Event Sourcing : deux idées qu'on confond tout le temps

On les cite toujours ensemble, au point de croire que l'un implique l'autre. C'est faux, et cette confusion est la première source de projets qui s'effondrent sous leur propre poids.

Tu as sûrement déjà entendu la formule « on fait du CQRS avec de l'Event Sourcing », lâchée comme si c'était un seul et même choix. C'est le malentendu fondateur, et le dissiper est la première chose utile que ce cours puisse faire pour toi. Ce sont deux idées indépendantes, qu'on peut adopter séparément.

CQRS
Command Query Responsibility Segregation. Séparer le modèle qui écrit (traite les commandes, protège les règles) du modèle qui lit (répond aux affichages). Deux modèles pour deux responsabilités, au lieu d'un seul qui fait tout.
Event Sourcing
Ne pas stocker l'état courant d'un objet, mais la suite des événements qui l'ont amené à cet état. L'état n'est plus une donnée qu'on écrase : c'est un résultat qu'on recalcule en rejouant son histoire.

Les quatre combinaisons existent, et trois d'entre elles sont parfaitement légitimes :

Sans Event SourcingAvec Event Sourcing
Sans CQRSLe CRUD classique. La grande majorité des applications, et c'est très bien.Rare et souvent inconfortable — l'ES pousse presque toujours vers CQRS.
Avec CQRSTrès courant : deux modèles, une base classique. Le point d'entrée idéal.Le duo « complet », puissant mais coûteux, réservé aux domaines qui le méritent.
🔑 L'ordre d'adoption compte. On peut faire du CQRS sans jamais toucher à l'Event Sourcing — et c'est même la porte d'entrée recommandée. L'inverse est bien plus rare : dès qu'on stocke des événements, on sépare presque toujours lecture et écriture. Retiens : ES implique souvent CQRS ; CQRS n'implique jamais ES.

Ce cours prolonge la série DDD. Tu y retrouveras les agrégats (ce qui émet les événements), les domain events (ce qu'on stocke), et le change-tracking par événements qu'on avait déjà entrevu. On file un seul exemple du début à la fin : un compte bancaire, le cas d'école de l'Event Sourcing.

01 — LE PRINCIPE CQRS

CQRS : lire et écrire ne sont pas le même métier

Un même modèle qui sert à la fois à valider une opération et à alimenter trente écrans finit tiraillé entre deux besoins contradictoires. CQRS tranche : on arrête de vouloir un modèle unique.

Reprends le compte bancaire. Quand tu écris — un retrait — ce qui compte, c'est la règle : « pas de découvert au-delà de l'autorisation ». Tu as besoin d'un modèle riche, qui protège cet invariant, qui refuse l'opération illégale. Quand tu lis — afficher le solde, l'historique, un relevé PDF, un graphique de dépenses — tu te fiches des règles : tu veux des données, vite, dans la forme exacte de l'écran.

Ces deux besoins tirent un modèle unique dans des directions opposées. Le modèle d'écriture veut être normalisé, encapsulé, avare en accès. Le modèle de lecture veut être dénormalisé, plat, taillé pour chaque vue. Vouloir les servir tous les deux avec les mêmes classes, c'est le compromis permanent — et médiocre des deux côtés.

L'idée de Greg Young
CQRS naît d'un principe plus ancien de Bertrand Meyer, la Command-Query Separation : une méthode change l'état (commande, ne retourne rien) ou renvoie une donnée (requête, ne change rien), jamais les deux. Greg Young a fait passer cette règle du niveau de la méthode à celui du modèle entier : un modèle pour écrire, un autre pour lire.
Commandes
écrivent — protègent les règles
Requêtes
lisent — servent les écrans

Une fois les deux côtés séparés, chacun peut évoluer selon ses propres contraintes. Le côté écriture reste un agrégat DDD soigné, gardien de ses invariants. Le côté lecture devient une collection de modèles de lecture (read models) : autant de vues dénormalisées que d'écrans, chacune optimisée pour sa requête, sans la moindre logique métier.

💡 Ce que CQRS n'est pas. Ce n'est pas « deux bases de données », ni « de l'asynchrone », ni « des micro-services ». C'est d'abord une séparation de responsabilités dans le code. Elle peut se réaliser sur une seule base, en synchrone, dans un monolithe. Les versions plus lourdes (bases séparées, projections asynchrones) viennent après, et seulement si le besoin les justifie — on y revient en section 03.

C'est exactement le principe qu'on avait esquissé dans le cours repositories & factories : « un repository ne sert pas à lire pour afficher ». Le côté commande passe par les agrégats et leurs repositories ; le côté requête passe par des read models dédiés. CQRS, c'est ce principe, nommé et assumé.

02 — COMMANDES VS REQUÊTES

Deux flux : la commande qui décide, la requête qui renseigne

Concrètement, à quoi ressemblent ces deux côtés dans le code ? À deux chemins qui ne se croisent presque jamais, avec deux vocabulaires distincts.

Le côté écriture s'articule autour de la commande : une intention exprimée par l'utilisateur, formulée à l'impératif. « Retire 20 € du compte. » Une commande n'est pas une donnée, c'est une demande d'action qui peut être refusée si elle viole une règle.

// Une commande : une intention, immuable, nommée à l'impératif.
final readonly class WithdrawMoney
{
    public function __construct(
        public string $accountId,
        public int    $amount,   // en centimes
    ) {}
}

// Son handler : charge l'agrégat, applique la règle, sauvegarde. Ne retourne rien d'utile.
final class WithdrawMoneyHandler
{
    public function __invoke(WithdrawMoney $command): void
    {
        $account = $this->accounts->get(new AccountId($command->accountId));
        $account->withdraw($command->amount);   // l'agrégat protège l'invariant de découvert
        $this->accounts->save($account);
    }
}

Le mot handler ici n'a rien de spécifique à un projet : c'est le vocabulaire officiel des bus de messages, comme Symfony Messenger. Un bus de commandes reçoit la commande et la route vers son unique handler. Rien ne t'oblige à utiliser un bus, cela dit — un simple appel de service fait aussi l'affaire ; le bus apporte surtout un point d'entrée uniforme et des middlewares (transaction, validation, journalisation).

Le côté lecture, lui, s'articule autour de la requête : une question, sans le moindre effet de bord. « Quel est le solde du compte ? » Le handler de requête va chercher dans un read model taillé pour la réponse, et ne touche jamais à un agrégat.

// Une requête : une question. Son handler lit un read model, ne modifie rien.
final class GetAccountBalanceHandler
{
    public function __invoke(GetAccountBalance $query): BalanceView
    {
        return $this->readDb->fetchBalanceView($query->accountId);  // données plates, prêtes à afficher
    }
}
CommandeRequête
Intentionchanger l'étatobtenir une information
Nomimpératif — WithdrawMoneyinterrogatif — GetAccountBalance
Passe parl'agrégat + son repositoryun read model dédié
Peut échouer surune règle métierrien (lecture pure)
Retourrien (ou juste un identifiant)les données de la vue
🔑 Le bénéfice caché : comme les commandes portent une intention nommée (« retirer », « clôturer », « autoriser un découvert »), ton code raconte enfin ce que veut l'utilisateur, au lieu d'un vague update() qui écrase des champs. Ce vocabulaire d'intentions est aussi ce qui rend l'Event Sourcing naturel — une commande acceptée produira un événement du même souffle.
03 — CQRS SANS EVENT SOURCING

CQRS tout seul : trois degrés de séparation

Avant de plonger dans les événements, arrêtons-nous : la plupart des bénéfices de CQRS s'obtiennent sans jamais stocker un seul événement. Il suffit de choisir jusqu'où pousser la séparation.

La grande erreur, c'est de croire qu'il faut « tout ou rien ». CQRS se décline en degrés, du plus léger au plus engageant, et tu peux t'arrêter à celui qui sert ton besoin. Voici les trois paliers, sur une seule base de données classique.

Palier 1 — Séparer les modèles, même base, même schéma
Deux jeux de classes : des agrégats pour écrire, des objets de lecture plats (souvent construits par des requêtes SQL dédiées) pour lire. Une seule base, les mêmes tables. Aucune synchronisation à gérer : la lecture interroge directement les tables d'écriture, mais avec ses propres requêtes optimisées.
Palier 2 — Modèle de lecture dénormalisé, même base
On maintient des tables de lecture séparées, dénormalisées, pré-calculées pour les écrans. Elles sont mises à jour dans la même transaction que l'écriture (donc toujours cohérentes). On gagne en vitesse de lecture, on paie en écritures supplémentaires.
Palier 3 — Read model séparé, mis à jour de façon asynchrone
Les tables de lecture vivent dans un autre stockage (voire une autre techno : Elasticsearch pour la recherche, Redis pour un compteur). Elles sont alimentées après coup, par un flux d'événements. C'est là qu'apparaît la cohérence eventual — et le vrai coût de CQRS.
Palier 1
requêtes dédiées
Palier 2
tables de lecture, sync
Palier 3
read model, async

Le point crucial : jusqu'au palier 2, tout reste synchrone et fortement cohérent. Une lecture juste après une écriture voit la nouvelle valeur. On profite de la clarté de CQRS sans le casse-tête de la cohérence différée. Une écrasante majorité d'applications n'a jamais besoin d'aller au-delà.

💡 Ne saute pas les paliers. Beaucoup d'équipes attaquent directement au palier 3 « parce que c'est ça, le vrai CQRS », et découvrent la cohérence eventual comme un mur. Commence au palier 1 ou 2. Tu ne montes au palier 3 que quand un besoin réel l'exige : des lectures massivement plus nombreuses que les écritures, une techno de lecture différente, une mise à l'échelle indépendante.

Tout ce qu'on vient de voir tient sans Event Sourcing. Le read model du palier 3 peut être alimenté par de simples domain events émis à l'écriture, sans jamais faire de l'état stocké un log d'événements. C'est maintenant, seulement, qu'on ouvre l'autre grande idée : et si on ne stockait que les événements ?

04 — LE PRINCIPE DE L'EVENT SOURCING

Event Sourcing : stocker l'histoire, pas le solde

Ta banque ne stocke pas « tu as 340 € » et rien d'autre. Elle stocke chaque opération, et le solde n'est que leur somme. C'est exactement ça, l'Event Sourcing.

Ouvre ton relevé bancaire. Que vois-tu ? Pas une ligne unique « solde : 340 € », mais une suite d'opérations : dépôt de 500, retrait de 60, paiement de 100, retrait de 20… Le solde, lui, n'est écrit nulle part comme une vérité première — il se déduit de la somme des opérations. Ta banque fait de l'Event Sourcing depuis des siècles.

Event Sourcing
La source de vérité n'est pas l'état courant, mais un journal d'événements immuable et append-only. On n'écrase jamais une donnée : on ajoute un nouvel événement. L'état courant est un calcul dérivé, obtenu en rejouant les événements depuis le début.

Compare les deux approches sur notre compte. En stockage classique, une colonne balance qu'on écrase à chaque opération. En Event Sourcing, une table d'événements où l'on ne fait qu'ajouter :

// Chaque événement est un fait passé, immuable, nommé au passé composé.
MoneyDeposited  { amount: 500 }
MoneyWithdrawn  { amount:  60 }
MoneyWithdrawn  { amount: 100 }
// solde = 500 - 60 - 100 = 340  (calculé, jamais stocké tel quel)

Ce simple renversement change tout. Tu ne perds plus jamais d'information : le « pourquoi » du solde est là, gravé. Tu peux répondre à des questions qu'on ne t'avait pas encore posées (« combien de retraits en mars ? »), auditer sans effort, et même remonter le temps pour connaître l'état à n'importe quelle date. Un état stocké classiquement, lui, a oublié tout ce qui l'a précédé.

🔑 Les événements sont des faits, pas des ordres. Une commande (WithdrawMoney) est une demande qui peut être refusée. Un événement (MoneyWithdrawn) est ce qui s'est réellement produit — au passé, immuable, non négociable. On ne « supprime » jamais un événement du journal ; pour annuler, on ajoute un événement compensatoire. C'est un domain event promu au rang de source de vérité.

Le simulateur ci-dessous te fait vivre la différence. Choisis un mode de stockage, effectue des opérations, puis pose la question qui tue : « d'où vient ce solde ? ». Un seul des deux modes saura répondre.

Un compte, deux façons de le stocker. Effectue des opérations, puis clique sur « Auditer » et « Rejouer ». En stockage d'état, l'histoire est perdue ; en Event Sourcing, tout est reconstituable.

Solde : 0 €
journal :
05 — REJOUER L'ÉTAT

Reconstruire l'agrégat en rejouant son histoire

Si l'état n'est plus stocké, comment charge-t-on un compte pour lui appliquer une nouvelle opération ? On le fait renaître de ses événements — un pliage de son histoire.

Le cours repositories & factories distinguait créer un objet et le reconstituer depuis la persistance. L'Event Sourcing pousse la reconstitution à sa forme la plus pure : on ne recharge pas un état figé, on rejoue les événements pour le reconstruire.

Rehydration (rejeu)
Pour charger un agrégat, le repository lit tous ses événements dans l'ordre et les applique un à un à un objet vierge. Chaque événement fait avancer l'état d'un cran. À la fin du journal, l'agrégat est dans son état courant, prêt à recevoir une nouvelle commande.

Concrètement, l'agrégat sait appliquer chaque type d'événement à son propre état — une méthode par événement, qui ne fait que muter des champs, sans aucune règle. Les règles, elles, vivent dans les méthodes de commande.

final class Account
{
    private int $balance = 0;

    // Reconstitution : on plie la liste d'événements sur un objet neuf.
    public static function replay(array $events): self
    {
        $account = new self();
        foreach ($events as $event) {
            $account->apply($event);   // aucune règle ici : juste avancer l'état
        }
        return $account;
    }

    private function apply(object $e): void
    {
        match ($e::class) {
            MoneyDeposited::class => $this->balance += $e->amount,
            MoneyWithdrawn::class => $this->balance -= $e->amount,
        };
    }

    // Une commande : ici vivent les RÈGLES. Elle ne mute pas directement — elle émet un événement.
    public function withdraw(int $amount): void
    {
        if ($amount > $this->balance) {
            throw new InsufficientFundsException($this->balance, $amount);
        }
        $this->recordThat(new MoneyWithdrawn($amount));  // l'événement, puis apply()
    }
}

Note la séparation nette, propre à l'Event Sourcing : la méthode de commande withdraw() décide (elle vérifie la règle et refuse le découvert), tandis que la méthode apply() enregistre le fait (elle avance l'état, sans jamais juger). Une commande acceptée produit un événement ; cet événement est appliqué à l'état, puis ajouté au journal au moment du save.

💡 C'est le change-tracking par événements, en vrai. On avait vu dans repositories & factories que, sans ORM, on pouvait suivre les changements par des événements. L'Event Sourcing est cette idée poussée jusqu'au bout : le journal d'événements n'est plus un moyen de savoir quoi sauvegarder — il est la donnée sauvegardée.

Une inquiétude légitime surgit ici : rejouer tous les événements à chaque chargement, pour un compte ouvert depuis dix ans, ça fait beaucoup. C'est vrai, et c'est le problème que les snapshots résoudront (section 08). Mais d'abord, réglons la question du côté lecture : comment ces événements alimentent-ils les écrans ?

06 — LES PROJECTIONS

Les projections : des événements aux écrans

Rejouer l'histoire à chaque affichage serait absurde. On pré-calcule des vues en écoutant le flux d'événements. Mais ces vues arrivent avec un léger retard — et ce retard a un nom.

On ne va évidemment pas rejouer dix ans d'opérations chaque fois qu'un client rafraîchit sa page de solde. Le côté lecture de CQRS reprend ici tout son sens : on maintient des projections, des read models pré-calculés qui écoutent le flux d'événements et se tiennent prêts à répondre instantanément.

Projection
Un processus qui consomme les événements au fil de l'eau et met à jour un modèle de lecture dénormalisé. À chaque MoneyDeposited ou MoneyWithdrawn, la projection « solde » ajuste une simple ligne account_id → balance. La requête d'affichage lit cette ligne, sans jamais toucher au journal.

La beauté, c'est qu'un même journal peut nourrir autant de projections que tu veux, chacune taillée pour un besoin : une pour le solde, une pour l'historique paginé, une pour un tableau de dépenses par catégorie, une pour un moteur de recherche. Elles sont toutes dérivées de la même source de vérité, sans jamais la modifier.

Journal d'événements
source de vérité
Projection solde
read model
Projection historique
read model

Mais dès que la projection tourne après l'écriture — de façon asynchrone, dans un worker — un décalage apparaît. Entre le moment où l'événement est écrit dans le journal et le moment où la projection l'a traité, le read model est en retard. C'est la fameuse cohérence eventual.

Cohérence eventual (eventual consistency)
Le read model n'est pas immédiatement à jour après une écriture ; il le devient au bout d'un moment, une fois la projection traitée. Entre les deux, une lecture peut renvoyer une valeur périmée. Ce n'est pas un bug — c'est le contrat qu'on a signé en rendant les projections asynchrones.

Le piège classique, c'est le « read-your-writes » : l'utilisateur fait un dépôt, l'écran se rafraîchit aussitôt, et affiche encore l'ancien solde parce que la projection n'a pas rattrapé. Le simulateur ci-dessous te fait sentir ce décalage — et pourquoi il faut le concevoir, pas le subir.

Une commande écrit tout de suite ; la projection suit avec un temps de retard. Dépose de l'argent, lis le solde affiché avant d'avoir traité la projection, et regarde les deux côtés diverger puis se rejoindre.

côté écriture (journal)

solde = 0 €

côté lecture (projection)

solde affiché = 0 €
événements en attente de projection : 0
cohérent
07 — REJOUER POUR DE NOUVELLES VUES

Le superpouvoir : reconstruire et inventer des vues rétroactivement

Comme tout est dans le journal, tu peux jeter une projection et la reconstruire à l'identique. Mieux : tu peux créer aujourd'hui une vue qui répond à une question d'hier.

Voici le bénéfice qui, à lui seul, justifie parfois l'Event Sourcing. Puisque le journal d'événements est la source de vérité et que les projections n'en sont que des dérivés, une projection devient jetable et reconstructible. Un read model corrompu ? On le vide et on rejoue le journal. Il repart neuf, exact.

Reconstruire une projection
Une projection n'est jamais une donnée précieuse : c'est un cache d'une forme particulière. On peut la supprimer et la régénérer à tout moment en rejouant les événements. Un bug dans la logique de projection ? On corrige le code, on rejoue depuis le début, et le read model est réparé rétroactivement.

Mais le vrai coup de génie va plus loin. Le jour où le métier te demande une statistique à laquelle personne n'avait pensé — « combien de retraits par mois depuis l'ouverture ? » — tu n'es pas condamné à ne compter qu'à partir d'aujourd'hui. Tu écris une nouvelle projection, tu rejoues tout l'historique, et la voilà remplie comme si elle avait toujours existé.

Projections rétroactives
Une nouvelle vue, créée bien après les faits, peut être remplie à partir du passé complet. L'Event Sourcing conserve des données que tu n'avais pas encore décidé d'exploiter. Un stockage d'état classique, lui, n'a gardé que ce qu'il jugeait utile sur le moment — le reste est perdu à jamais.

Le simulateur illustre cet écart de manière frappante. On a un historique d'opérations. Le métier réclame une nouvelle stat aujourd'hui. Selon le mode de stockage, tu peux remonter le temps — ou tu pars de zéro.

Un compte a déjà vécu : dépôts et retraits passés. On te demande aujourd'hui une nouvelle vue : « nombre total de retraits ». Construis-la, et vois si elle voit le passé ou seulement l'avenir.

Historique déjà survenu :
Nb de retraits calculé :
🔑 La contrepartie, honnête : ce superpouvoir suppose que tu ne jettes jamais un événement, et que tes vieux événements restent lisibles des années plus tard. C'est précisément ce qui rend le versioning des événements et le RGPD délicats — on y vient en section 09. Le passé qu'on garde est une richesse et une dette.
08 — SNAPSHOTS & PERFORMANCE

Snapshots : quand rejouer dix ans devient trop long

Reconstruire un agrégat en rejouant chaque événement est élégant — jusqu'à ce que le journal compte des dizaines de milliers d'entrées. Le remède est simple : une photo, prise de temps en temps.

Le rejeu intégral (section 05) a une limite évidente. Un agrégat très actif — un compte de trading, un panier géant, un jeu — peut accumuler des dizaines de milliers d'événements. Les rejouer tous à chaque chargement finit par coûter cher. La réponse canonique porte un nom photographique.

Snapshot
Une photographie de l'état de l'agrégat à un instant donné, avec le numéro du dernier événement qu'elle intègre. Pour recharger, on part du snapshot le plus récent et on ne rejoue que les événements postérieurs. On passe de « rejouer 40 000 événements » à « charger un snapshot + rejouer les 12 derniers ».
Snapshot
état au n°39 988
+
12 événements
n°39 989 → 40 000
=
État courant
n°40 000

Le point essentiel — et souvent mal compris — c'est qu'un snapshot est une optimisation, pas une source de vérité. Le journal reste la vérité ; le snapshot n'est qu'un raccourci qu'on peut jeter et régénérer. Si un snapshot est corrompu ou si la logique change, on le supprime et on rejoue depuis le journal. On ne « répare » jamais un snapshot à la main.

💡 Ne snapshotte pas trop tôt. Les snapshots ajoutent de la complexité (quand les prendre ? tous les combien ? où les stocker ?). La plupart des agrégats ont des journaux courts — quelques dizaines d'événements — et se rejouent en une fraction de milliseconde. Mesure d'abord. Le snapshot est la réponse à un problème de performance constaté, pas une case à cocher par principe.

Au-delà des snapshots, la performance de l'Event Sourcing repose sur une asymétrie confortable : les écritures sont des ajouts (append-only), l'opération la plus rapide qu'une base sache faire — pas de verrou de mise à jour, pas de contention sur des lignes existantes. Et les lectures passent par les projections, déjà pré-calculées. Chaque côté est optimisé pour ce qu'il fait, ce qui nous ramène, encore une fois, à la séparation CQRS.

Reste une garantie à assurer côté écriture : deux commandes concurrentes sur le même agrégat ne doivent pas produire deux séries d'événements incohérentes. On s'appuie pour cela sur le verrou optimiste par version — exactement le mécanisme vu dans les cours agrégats et transactions SQL : on n'ajoute des événements que si la version attendue du flux n'a pas bougé entre-temps.

09 — PIÈGES & LIMITES

Les pièges : ce dont personne ne te prévient

L'Event Sourcing brille dans les slides. En production, il présente une facture bien réelle : cohérence différée, événements qu'on ne peut plus modifier, et un droit à l'oubli qui se heurte à un journal immuable.

Tout ce qui précède est séduisant. Il serait malhonnête de s'arrêter là : l'Event Sourcing a des coûts spécifiques, et les ignorer mène à des projets qui regrettent amèrement leur choix. Voici les quatre murs contre lesquels on se cogne.

1 · La cohérence eventual n'est pas gratuite
Dès que les projections sont asynchrones (section 06), tes utilisateurs peuvent voir des données périmées, et ton interface doit le gérer : masquer le décalage, afficher un état « en cours », ou forcer une lecture cohérente sur les écrans critiques. Ce n'est pas un détail technique — c'est une contrainte de conception qui remonte jusqu'à l'UX.
2 · Les événements sont immuables… donc leur schéma se fige
Un événement MoneyWithdrawn écrit il y a trois ans est encore dans ton journal, et tu devras toujours savoir le lire. Le jour où tu veux changer sa structure (renommer un champ, en ajouter un obligatoire), tu ne peux pas réécrire le passé. Il faut versionner les événements et upcaster : traduire à la volée les vieux événements vers le nouveau format à la lecture.
3 · Le droit à l'oubli contre un journal append-only
Le RGPD impose de pouvoir effacer les données personnelles d'un individu. Mais un journal d'Event Sourcing est, par principe, immuable et permanent. Les deux exigences se heurtent de plein fouet. Les parades existent (chiffrement par personne dont on jette la clé — crypto-shredding —, ou externalisation des données personnelles hors du journal), mais elles doivent être pensées dès le départ, pas ajoutées après coup.
4 · La courbe d'apprentissage et l'outillage
Déboguer un état qui n'existe nulle part directement, tester des projections, gérer un event store, former une équipe entière à ce renversement mental : tout cela a un coût humain réel. L'Event Sourcing demande une maturité d'équipe qu'on sous-estime presque toujours.
🚨 Quand NE PAS faire d'Event Sourcing. Si ton domaine est un CRUD sans histoire à raconter, si personne ne demande d'audit ni de « pourquoi », si l'état courant suffit amplement : n'en fais pas. Tu paierais tous les coûts ci-dessus sans toucher aucun des bénéfices. L'Event Sourcing se mérite par une vraie valeur métier de l'historique — la finance, la logistique, le médical, la collaboration — pas par goût de la technique.

La bonne nouvelle, c'est que ces pièges sont surtout ceux de l'Event Sourcing. CQRS seul — jusqu'au palier 2, synchrone — n'en subit presque aucun. C'est une raison de plus d'adopter les deux idées séparément, et dans le bon ordre.

10 — SYNTHÈSE

Synthèse : séparer, puis peut-être sourcer

Deux idées, un ordre d'adoption, et une question à te poser honnêtement avant de te lancer : mon domaine mérite-t-il vraiment tout ça ?

Récapitulons le cheminement. CQRS sépare le modèle qui écrit du modèle qui lit — une idée simple, applicable graduellement, dont l'essentiel des bénéfices s'obtient sans jamais quitter le confort de la cohérence forte. L'Event Sourcing, lui, remplace l'état stocké par un journal d'événements dont l'état n'est qu'un dérivé — une idée puissante, qui débloque l'audit, l'historique et les projections rétroactives, au prix d'une vraie complexité.

La règle d'or de l'adoption
Adopte CQRS avant l'Event Sourcing, et par paliers. La séparation lecture/écriture rend service presque partout ; l'Event Sourcing ne se justifie que quand l'histoire elle-même a de la valeur métier. On peut faire l'un sans l'autre — et le plus souvent, on devrait.

L'arbre de décision

Ta situationLa réponse raisonnable
CRUD simple, pas de règles subtilesNi CQRS ni ES. Un modèle unique suffit.
Lectures complexes qui polluent ton modèle d'écritureCQRS palier 1 ou 2 (synchrone).
Lectures massives, techno de lecture dédiée, mise à l'échelleCQRS palier 3 (read model asynchrone).
L'audit, l'historique, le « pourquoi » ont une vraie valeur métierEvent Sourcing — et donc CQRS avec.
« Ça a l'air cool sur les conférences »Surtout pas. Reviens aux lignes du dessus.
🔑 Le fil conducteur : une commande exprime une intention et peut être refusée ; un événement est un fait immuable et non négociable ; une requête ne fait que lire une vue dérivée. Sépare ces trois flux, et ton architecture cesse de se battre contre elle-même. Ajoute les événements comme source de vérité seulement si le passé, chez toi, vaut de l'or.

Où ça se relie

💡 Bibliographie : Greg Young (CQRS, Event Sourcing — articles et conférences fondateurs) ; Bertrand Meyer, Object-Oriented Software Construction (Command-Query Separation) ; Vaughn Vernon, Implementing DDD (chapitres CQRS & Event Sourcing) ; Martin Fowler (articles « CQRS », « Event Sourcing », « Reporting Database ») ; documentation EventStoreDB et Axon pour l'outillage.