Bibliothèque · Résumé et avis

Understanding Eventsourcing

De Martin Dilger. Ce que je retiens de 650 pages sur l'Event Sourcing, l'Event Modeling et les patterns de lecture.

FR EN
Couverture de Understanding Eventsourcing, Martin Dilger

Understanding Eventsourcing

Understanding Eventsourcing: Planning and Implementing scalable Systems with Eventmodeling and Eventsourcing

7.5 /10

« Exhaustif, honnête sur ses limites Java, et le premier à couvrir Event Sourcing et Event Modeling ensemble de A à Z. »

  • AuteurMartin Dilger
  • VONebulit (auto-édité), 2024 · 650 pages
  • NoteEn anglais uniquement, pas d'édition française
  • Fiche~21 min de lecture
Notation du livre sur 5 dimensionsIdées8/10Applicable6/10Lisibilité7/10Actualité8/10Exemples7/10

De l'étiquette de prix dans une boîte aux patterns de lecture avancés : le guide qui couvre ES et Event Modeling ensemble, galères comprises.

Pourquoi ce livre

L'Event Sourcing et l'Event Modeling sont omniprésents dans les offres d'emploi, les conférences d'architecture et les réunions d'équipe. La plupart des ressources donnent soit une définition de trois lignes, soit cinq cents lignes de code Axon sans contexte. Ce livre, écrit par Martin Dilger, consultant allemand qui accompagne des équipes sur Event Modeling depuis 2021, fait quelque chose de plus rare : il couvre les deux méthodes ensemble, de la modélisation sur tableau blanc aux patterns de lecture en base de données, et il dit franchement ce qui est dur.

L'autre livre de Dilger sur ce site, The little Eventmodeling Book, est un guide Q&A de 72 pages. Understanding Eventsourcing est sa thèse complète : 650 pages, deux préfaces signées Adam Dymitruk (le créateur d'Event Modeling) et Gabriel Schenker (Head of Platform Engineering chez iptiQ). Le code est en Kotlin avec le framework Axon. Les concepts sont portables. La plomberie Java ne l'est pas.

Les idées qui restent

Dix idées à emporter, dans l'ordre de la logique du livre.

1L'étiquette dans la boîte

Les enfants de Dilger vendent leurs jouets dans la rue. Chaque vente : une petite étiquette de prix, rouge pour le fils, jaune pour la fille, glissée dans une boîte à chaussures. En fin de journée, ils vident la boîte, comptent les étiquettes par couleur, et chacun sait ce qu'il a gagné. Ils n'ont jamais mis à jour un tableau ni écrasé un solde. Ils ont naturellement inventé l'Event Store.

C'est l'analogie que Martin Dilger utilise au chapitre 2, et c'est la plus claire que j'aie rencontrée pour expliquer l'Event Sourcing sans jargon. La boîte est l'Event Store : une base où on ne fait qu'ajouter, jamais modifier ni supprimer. Chaque étiquette est un événement. Un événement (le vocabulaire du domaine reste en anglais : event, Event Store, replay) est un fait passé, écrit au passé, à jamais immuable. La règle que pose Dilger : « Whatever happens in the system, you need to record it as an event. If there is no event, it didn't happen. » (p. 40), ce qu'on traduit par « tout ce qui se passe dans le système doit être enregistré comme un événement. Si l'événement n'existe pas, la chose n'a pas eu lieu ».

Pour connaître l'état actuel, on rejoue les événements dans l'ordre (replay). C'est la différence fondamentale avec le CRUD (Create, Read, Update, Delete), la façon classique de gérer les données : en CRUD, un UPDATE écrase l'état précédent et l'ancienne valeur disparaît. En Event Sourcing, on n'écrase jamais, on insère un événement de plus. Un panier e-commerce qui démarre vide, reçoit trois articles, en retire un, se termine avec deux. Les trois mouvements sont conservés.

Deux termes à retenir. Un Event Stream est la séquence chronologique de tous les événements pour une entité précise, un panier ou un client, identifiée par un Stream-Id (l'équivalent d'une clé primaire). Une Projection est une vue construite en rejouant ces événements vers un autre format : une table SQL, un index Elasticsearch, ou même un fichier CSV.

Event Store : flux append-only et replay vers projection EVENT STORE · append-only ClientCréé ArticleAjouté ×3 ArticleRetiré CommandeSoumise PaiementConfirmé replay Projection : état courant = rejouer(tous les événements)
L'Event Store est append-only : on ne fait qu'ajouter des événements, jamais les modifier. Les rejouer dans l'ordre reconstruit l'état courant à n'importe quel instant. La projection est une vue dérivée, toujours regénérable depuis la source de vérité.

24 post-its pour dessiner le système avant d'écrire le code

Adam Dymitruk écrit en préface que la cause principale des échecs logiciels n'est pas technique : c'est l'absence d'un langage commun entre les développeurs et le métier. L'Event Modeling est la réponse. C'est une méthode de planification visuelle fondée sur 4 types de blocs, lisible par tout le monde.

Les 4 patterns :

  • State Change (Command en bleu → Event en orange) : la seule façon de modifier le système. Un utilisateur déclenche une commande, qui produit un événement si les règles métier sont respectées.
  • State View (Event orange → Read Model vert) : la façon de lire les données. Un événement alimente une vue qui peut être consultée.
  • Automation (symbole engrenage) : un processus de fond déclenché par un événement, un timer ou une action utilisateur. Par exemple, envoyer un email de confirmation après une commande.
  • Translation : réception d'un événement externe (API tierce, message Kafka, fichier CSV) et sa conversion en événement ou lecture interne.

Ce que Dilger appelle le "completeness check" (vérification de complétude) est le vrai apport de la méthode. Chaque attribut d'un Read Model doit pouvoir être tracé vers un Event existant dans le modèle. Si un champ "email" dans une vue n'a pas de Event source correspondant, la donnée n'existe tout simplement pas encore dans le système. Cette règle empêche les fausses suppositions sur la disponibilité des données, une des causes les plus fréquentes de retards de projet.

Une anecdote du livre : à Vienne, au printemps 2024, deux personnes débattent avec véhémence d'une même fonctionnalité lors d'un workshop Dymitruk. Il affiche les wireframes. Le désaccord se résout en cinq minutes : elles décrivaient deux écrans différents. Dilger estime que la planification devrait représenter 60 à 70 % de l'effort total d'un projet.

Board Event Modeling — les 4 patterns sur un flux e-commerce ① State Change ② Automation ③ State View ④ Translation Checkout PasserCommande CommandePassée EnvoyerEmail Command EmailEnvoyé CommandePassée CommandesActives Stripe API paiement_confirmé ⇄ adapte PaiementConfirmé StatutPaiement Écran Command Event Read Model Automation Translation
Un flux e-commerce, les 4 patterns d'un coup. De gauche à droite : State Change (écran Checkout → Command PasserCommande → Event CommandePassée), Automation (CommandePassée déclenche le ⚙ engrenage → Command EnvoyerEmail → Event EmailEnvoyé), State View (CommandePassée alimente le Read Model CommandesActives), Translation (webhook Stripe → adaptateur parallélogramme → Event interne PaiementConfirmé → Read Model StatutPaiement). La case orange atténuée en colonne ③ est le même event CommandePassée, référencé par la slice State View.

3CQRS : lire et écrire autrement

Bertrand Meyer, dans les années 1980, formulait le principe CQS (Command-Query Separation, séparation commande-requête) : dans une classe, chaque méthode est soit une commande (elle modifie l'état, ne retourne rien) soit une requête (elle retourne des données, ne modifie rien). Une méthode qui fait les deux est une source de confusion. C'est le niveau du code.

CQRS (Command Query Responsibility Segregation) applique la même idée à l'échelle de l'architecture. On maintient deux modèles distincts : le Write Model (qui gère les commandes et l'écriture des événements) et le Read Model (optimisé pour la lecture, sans les règles métier).

Ce n'est pas la même chose que l'Event Sourcing. Les deux se complètent, mais sont indépendants. Dilger donne un exemple concret tiré de sa propre pratique : une table PostgreSQL customer_registrations propagée vers Elasticsearch via CDC (Change Data Capture, un outil qui détecte les modifications d'une source et les propage automatiquement vers une autre). Pas un événement en vue, et pourtant c'est du CQRS. « You can implement CQRS perfectly fine without using Event Sourcing. » (p. 82)

La cohérence éventuelle est la conséquence naturelle de la séparation. Quand le Write Model et le Read Model se synchronisent de façon asynchrone, la vue peut avoir un léger retard sur la réalité. Dilger cite l'exemple Amazon : achetez un ebook et allez immédiatement sur votre page "Contenus numériques". Le livre n'est pas encore là. Environ trente secondes. C'est de la cohérence éventuelle visible. Ce n'est pas un bug : c'est la façon dont le système fonctionne. La réponse de design : prévoir ce délai dans l'interface, ne pas faire semblant qu'il n'existe pas.

4L'anatomie : qui fait quoi dans le cycle

Un système event-sourcé, c'est quatre composants qui se passent le relais. Le cycle complet (fig. 6.8 du livre) en six étapes :

  1. Une requête arrive. Elle est traduite en une Command : "AssignContactAddressCommand". Une commande est une instruction d'action.
  2. Le Command Handler (gestionnaire de commande) la reçoit. Il valide les règles de structure (champs obligatoires, format du code postal) et délègue à l'Aggregate.
  3. L'Aggregate (agrégat, emprunté au DDD, Domain-Driven Design) est le videur (bouncer) : c'est lui qui décide ce qui a le droit d'entrer dans l'Event Store. Il vérifie les règles métier ("un client ne peut pas avoir deux adresses de livraison identiques"). Si tout est valide, il ordonne l'écriture de l'événement résultant dans l'Event Store. C'est lui qui fait autorité sur la cohérence des données.
  4. L'événement est écrit dans l'Event Store (append-only). C'est la preuve que quelque chose a changé.
  5. Des Projectors (des handlers d'événements) réagissent à ce nouvel événement. Ils le traduisent vers une table SQL, un index de recherche, un cache. Ce sont eux qui alimentent les Read Models.
  6. Pour lire les données, le système émet une query. Le Query Handler sait où aller chercher et dans quel format. Il abstrait la technologie de stockage : changer le backend de lecture ne casse pas les clients.
Anatomie d'un système event-sourcé — cycle en 6 étapes écriture/lecture CHEMIN ÉCRITURE → ← CHEMIN LECTURE UI Client Command Handler valide la structure Aggregate règles métier Event Store append-only → nouvel event Projectors event → lignes / index Read Model SQL / index / cache Query Handler abstrait le stockage
Le Read Model est jetable. Supprime-le, rejoue l'Event Store, il se reconstruit. Le chemin écriture ne change jamais. Chaque nouveau besoin de reporting = un nouveau Projector qui écoute le même flux.
// Aggregate — le bouncer du système (Kotlin ; concept portable à tout langage objet)
class CustomerAggregate {
    fun handle(command: AssignContactAddressCommand) {
        // Vérifie : pas d'adresse dupliquée, maximum 2 adresses par client
        storeEvent(ContactAddressAssigned(...))  // preuve que ça s'est passé
    }
}

5Événements internes vs contrats publics

Un argument séduisant pour l'Event Sourcing : « vous avez accès à tous les événements, donc tous les systèmes peuvent consommer votre stream ». Dilger dit franchement que c'est faux. Consommer les événements internes d'un autre service, c'est techniquement identique à lire directement sa base de données. Les autres équipes deviennent dépendantes de la structure interne de vos événements. Chaque refactoring devient une négociation.

Dilger distingue deux catégories :

  • Domain Events (événements de domaine) : les faits internes qui capturent les changements d'état. Ils appartiennent au domaine et sont libres d'évoluer au rythme des règles métier.
  • Integration Events (ou External Events) : des contrats stables publiés à destination d'autres systèmes. Ils résument les données dans un format versionné et convenu — et les modifier demande de la coordination.

Un exemple du livre : les événements internes du panier e-commerce sont "Item Added", "Item Removed", "Voucher Applied". Un service externe qui les consomme doit comprendre toutes les règles de calcul du panier. Si ces règles changent, tous les consommateurs cassent. La bonne approche : publier un événement "Cart Submitted" qui contient déjà le total calculé dans un format propre. C'est le contrat.

Le versionning des événements suit deux règles :

  • Changement compatible (backward-compatible change) : ne casse pas les consommateurs existants — ajouter un champ optionnel.
  • Changement cassant (breaking change) : les casse — supprimer ou renommer un champ. La stratégie : versionner l'événement (CustomerRegistered_V2) et gérer les deux versions dans le code jusqu'à migration complète.

68 façons de lire les mêmes événements

La Part IV du livre est un des rares endroits en littérature d'architecture qui donne des noms précis à des décisions de design que la plupart des équipes prennent implicitement. Huit patterns de lecture, chacun pour un problème distinct :

PatternProblème résoluCaractéristique clé
Database Projected Read ModelRequêtes complexes, multi-streamsTable SQL dédiée, async, reconstruite par replay
Live ModelConsistance forte sur un stream courtReconstruit from scratch à chaque requête, pas de base secondaire
Synchronous ProjectionProjection async avec un point critique de consistanceBuffer in-memory (LinkedBlockingQueue) comble le délai
Logic Read ModelOù placer calculs et agrégationsLogique dans le Read Model, zéro effet de bord, zéro appel externe
SnapshotsPerformance sur streams longsCache d'état à un instant T : exception, pas règle
Processor-TODO-ListAutomations complexes sans orchestrateur SagaRead Model = liste de tâches ; l'event résultant = cocher la tâche
Reservation PatternUnicité d'une ressource sans transaction ACID distribuéeAggregate dont l'ID = la ressource (email, ticket)
Lookup TablesDonnées descriptives (noms, images) non dupliquées dans les eventsProjection ID → info, locale à chaque slice

Le Processor-TODO-List est la contribution la plus originale de Dilger. Au lieu d'un orchestrateur de Saga avec une définition de processus explicite, un Read Model calcule la liste des tâches en attente. Un scheduler vérifie cette liste, émet une commande par tâche, et l'event résultant la coche. Simple, testable, sans orchestrateur. Dilger résume : « We do not define a specific Process-Orchestrator or Saga-Process-Definition but simply act on the facts in the system. That's it. » (ch. 35), ce qui donne : on n'invente pas d'orchestrateur, on agit juste sur les faits déjà dans le système.

Sur les Snapshots, sa position est délibérée : « You have a problem, and you cache it. Now you have two problems. » La vraie solution est de limiter la longueur des streams via des concepts métier, comme la notion comptable de "clôture mensuelle". Les Snapshots sont un patch de performance, pas un pattern de conception.

7Le prix du passé

L'Event Sourcing conserve l'historique complet. Le RGPD (Règlement général sur la protection des données) exige le droit à l'oubli. La tension est réelle. Mais Dilger la recadre : le problème n'est pas spécifique à l'Event Sourcing. Les logs, les backups et les exports Excel des systèmes CRUD ont exactement le même problème.

La règle numéro un est de design, pas technique : Data Minimalism (minimalisme des données). « The simplest data to handle is the data that doesn't exist. » (p. 551) Ne stocker que ce qui est strictement nécessaire.

Pour le reste, deux techniques :

  • Crypto Shredding (déchiquetage cryptographique) : les données personnelles (PII, Personally Identifiable Information, c'est-à-dire les données permettant d'identifier une personne : nom, adresse, email) sont chiffrées dans les événements avec une clé par utilisateur, stockée dans un service de gestion de clés (AWS KMS, HashiCorp Vault). Pour "oublier" un utilisateur : supprimer sa clé. Les événements restent dans le store, mais deviennent illisibles. Le passé est toujours là, simplement opaque.
  • Forgettable Payload (terme de Mathias Verraes, "charge utile oubliable") : les données personnelles ne sont pas dans les événements. Elles sont stockées dans une table séparée, référencée par un ID dans les événements. Pour oublier : supprimer la ligne dans la table. Les événements restent intacts.

Après suppression, certaines projections doivent être rejouées. L'Event Model est précieux ici : il permet d'identifier précisément quelles projections utilisent ces données et lesquelles doivent être reconstruites.

8Le Processor-TODO-List : des automations sans orchestrateur

Tout système a des workflows automatisés. Après qu'une commande est confirmée, envoyer l'email de confirmation. Après le paiement, générer la facture. Après l'expiration d'une période d'essai, désactiver le compte. La réponse habituelle est une Saga (un pattern qui définit le workflow entier comme une séquence d'étapes explicites, avec des transactions compensatoires si quelque chose échoue) ou un Process Manager (un objet dont le seul rôle est de router les messages entre les services et de savoir quelle commande envoyer après chaque événement). Les deux nécessitent une définition nommée et maintenue du processus lui-même.

Dilger n'utilise ni l'un ni l'autre. Son Processor-TODO-List Pattern part d'une question différente : qu'est-ce que le système sait déjà ? La réponse est dans l'Event Store. Une commande a été confirmée. Un email de confirmation n'a pas encore été envoyé. Un entrepôt n'a pas encore été notifié. Ce ne sont pas des plans futurs : ce sont des faits, déjà exprimables à partir des événements. L'écart entre "ceci s'est passé" et "cela ne s'est pas encore produit" est la liste de tâches. Il suffit de construire un Read Model qui la calcule.

Le mécanisme se déroule en trois étapes qui se répètent. D'abord, les événements arrivent et un Projector écrit des lignes dans le Read Model "TODO List" : une ligne par tâche en attente, avec un statut (en cours ou fait) déduit des événements déjà dans le store. Ensuite, un scheduler, un job de fond qui tourne toutes les quelques secondes, interroge le Read Model pour les lignes en attente. Pour chacune, il émet une Command. Le Command Handler la valide et délègue à l'Aggregate, qui écrit l'événement résultant. Enfin, c'est cet événement que le Projector utilise pour passer la ligne à "fait". La TODO List se met à jour d'elle-même, depuis le même stream qu'elle écoute déjà.

Le résumé de Dilger : « We do not define a specific Process-Orchestrator or Saga-Process-Definition but simply act on the facts in the system. That's it. » (ch. 35) Traduit : on n'invente pas d'orchestrateur, on agit simplement sur les faits déjà dans le système. L'état complet du workflow est implicite dans les événements ; pas de machine d'état, pas de processus orchestrateur à surveiller ni à redémarrer.

Processor-TODO-List : les événements alimentent une liste de tâches, le scheduler la lit et émet des commandes, les événements résultants cochent les tâches ① événements ② TODO List ③ scheduler agit CommandeConfirmée ArticleExpédié projector TODO List — Read Model ⧗ envoyer email de confirmation ⧗ notifier l'entrepôt ✓ générer la facture lit Scheduler (toutes 5s) émet Command EnvoiEmail ConfirmationEmailEnvoyée coche ✓
Le cycle en trois étapes. Les événements alimentent le Read Model "TODO List" (une ligne par tâche en attente). Le Scheduler l'interroge et émet une Command par ligne en attente. L'événement résultant est ce que le Projector utilise pour cocher la ligne, ce qui boucle la boucle.
// TODO List Read Model : concept Kotlin, portable dans n'importe quel langage
// Une ligne par tâche d'automatisation, statut déduit des événements

data class TacheAutomatisation(
    val commandeId: String,
    val tache: String,           // "envoyer_email_confirmation"
    val statut: String           // "en_attente" | "fait"
)

// Le Projector met à jour la table à chaque événement :
// CommandeConfirmée        → INSERT ligne (statut=en_attente)
// ConfirmationEmailEnvoyée → UPDATE ligne (statut=fait)

// Le Scheduler : tourne toutes les 5 secondes
fun traiterEnAttente(taches: List<TacheAutomatisation>) {
    taches.filter { it.statut == "en_attente" }.forEach { tache ->
        commandBus.dispatch(EnvoyerEmailConfirmationCommand(tache.commandeId))
    }
}

Ce que le pattern ne fait pas : le workflow reste implicite. Aucun fichier unique ne dit "si commande confirmée, envoyer l'email, puis notifier l'entrepôt". La logique est répartie entre Projectors et Handlers. Pour des workflows complexes avec des modes d'échec explicites et des transactions compensatoires, une Saga reste l'outil adapté. Dilger préfère manifestement ce pattern, et admet ouvertement ne pas être certain d'utiliser le terme "Saga" correctement, mais le choix dépend de combien l'équipe a besoin de voir le workflow en un seul endroit.

9Le Reservation Pattern : l'unicité sans transaction distribuée

Certaines règles métier exigent l'unicité à l'échelle du système entier. Une adresse email ne peut être enregistrée qu'une seule fois. Un siège de concert ne peut être réservé que par une seule personne. Un code promo ne peut être utilisé qu'une seule fois. Dans une base SQL classique, on l'impose avec un index unique : la base rejette tout doublon au niveau de la colonne. Mais dans un système event-sourcé, où les événements vont dans un store append-only et où les projections sont en cohérence éventuelle, il n'y a pas d'équivalent évident. Deux utilisateurs peuvent simultanément essayer de réserver le même siège, tous les deux lire "disponible" dans la projection (qui peut avoir un léger retard), tous les deux émettre un événement SiegeReservé, et le siège est double-réservé avant que quiconque s'en aperçoive.

Le Reservation Pattern résout ça en faisant de l'ID de l'Aggregate la ressource elle-même. Au lieu d'un aggregate "concert" avec une liste de sièges, on crée un Aggregate dont l'ID est la ressource spécifique : siege-A15-concert-2024-06-20. L'Event Store utilise la concurrence optimiste : chaque écriture spécifie la version de stream attendue. Si deux utilisateurs essaient de créer le même Aggregate simultanément, ils partent tous les deux de la version 0. La première écriture réussit, la version passe à 1. La deuxième écriture, qui attendait aussi la version 0, échoue avec un conflit de version. Le premier arrivé gagne. L'Event Store devient le verrou distribué.

// Reservation Pattern — l'ID de l'Aggregate EST la ressource
// Deux tentatives de réservation simultanées pour le même siège :

// Utilisateur A                   Utilisateur B
// lit le stream : version=0       lit le stream : version=0
// (siège apparaît disponible)     (siège apparaît disponible)
//
// écrit SiegeReservé              écrit SiegeReservé
//   versionAttendue: 0              versionAttendue: 0
//   → OK, version passe à 1         → CONFLIT : attendu 0, trouvé 1
//
// Utilisateur A : siège confirmé  Utilisateur B : réponse "siège indisponible"
//   (événement SiegeReservé écrit)  (exception de verrou optimiste capturée,
//                                    traduite en erreur métier)

Ce qui rend ça propre : pas de commit en deux phases, pas de coordinateur distribué, pas de saga pour gérer le timeout du verrou. La garantie d'unicité vit dans la vérification de version intégrée à l'Event Store, que tout Event Store de production implémente (EventStoreDB, Marten, Axon supportent tous les écritures avec version attendue). Le compromis : chaque ressource unique devient son propre Aggregate, ce qui peut mener à un très grand nombre de streams courts. Pas un problème en pratique, mais à connaître avant d'appliquer à grande échelle.

Dilger étend le pattern au problème de la réservation en deux temps : d'abord bloquer, puis confirmer. Une billetterie peut vouloir retenir un siège deux minutes pendant que l'utilisateur finalise son paiement. Deux commandes : BloquerSiege produit SiegeBloqué (retenu 2 min) ; ConfirmerSiege produit SiegeConfirmé. Le Processor-TODO-List de l'idée 8 gère l'expiration : un scheduler trouve les réservations dont l'événement SiegeBloqué est vieux de plus de deux minutes sans SiegeConfirmé correspondant, et émet une commande LibérerSiege. Deux patterns qui se composent naturellement.

10Tester un système event-sourcé : Given-When-Then

L'Event Sourcing cache un cadeau pour les tests que la plupart des introductions ne mentionnent jamais. Dans un système CRUD, tester la logique métier signifie préparer l'état en base, appeler une méthode, puis requêter la base pour vérifier le résultat. Le test dépend des migrations de schéma, des connexions à la base, des scripts de fixtures. N'importe lequel de ces éléments peut tomber avant même d'atteindre le code qu'on voulait tester. Dilger montre une autre approche : les tests d'Aggregate sont des fonctions pures.

L'Aggregate n'a besoin que de deux choses pour tourner : des événements passés (pour reconstruire son état) et une nouvelle Command. Il produit des Events. Rien d'autre. Pas de connexion à la base, pas d'appel HTTP, pas de dépendance externe. La structure du test reprend exactement la notation des slices d'Event Modeling, Given-When-Then, et se lit comme une spécification :

// Test Given-When-Then pour un Aggregate Panier
// Pas de mock, pas de base de données, pas de contexte Spring

@Test
fun `ajouter un article à un panier soumis est rejeté`() {
    // GIVEN : événements passés qui reconstituent l'état de l'aggregate
    val historique = listOf(
        PanierCréé(panierId = "panier-42"),
        ArticleAjouté(panierId = "panier-42", articleId = "livre-1", qté = 1),
        PanierSoumis(panierId = "panier-42")
    )

    // WHEN : une nouvelle commande est émise
    val aggregate = PanierAggregate.réhydrater(historique)
    val résultat = aggregate.handle(AjouterArticleCommand(panierId = "panier-42", articleId = "livre-2"))

    // THEN : le résultat est une erreur métier, pas un nouvel événement
    assertThat(résultat).isInstanceOf(PanierDéjàSoumisErreur::class.java)
}

@Test
fun `soumettre un panier produit PanierSoumis et vide le flag en attente`() {
    val historique = listOf(
        PanierCréé(panierId = "panier-42"),
        ArticleAjouté(panierId = "panier-42", articleId = "livre-1", qté = 2)
    )
    val aggregate = PanierAggregate.réhydrater(historique)
    val événements = aggregate.handle(SoumettreCommand(panierId = "panier-42"))

    assertThat(événements).containsExactly(
        PanierSoumis(panierId = "panier-42", totalArticles = 2)
    )
}

Chaque test est une histoire autonome. La section Given est une liste explicite de faits passés — l'équivalent d'une fixture de base de données, mais lisible, typée et versionnée avec le test. La section When est la commande à tester. La section Then est la liste des Events produits (ou l'erreur métier levée). Pas d'état caché, pas de setup partagé, pas de nettoyage après. La réhydratation depuis la liste historique est le même replay que le système de production utilise ; le test emprunte exactement le même chemin que le runtime réel.

Dilger montre aussi les tests de Read Model, qui suivent le même pattern mais vérifient l'état de la projection résultante : Given [liste d'événements], Then [voilà ce à quoi ressemble la table du Read Model]. Ces tests vérifient que les Projectors traduisent correctement les événements en lignes optimisées pour la lecture. Ensemble, les deux types de test couvrent complètement une fonctionnalité event-sourcée : le côté écriture (Aggregate + Command Handler) et le côté lecture (Projector + Read Model) — sans base de données vivante ni serveur démarré.

Trois choses que je ne savais pas

Mon avis, honnêtement

Je suis dev web PHP/JS, pas développeur Java/Spring. Ce livre a deux visages. La première moitié (Foundations et Modeling) est parmi ce que j'ai lu de plus clair sur l'Event Sourcing et l'Event Modeling. La métaphore de la boîte de jouets, le completeness check, la distinction events internes vs Integration Events : j'aurais voulu avoir ça il y a cinq ans, quand CQRS revenait dans les réunions sans que je comprenne de quoi on parlait. Le catalogue de patterns de la Part IV est aussi vraiment utile : des noms, des périmètres, des justifications.

La deuxième moitié (Part III, l'implémentation Axon) est une autre planète. Le Kotlin avec les QueryGateway, HandlerEnhancerDefinition et @EventHandler d'Axon : lisible, mais ça suppose un écosystème Spring/Java que la plupart des devs web n'ont pas. Six cent cinquante pages pour les deux publics ensemble, c'est un engagement.

Ce qui voyage : les huit patterns de lecture, la frontière events internes/Integration Events, le Processor-TODO-List, le Reservation Pattern, la structure de test Given-When-Then, le completeness check. Rien de tout ça ne dépend de Java. À lire en parallèle avec Exploring CQRS and Event Sourcing (le guide Microsoft patterns & practices) pour voir les mêmes idées appliquées en situation réelle, puis décider quelle stack convient à votre contexte.

Odilon

Toujours valable en 2026 ?

Oui, plus que le guide Microsoft de 2012. La partie Foundations vieillira lentement ; les patterns et la méthode Event Modeling encore moins. La partie Axon vieillira plus vite, mais les patterns qu'elle illustre sont portables. Un angle s'est ajouté depuis la rédaction : les agents IA consomment aujourd'hui les mêmes event streams que les humains, avec le même besoin de flux bien nommés et auto-descriptifs. Un langage d'événements propre est aussi précieux pour un LLM qui génère du code que pour un développeur qui debug une régression.

Pour qui ?

Lisez-le si

  • Vous rejoignez une équipe qui parle CQRS/ES/Event Modeling et vous n'avez pas encore décrypté le vocabulaire
  • Vous planifiez un système distribué et cherchez une méthode de modélisation collaborative
  • Vous travaillez en Java/Spring et envisagez d'adopter l'Event Sourcing
  • Vous connaissez la théorie ES mais pas les patterns de lecture nommés (les huit de la Part IV)

Passez si

  • Vous cherchez un guide ES agnostique de framework : les exemples sont trop spécifiques à Axon
  • Vous avez besoin d'une couverture complète du versionning d'événements : le livre renvoie au Leanpub de Greg Young
  • 650 pages vous fait peur : les chapitres 1 à 9 (Foundations) se lisent seuls, environ 150 pages

Pour aller plus loin

Pour la vision conceptuelle de l'Event Sourcing appliqué à un projet réel avec les galères incluses, Exploring CQRS and Event Sourcing (Microsoft patterns & practices) suit un vrai système à travers ses itérations successives. Pour l'Event Modeling seul, The little Eventmodeling Book (Dilger aussi) est un guide Q&A de 72 pages centré sur la méthode de planification. Pour le seul problème difficile que ce livre laisse ouvert, le versionning des événements, Versioning in an Event Sourced System de Greg Young (Leanpub, 2017) est le seul traitement dédié.

Commentaires (0)