Event Sourcing et CQRS : le guide pratique pour développeurs

Le comptable arrive à ton bureau. "Ce compte affiche 150 euros. Pourquoi ?" Tu ouvres la base, tu regardes la colonne solde. Elle dit 150. C'est tout ce qu'elle sait dire. Les vingt opérations qui ont mené à ce chiffre ? Écrasées, une par une, à chaque UPDATE. Tu ne peux pas répondre.

C'est le problème que résout l'Event Sourcing. Et CQRS est le compagnon qui le rend utilisable au quotidien. Ces deux mots font peur, on les associe à Kafka, aux microservices et aux architectures de licorne. La réalité est plus simple, et plus utile. Ce guide pose le modèle mental, dit quand ces patterns valent le coup (et quand ils n'en valent pas), puis trace un chemin d'implémentation concret en Go avec PostgreSQL.

Event Sourcing en une phrase

Au lieu de stocker l'état actuel d'une donnée, tu stockes tous les événements qui ont mené à cet état. L'état courant se recalcule en rejouant les événements dans l'ordre.

Un compte bancaire n'est plus une ligne solde = 150. C'est une liste : Crédité(200), Débité(70), Crédité(20). Le solde est le résultat du calcul, pas une valeur stockée. Tu n'as rien perdu, parce que tu n'écrases jamais rien. Tu ne fais qu'ajouter.

// CRUD : on stocke le résultat. L'histoire est écrasée.
account := Account{Balance: 150}

// Event Sourcing : on stocke les faits. Le solde se recalcule.
events := []Event{Credited{200}, Debited{70}, Credited{20}}
balance := replay(events) // 150, reconstruit depuis le début

Un événement est un fait passé, immuable, nommé au passé : OrderPlaced, PaymentConfirmed, AccountDebited. Une fois écrit, il ne bouge plus. C'est une décision qui change tout le reste.

CQRS en une phrase

Tu sépares les opérations d'écriture (les commands, qui modifient l'état) des opérations de lecture (les queries, qui lisent l'état). Deux chemins, deux modèles.

CQRS et Event Sourcing sont deux patterns indépendants. On peut faire du CQRS sans Event Sourcing, et inversement. Mais en production, ils vont presque toujours ensemble : l'écriture produit des événements, et la lecture se sert dans des vues calculées à partir de ces événements. Le modèle d'écriture protège les règles métier, le modèle de lecture est optimisé pour être lu vite.

Les trois gains réels

On n'adopte pas Event Sourcing pour faire moderne. On l'adopte pour trois choses concrètes, et si aucune ne te parle, passe ton chemin.

  • Un journal d'audit gratuit et immuable. Chaque changement est un événement horodaté qu'on n'écrase jamais. En fintech, en santé, partout où un régulateur peut demander "qui a fait quoi, quand", c'est l'argument qui justifie tout.
  • Les requêtes temporelles. "Quel était le solde mardi dernier à 14h ?" devient trivial : tu rejoues les événements jusqu'à cette date. Avec une table CRUD, cette question est impossible à répondre.
  • La reconstruction. Une projection corrompue, un bug dans une vue de lecture ? Tu la jettes et tu la recalcules depuis les événements. La source de vérité est intacte par construction.

Quand ne PAS utiliser Event Sourcing

C'est la section que les tutoriels oublient. Event Sourcing a un coût réel : plus de code, un modèle mental différent, une équipe à former. Dans la majorité des applications, un bon vieux CRUD avec une table d'audit suffit largement.

Évite Event Sourcing si ton domaine est un simple formulaire qui sauve des lignes, si l'historique ne t'intéresse pas, si personne ne te demandera jamais "et avant ?", ou si l'équipe découvre le sujet sur un projet déjà sous pression. Le pattern brille sur les domaines où l'histoire EST la donnée : comptes, paiements, stocks, réservations, workflows métier complexes. Ailleurs, il ajoute de la friction sans payer en retour.

Le modèle mental complet

Trois briques suffisent à tout comprendre. Une fois qu'elles sont claires, le reste n'est que de l'implémentation.

L'aggregate est le gardien de la cohérence métier. C'est lui qui dit "non" quand une opération est invalide (un compte ne descend pas sous zéro, une commande déjà expédiée ne s'annule pas). En Event Sourcing, l'aggregate n'a pas d'état persisté directement : son état est le résultat du replay de tous ses événements depuis le début.

La command est une intention ("débite ce compte de 70"). Elle passe par l'aggregate, qui valide, et qui produit un ou plusieurs événements si c'est permis, ou une erreur si ce ne l'est pas. Jamais les deux.

La projection est une vue calculée depuis les événements. "Le solde de tous les comptes" est une projection. "La liste des commandes en attente" en est une autre. Ce sont elles que lisent tes queries, et on peut les jeter et les recalculer à volonté.

Le chemin d'implémentation, et où creuser

J'ai écrit une série d'articles qui prennent chaque brique en main, avec du vrai code Go testé. Voici l'ordre qui a du sens pour apprendre, du modèle mental à la production.

  1. Le point de départ : comprendre pourquoi l'écriture doit être sérialisée par aggregate alors que la lecture peut être massivement parallèle, dans concurrence et parallélisme en Go appliqués à l'Event Sourcing. C'est aussi l'article qui définit les concepts sans prérequis.
  2. L'aggregate : la fonction Transition() qui rejoue les événements, et le piège du Clone() que Go te fait oublier (les slices partagent leur backing array et corrompent les anciens états en silence), dans CQRS en Go : aggregate, Transition() et Clone().
  3. Le command handler : la signature Handle(ctx, state, cmd) (Events, error) qui rend la logique métier testable sans mock ni base de données, dans des command handlers sans effet de bord.
  4. Le stockage : pourquoi PostgreSQL suffit comme event store, avec une table append-only et l'optimistic locking via UNIQUE(aggregate_id, version), dans PostgreSQL comme event store.
  5. Plusieurs aggregates qui coopèrent : la chorégraphie par événements plutôt qu'un orchestrateur central, et les sagas, dans sagas et chorégraphie par events.
  6. La fiabilité face aux doublons : l'idempotency key pour les retries et les double-clics, puis les quatre couches d'idempotence d'un système CQRS complet, dans les bases de l'idempotence puis commands, projections et outbox.
  7. Le cas avancé : comment renvoyer un résultat synchrone au client HTTP dans un système asynchrone, et garder un audit log cohérent, dans le pubsub bridge et l'audit atomique.

La stack minimale qui suffit

Le plus gros piège d'Event Sourcing, ce n'est pas le concept, c'est la sur-ingénierie. On te vend Kafka, EventStoreDB, un bus de messages distribué et trois brokers avant même d'avoir un seul aggregate qui marche.

Pour 90 % des projets, PostgreSQL seul fait tout le travail. Une table append-only pour les événements, une contrainte UNIQUE pour gérer la concurrence, du polling ou LISTEN/NOTIFY pour les projections. Les snapshots et l'outbox vers un broker n'arrivent que quand un vrai problème de volume ou d'intégration les réclame, pas avant. Commence petit. Tu ajouteras de l'infrastructure le jour où la douleur sera réelle.

Ce qu'il faut retenir

Event Sourcing et CQRS ne sont pas une architecture qu'on adopte en bloc parce que c'est à la mode. Ce sont des outils pour un problème précis : quand l'histoire de tes données vaut autant que leur état actuel. Si un régulateur, un comptable ou un bug de production peut un jour te demander "et avant ?", ils valent leur coût.

Le bon premier pas n'est pas de tout réarchitecturer. C'est de prendre un seul aggregate qui compte (un compte, une commande, un portefeuille), de le modéliser en événements sur une table PostgreSQL, et de voir ce que ça change. Le reste suit, brique par brique.

Questions fréquentes

Quelle est la différence entre Event Sourcing et CQRS ?

Event Sourcing décrit comment tu stockes les données : une suite d'événements immuables plutôt que l'état courant. CQRS décrit comment tu organises le code : un chemin pour écrire (commands), un autre pour lire (queries). Ce sont deux patterns indépendants, mais en production on les combine presque toujours, parce que l'un nourrit l'autre.

Faut-il Kafka ou EventStoreDB pour faire de l'Event Sourcing ?

Non. Pour la grande majorité des projets, PostgreSQL suffit : une table append-only, une contrainte d'unicité pour la concurrence, du polling ou LISTEN/NOTIFY pour les projections. Kafka et les event stores dédiés répondent à des besoins de volume ou d'intégration spécifiques, pas à un prérequis du pattern.

Event Sourcing, n'est-ce pas trop compliqué pour mon projet ?

Souvent, oui. Si ton domaine est un CRUD simple sans besoin d'historique, une table d'audit classique suffit et coûte bien moins cher. Event Sourcing vaut son coût quand l'histoire est la donnée : comptes, paiements, stocks, réservations, ou tout domaine où "qui a fait quoi, quand" est une vraie question.

Par où commencer concrètement ?

Par un seul aggregate qui compte, modélisé en événements sur une table PostgreSQL. Comprends d'abord le replay et l'aggregate, puis le command handler, puis le stockage, puis l'idempotence. La série d'articles liée plus haut suit exactement cet ordre, avec du code Go testé.

Commentaires (0)