Une appli web qui garde chaque fait dans PostgreSQL et dont chaque morceau ne parle aux autres que par messages.
Pourquoi ce livre
Écrire un logiciel est facile, le modifier l'est beaucoup moins. Ethan Garofolo ouvre Practical Microservices (Pragmatic Bookshelf, 2020) sur ce constat, et il désigne un coupable. Le CRUD (créer, lire, modifier, supprimer des lignes) est le geste de base du web. Le MVC (modèle, vue, contrôleur) est le découpage de Rails, Laravel ou Symfony qui l'organise. Pour l'auteur, ce couple pousse vers le monolithe, une application d'un seul bloc où tout dépend de tout : « les frameworks MVC CRUD vous mènent à des architectures monolithiques, qui optimisent l'écriture » (p. 25). Ces résultats rapides se paient en couplage : renommer une colonne casse ce qui la lit ailleurs (p. 25).
Sa réponse tient en une phrase. On garde chaque fait au lieu de l'écraser. Puis on découpe le système en petits programmes qui ne se parlent que par messages, avec PostgreSQL pour seule infrastructure. Garder la suite des faits plutôt que l'état final s'appelle l'Event Sourcing. Écrire les données sous une forme et les lire sous une autre s'appelle CQRS. Le livre construit tout cela à la main, sur une seule application qui grandit page après page : Video Tutorials, un site de vidéos écrit en Node.js.
Je suis développeur dans la fintech crypto, en Go, avec de l'Event Sourcing et du CQRS. Ma question de lecture était simple : que m'apprend un livre qui bâtit ces briques sur la base de données que tout développeur web a déjà ? Si vous avez lu la fiche d'Implementing DDD, CQRS and Event Sourcing (Alex Lawrence), certaines briques vous seront familières. Son titre parle de DDD, pour Domain-Driven Design, la conception pilotée par le domaine. Cette fiche garde ce que Garofolo fait autrement.
Les douze idées se rangent en trois familles :
- Pourquoi quitter le CRUD (idées 1 et 2) : qu'est-ce qui coince quand tout le monde partage la même table, même découpée en services ?
- Les pièces, sur PostgreSQL (idées 3 à 6) : où ranger les faits, comment en tirer des écrans, comment les faire circuler, comment décider ?
- Le prix de l'asynchrone (idées 7 à 12) : que faire quand un message arrive en retard, en double, ou quand le monde extérieur ne répond pas ?
Les idées qui restent
1Un compteur de vues efface ce qu'il compte
Le premier besoin de Video Tutorials est banal : afficher sur la page d'accueil le nombre total de vidéos regardées. Le réflexe CRUD tient en une ligne de code. La table videos a une colonne de compteur, et chaque lecture l'augmente de un. Pour qui a l'habitude du CRUD MVC, c'est le geste naturel (p. 20).
Garofolo pose alors trois questions. Et si l'on payait les créateurs d'après ces compteurs ? Et si l'on découvrait une erreur dans le comptage ? Et si l'on voulait suivre la popularité d'une vidéo dans le temps ? Les trois butent sur le même mur : « Si tout ce qu'on a, ce sont les totaux actuels, on a perdu l'information qu'il faudrait pour recalculer. » (p. 20)
Une table video_views, avec une ligne par vue, ferait mieux. Encore faut-il l'avoir créée dès le premier jour. Ajoutée plus tard, elle ne se remplit que pour l'avenir, et les vues d'avant sont perdues pour de bon (p. 21). Même cette table ne suffit pas longtemps. Avant de payer un créateur, il faudra vérifier qu'une vue est légitime, un contrôle trop long pour la durée d'une requête web. Il faudra aussi prévenir d'autres parties du système, à chaque vue, qu'elles ont du travail. La table y suffirait à peine, et pour ce second besoin ce serait un montage sur mesure (p. 21).
Le second défaut est plus sournois. Le livre prévoit d'envoyer, lire, compter, commenter et renommer des vidéos, et demande si une seule table videos tiendrait pour tout cela (p. 24). Avec cette table unique, chaque nouvelle fonction doit savoir qui d'autre la lit et l'écrit (p. 21). L'auteur le résume : « On n'en est qu'à un seul fichu modèle, et le CRUD MVC nous a déjà acculés dans un coin. » (p. 21)
Garofolo ne condamne pas le CRUD pour autant. Si le compteur était le seul usage de ces données, il le juge raisonnable (p. 22). Le problème naît dès que les mêmes données servent à plusieurs choses. Il y revient en conclusion. Pour prototyper, le framework MVC reste à ses yeux l'outil le plus rapide, surtout si l'on découpe déjà la table fourre-tout (pp. 247-248).
Sa réponse tient en deux changements. D'abord, on range les changements eux-mêmes : « nous modéliserons nos données en stockant les transitions d'état elles-mêmes » (p. 22). « Une vidéo a été vue » devient une ligne qu'on ne modifie plus jamais, et le compteur n'est plus qu'un calcul sur ces lignes. C'est l'Event Sourcing annoncé plus haut. Ensuite, les morceaux du système cessent de se coordonner en lisant la même table. Ils échangent des messages, de petits enregistrements que l'un écrit et que l'autre lit plus tard. La forme en est écrite noir sur blanc, un contrat : « les différentes parties communiqueront par un contrat explicite » (p. 22).
À retenir : une colonne qu'on écrase est une décision d'oublier, prise sans le savoir. Avant de créer un compteur, demandez-vous si quelqu'un voudra un jour savoir d'où il sort. L'idée suivante montre pourquoi découper l'application en services ne règle pas le second défaut, celui de la table partagée.
2Mettre une table derrière HTTP ne fait pas un microservice
Face à un monolithe qui grossit, le réflexe courant est d'en extraire des services. Le livre déroule ce réflexe sur son application. À côté de videos, une table users apparaît, et les deux modèles s'appellent dans le même code. On les sépare en deux services web, chacun avec sa base.
Le résultat est une facture. Il y a maintenant deux serveurs et deux bases. Les appels de fonction deviennent des appels HTTP, et la jointure SQL est à réécrire dans le code (p. 29). Pire, videos dépend de users : si users tombe, videos aussi. Le service des vidéos « a maintenant deux raisons d'échouer » (p. 29). Le diagnostic de l'auteur : « C'est en tout point encore un monolithe, seulement il est distribué maintenant : un monolithe distribué. » (p. 29)
Sa définition explique pourquoi le découpage n'a rien changé : « Un monolithe est un modèle de données, pas une stratégie de déploiement ou d'organisation du code. » (p. 26) Le nombre de serveurs ne compte pas. Tant que tout le monde dépend des mêmes données, le système reste d'un seul bloc.
Le critère du livre est donc l'autonomie, ce qui définit un microservice selon lui (p. 29), et il est strict. Le livre parle de composant (Component), un mot qu'il préfère à « service », jugé trop vague (p. 23). Un composant ne répond à aucune question : ce qu'on interroge s'appelle une base de données, même placée derrière HTTP (p. 30). Il n'en pose pas non plus : « Les composants ne posent de questions à rien d'autre. » (p. 30) S'il interrogeait un autre service, il tomberait avec lui.
Il reste un seul canal, le message asynchrone : on l'écrit dans un magasin commun, le Message Store, et qui veut le lit plus tard. Le livre en distingue deux sortes (p. 30) :
- Commande : une demande, au mode impératif (
PublishVideo). Le composant qui la reçoit peut la refuser. - Événement : un fait au passé (
VideoPublished). « Les événements sont des traces de choses qui sont arrivées. » (p. 30)
Un magasin que tout le monde lit, n'est-ce pas une table partagée de plus ? Non, et la différence tient au message. Une table se réécrit, et qui la lit dépend de sa forme du jour. Un événement est déjà arrivé : « Vous n'êtes pas obligé de l'aimer, mais vous devez faire avec. » (p. 30) Chaque composant documente les commandes qu'il traite et les événements qu'il publie (p. 30). Cette liste forme son contrat, et l'auteur la veut figée : « Les contrats de messages sont censés être immuables. » (p. 247) Les autres pièces dépendent de ce contrat, jamais de la façon dont le composant range ses données. L'équipe s'engage à communiquer « par ce contrat plutôt que par les entrailles des uns et des autres » (p. 163).
Le nom d'un message vient du métier. Le livre bannit « create », « update » et « delete » de ses noms de messages (p. 32). Un visiteur qui s'inscrit produit Registered, pas UserCreated. La nuance compte, car un compte créé par un administrateur n'est pas une inscription (p. 86). L'auteur insiste : « l'événement de domaine n'est pas un alias d'une action CRUD, il capture l'intention de l'utilisateur » (p. 86).
Autour du composant, le livre range tout le système en cinq pièces (pp. 22-23) :
- Application : ce que l'utilisateur touche, les routes HTTP, en requête et réponse. Tout le code CRUD qu'on écrivait déjà en est une.
- Composant : il porte un processus métier, lit des commandes et publie des événements.
- Message Store : le magasin où vivent tous les messages, à la fois « un stockage durable de l'état et un moyen de transport » (p. 23).
- Agrégateur (Aggregator) : il lit des événements et en tire des données prêtes à afficher.
- Données de vue (View Data) : ces données, en lecture seule. On ne prend aucune décision à partir d'elles (elles ont toujours un peu de retard sur les événements), « d'où leur nom » (p. 23).
Une action de l'utilisateur fait le tour de ces pièces. Il appelle une Application, qui écrit un message. Un composant le lit, travaille, puis écrit de nouveaux messages. Un agrégateur les transforme en données de vue, que l'Application lit pour répondre (p. 24). L'auteur prévient : « Cette structure va vous sembler bizarre au début. » (p. 24)
L'autonomie paie vite. Quand le livre écrit l'Application d'inscription, le composant d'identité n'existe pas encore. Seul existe son contrat, un fichier contract.md qui décrit ses messages (p. 87). Le bilan de l'auteur : « Avec un contrat de messages bien défini, vous avez pu construire un client d'un composant sans avoir construit le composant lui-même. » (p. 104) Deux équipes peuvent donc avancer en parallèle, chacune de son côté du contrat.
À retenir : avant d'appeler « microservice » un service web, demandez-lui s'il répond à des questions sur ses données. S'il le fait, les autres dépendent de son modèle, et vous avez distribué votre monolithe. Reste à savoir où ranger ces messages : l'idée suivante ouvre le Message Store.
3Le magasin de messages n'est qu'une table PostgreSQL
Les messages de l'idée 2 doivent vivre quelque part, et beaucoup d'équipes pensent aussitôt à un logiciel dédié. Garofolo réduit d'abord le cahier des charges. Un stream est la suite ordonnée des messages d'une même chose, par exemple tous les événements de l'utilisateur 7f3a. Le magasin doit garder des streams de messages qu'on ne modifie jamais et auxquels on ne fait qu'ajouter (p. 41). Il doit aussi empêcher deux écritures simultanées de se marcher dessus (p. 41).
Le livre choisit Message DB, un projet libre de l'équipe Eventide qui s'installe dans PostgreSQL. Tout repose sur une seule table, message_store.messages, et sur quelques fonctions SQL par lesquelles on passe pour lire et écrire (p. 43). L'auteur s'en amuse : « C'est beaucoup plus simple qu'on ne l'attendrait d'une infrastructure de messagerie. » (p. 43) Voici quatre lignes de cette table, réduites à leurs colonnes utiles. C'est un exemple de ma construction, pas une capture du livre :
stream_name type position global_position
identity:command-7f3a Register 0 41
identity-7f3a Registered 0 42
viewing-c2d9 VideoViewed 0 43
identity-7f3a RegistrationEmailSent 1 47
Quatre colonnes portent l'essentiel. La colonne type donne le nom du message (Registered). La colonne stream_name dit à qui il appartient. Son nom suit une convention : ce qui précède le premier tiret est la catégorie (identity), ce qui suit identifie l'entité, la chose suivie, ici un utilisateur (p. 34). Les commandes vivent dans une catégorie à part : « Notez que identity et identity:command sont deux catégories distinctes. » (p. 88) Cette séparation trace la frontière du composant : lui seul écrit dans identity et traite identity:command (p. 35). La position est le rang d'un message dans son stream. La position globale (global_position) est son rang dans toute la table (p. 42). La première sert à détecter les conflits, la seconde à lire les messages dans l'ordre d'arrivée, comme le montrent les idées 4 et 5.
La seconde exigence passe par la fonction d'écriture, write_message, qui accepte une version attendue (p. 45). « Les streams ont des versions, et leur version est égale à la plus grande position de leurs messages. » (p. 47) L'exemple du livre : deux instances d'un même composant traitent en même temps la commande « créditer 5 dollars » sur un compte. Chacune écrirait un événement Deposited, et le compte serait crédité deux fois (p. 47). On écrit donc « seulement si le stream est encore à la version 3 » (p. 47). D'après la définition ci-dessus, la première écriture amène le stream à la version 4, et la seconde est refusée. Le code du livre traduit ce refus en une erreur de conflit lisible (p. 47). Le mécanisme est celui de l'idée 4 de la fiche Implementing DDD, CQRS and Event Sourcing, logé ici dans la base.
Reste la question que l'auteur dit entendre souvent : pourquoi pas Kafka ? Kafka est un broker (un courtier de messages) très répandu. Il fait circuler de gros volumes d'événements entre programmes et les range dans des files nommées, les topics. Garofolo y voit un bon outil et donne deux raisons de ne pas s'en servir comme magasin (p. 49) :
- Les streams ne se déclarent pas d'avance. Kafka attend des topics prévus à l'avance. Or « dans un système event-sourcé, on ne peut pas savoir à l'avance de quels streams on aura besoin » (p. 49). Avec un stream par utilisateur, il faudrait connaître chaque futur inscrit.
- Pas de version attendue. Kafka ne sait pas refuser une écriture concurrente. L'auteur reprend la formule de Scott Bellware : Kafka « est un tampon, pas un Message Store » (p. 49).
Un broker ajoute enfin de l'exploitation et des pannes nouvelles. Pour ce besoin précis, il n'offre aucun avantage « sur PostgreSQL, que nous utilisons déjà de toute façon » (p. 49).
Mise à jour 2026. L'auteur cite une demande ouverte chez Apache pour ajouter la version attendue à Kafka, KAFKA-2260. Elle est toujours ouverte et classée « Minor » (suivi du projet consulté le 13/11/2026). Pour écrire le même magasin vous-même, mon article CQRS en Go, partie 4 : PostgreSQL comme event store montre la table et la contrainte UNIQUE (aggregate_id, version) (identifiant de l'entité et version) qui refuse la seconde écriture.
À retenir : avant d'ajouter un broker pour faire de l'Event Sourcing, regardez la base que vous avez déjà. Une table, une convention de noms et une écriture conditionnelle suffisent à démarrer. Ranger les faits ne suffit pas, il faut encore en tirer des écrans : c'est l'idée suivante.
4Un écran se lit en une ligne, et la ligne sait quel message elle a déjà compté
Un journal de faits est parfait pour écrire et désastreux pour afficher. L'auteur le dit sans détour : « Les Message Stores sont fantastiques pour écrire des données, mais exceptionnellement mauvais pour afficher des écrans aux utilisateurs. » (p. 51) Additionner toutes les vues à chaque affichage de la page d'accueil n'est pas une option.
La réponse est l'agrégateur de l'idée 2. Il lit chaque événement VideoViewed et met à jour une table taillée pour l'écran. Garofolo pousse l'idée loin : « Et si l'on pouvait concevoir le schéma de la base pour que chaque écran ne demande qu'une seule ligne ? » (p. 53) Sa table pages a donc une ligne par écran, avec un document JSON. Celle de la page d'accueil contient { "videosWatched": 42, "lastViewProcessed": 24 } (p. 53). On écrit des événements, on lit des pages : c'est le CQRS annoncé en ouverture (p. 52).
Le danger arrive avec les doublons. Un message peut être livré deux fois (l'idée 5 montre comment), car « il est physiquement impossible de garantir une livraison exactement une fois » (p. 57). Un compteur naïf compterait alors deux vues pour une seule.
La parade s'appelle l'idempotence : une opération est idempotente quand la refaire ne change plus rien. L'auteur le dit à sa façon : « seuls deux états comptent, l'avoir appelée zéro fois, et l'avoir appelée une fois ou plus » (p. 57). Voici la requête de mise à jour, résumée d'après le livre (p. 55) :
UPDATE pages
SET -- compteur + 1, et position du message traité
WHERE page_name = 'home' AND
(page_data->>'lastViewProcessed')::int < :globalPosition
Le compteur et la position du dernier message appliqué changent dans la même instruction. Un message déjà vu a une position globale inférieure ou égale à celle rangée dans la ligne. La condition WHERE devient fausse, et la requête ne touche rien. L'exercice proposé est parlant : retirer cette condition, cliquer quelques fois sur le bouton qui simule une vue, redémarrer le serveur. Les compteurs grimpent sans nouvelle vue (p. 63). Le livre ne dit pas pourquoi à cet endroit. L'idée 5 en donne la raison. La position de lecture n'est écrite que tous les 100 messages, et un redémarrage relit donc des messages déjà comptés. Puis « imaginez qu'au lieu de compter des vues, ce soit un service qui effectue des virements à sept chiffres » (p. 63).
Le développeur SQL objecte alors que ce compteur duplique une information. Garofolo l'assume. Ce qu'on ne duplique jamais, c'est la source de vérité, les messages (p. 62). Les données dérivées se multiplient à volonté, et le livre tire trois vues différentes des mêmes événements : le total global, le compte par vidéo et la page d'accueil (p. 62). Le MVC classique en fait autant : ses jointures sont aussi des données dérivées, mais refaites à chaque requête (p. 62).
En pratique. En Go ou en PHP, une colonne last_position bigint est plus simple que le document JSON du livre. La requête devient UPDATE … SET count = count + 1, last_position = $1 WHERE id = $2 AND last_position < $1, et zéro ligne touchée signifie « déjà compté ». Cette garde suppose un seul lecteur qui traite les messages dans l'ordre des positions globales, ce que fait l'abonnement de l'idée 5.
À retenir : une vue idempotente range, à côté de sa donnée, la trace du dernier message appliqué, et elle écrit les deux dans la même requête. Reste à savoir comment l'agrégateur reçoit ses événements. La réponse est une boucle toute simple, objet de l'idée suivante.
5S'abonner aux messages, c'est relire la table en boucle
Un agrégateur doit voir les nouveaux messages au fil de l'eau. Un broker les pousse vers ses lecteurs. Message DB ne pousse rien, et l'auteur prévient : « J'espère que le polling ne heurte pas votre sens de la conception, parce que c'est sur lui que repose la solution. » (p. 65) Le polling consiste à demander régulièrement « quoi de neuf ? » au lieu d'attendre qu'on vous prévienne.
Le livre construit cet abonnement à la main. Chaque lecteur déclare des handlers, une fonction par type de message à traiter. Le cœur de la boucle tient en cinq lignes, où tick fait un tour de lecture (pp. 72-73) :
while (keepGoing) {
const messagesProcessed = await tick()
if (messagesProcessed === 0)
await Bluebird.delay(tickIntervalMs)
}
Bluebird est une bibliothèque de promesses JavaScript, que le livre emploie partout. À chaque tour, tick lit au plus 100 messages de la catégorie, après la dernière position globale traitée. Il les passe un par un, dans l'ordre, au handler de leur type. Si le lot était vide, la boucle attend 100 millisecondes avant de redemander (pp. 66-67).
L'abonné range sa position dans le Message Store lui-même. Tous les 100 messages, il écrit un événement Read qui contient sa position, dans un stream à son nom (subscriberPosition- suivi de son identifiant). Au redémarrage, il relit le dernier Read et repart de là (pp. 66-68). La taille du lot, l'attente et la fréquence des Read se changent à la création de l'abonnement.
Le détail compte. La position s'écrit après le traitement, et seulement de temps en temps. Un arrêt brutal entre deux écritures fait donc relire des messages déjà traités. Le livre l'assume : « nous n'avons aucune garantie de ne traiter un message donné qu'une seule fois » (p. 67). C'est une livraison au moins une fois : chaque message arrive, parfois plusieurs fois. Chaque handler doit donc être idempotent, comme la vue de l'idée 4. La position n'est qu'un raccourci : « enregistrer et relire la position de lecture est une optimisation de performance et n'a rien à voir avec la justesse » (p. 73). Sans elle, il faudrait revérifier tout le stream à chaque démarrage, un million de messages peut-être (p. 73).
Une erreur, enfin, arrête tout l'abonnement. Le choix est voulu : « Nous n'allons pas essayer de nous remettre automatiquement d'une erreur. Si quelque chose casse à ce niveau, nous voulons l'attention d'un humain. » (p. 73)
Mise à jour et piège. La version actuelle de Message DB sait répartir une catégorie entre plusieurs instances d'un même lecteur. Ce sont ses groupes de consommateurs. Deux paramètres de get_category_messages les règlent : consumer_group_member (le numéro de l'instance) et consumer_group_size (leur nombre) (dépôt du projet consulté le 13/11/2026). Le livre n'en esquisse que l'idée, dans son dernier chapitre. Un hachage du nom de stream, modulo le nombre d'instances, y désigne celle qui traite le message. Son code ne le gère pas et lit chaque catégorie avec un seul processus (p. 245). Autre conséquence de l'arrêt sur erreur : un message qui échoue à chaque fois bloque toute sa catégorie. Surveillez donc les positions qui n'avancent plus.
À retenir : un abonnement, c'est une boucle, une position et une règle, tout message peut revenir. Avec ces trois pièces, l'agrégateur voit passer les faits. Un composant, lui, doit décider, et il lui faut pour cela l'état exact d'une entité : c'est l'idée suivante.
6Pour décider, un composant recalcule l'entité depuis son propre stream
Le composant d'identité reçoit une commande Register. Avant d'écrire Registered, il doit savoir si cet utilisateur est déjà inscrit. Il ne peut pas se fier aux données de vue : elles ont toujours un temps de retard, et l'idée 2 interdit de décider sur elles. L'auteur prend un exemple plus lourd, celui d'un virement de 10 millions de dollars à accepter ou non (p. 106).
La réponse tient en une fonction du Message Store, fetch. Elle lit tous les événements du stream de l'entité, puis les applique un à un à un objet de départ. La recette qui dit comment appliquer chaque événement s'appelle une projection. Voici celle de l'identité, raccourcie de son champ e-mail (p. 108) :
const identityProjection = {
$init () {
return { id: null, isRegistered: false }
},
Registered (identity, registered) {
identity.id = registered.data.userId
identity.isRegistered = true
return identity
},
}
La clé $init donne l'état de départ, une identité vide. Chaque autre clé porte le nom d'un type d'événement, et sa fonction renvoie l'entité mise à jour. Un événement sans clé est ignoré (p. 107). L'auteur résume le rôle de fetch : « C'est la fonction qui implémente l'event sourcing dans notre système. » (p. 106) Le handler de Register charge l'identité ainsi. Si isRegistered vaut déjà vrai, la commande a déjà été traitée et le composant ne fait rien. Sinon, il écrit Registered dans le stream de cette identité (p. 115). L'idempotence du composant passe donc par là : l'effet d'un message déjà traité se voit dans l'entité projetée (p. 117).
Une règle découle de cette définition : seuls les événements entrent dans une projection. « Les commandes ne sont pas de l'état. Ce sont des demandes de changement d'état, donc nous n'utiliserons jamais un type de commande dans une projection. » (p. 107) Une commande refusée n'a rien changé. La compter fausserait l'entité.
Le livre clarifie au passage deux mots qui sèment la confusion. Projeter, pour un composant, c'est recalculer l'entité à chaque commande, pour décider. Un agrégateur fait la même opération en continu, pour nourrir un écran. D'où la formule : « Les composants projettent, les agrégateurs agrègent. » (p. 116) Rejouer a aussi deux sens. Le premier est cette projection refaite à chaque commande. Elle ne relance aucun effet de bord, parce qu'une projection ne fait que renvoyer un objet (p. 116). Le second sens concerne un composant ou un agrégateur écrit après coup. Son abonnement part du tout premier message du Message Store et remonte jusqu'au présent (p. 116). C'est ce qui arrive au composant d'identité, écrit alors que l'Application lui envoyait déjà des commandes (p. 116). Ce rattrapage-là traite des messages, avec leurs effets. Il ne doit pas les refaire, par exemple réécrire Registered pour une identité déjà inscrite. La réponse « commence par "idem" et finit par "potent" » (p. 117), autrement dit idempotent.
L'auteur avoue une limite. La lecture renvoie au plus 1 000 messages par défaut, donc fetch ne projette pas un stream plus long (p. 110). Il l'accepte : « Il est très peu probable qu'un stream donné dépasse jamais ce nombre, donc nous accepterons cette limite » (p. 110), avec une surveillance pour savoir quand changer. La parade connue est l'instantané (snapshot) : on range de temps en temps l'état calculé, puis on ne rejoue que la suite. Le livre la décrit sans l'implémenter (p. 114).
À retenir : un composant ne croit que son propre stream, relu à chaque commande, jamais une vue. Le calcul de l'état par les événements est détaillé dans l'idée 8 de la fiche Implementing DDD. Le principe se heurte vite à un cas concret, l'adresse e-mail en double, que l'idée suivante déroule.
7Une adresse e-mail en double est une question pour le métier, pas une catastrophe
Avec l'inscription, le livre tombe dans son propre piège, et il le dit. Les données de vue sont cohérentes à terme : elles finissent par refléter les événements, avec quelques instants de retard. Or le formulaire d'inscription doit vérifier que l'adresse n'est pas déjà prise. Pour cela, l'Application lit la table user_credentials, des données de vue remplies par un agrégateur (pp. 95-96). L'auteur le reconnaît : « Gardez en tête que ce contrôle de doublon a utilisé des données de vue cohérentes à terme. » (p. 96) C'est « une contradiction directe » de la règle posée à l'idée 2 (p. 99).
Le livre déroule la course qui en découle, en huit étapes (pp. 100-101). C'est le lecteur inquiet qui la raconte, et l'auteur la juge « tout à fait exacte » (p. 101). Deux visiteurs envoient en même temps l'adresse tricksie@example.com. Les deux passent le contrôle, car la table ne contient pas encore cette adresse. Le composant écrit deux événements Registered. L'agrégateur insère la ligne du premier, puis tombe sur un second Registered avec la même adresse. Deux identités partagent désormais une adresse, exactement ce que le contrôle devait empêcher. Le lecteur inquiet en conclut qu'on ne s'en remettra pas (p. 101).
Combien de temps dure le danger ? Une fenêtre « de l'ordre de quelques millisecondes » (p. 101), et l'auteur juge peu probable que deux demandes y tombent (p. 101). Le lecteur prudent objecte, et l'auteur formule lui-même l'objection avec une phrase de Nassim Nicholas Taleb : en présence d'une probabilité de ruine, on finit ruiné (p. 101). Sa réponse : « Écrire ces deux événements n'est pas la ruine. » (p. 101)
La réparation est simple, parce que le doublon a son propre identifiant d'utilisateur, ce qui distingue déjà les deux identités. Le livre l'esquisse sans la coder : on pourrait ajouter à user_credentials une colonne needs_to_change_email, à false pour le premier inscrit. L'agrégateur écrirait la ligne du second avec true, et un écran inviterait cet utilisateur à changer d'adresse (pp. 101-102).
Si l'unicité était vraiment non négociable, ce serait une propriété fondamentale des identités : deux d'entre elles ne partageraient jamais une adresse. Ce serait alors au composant qui possède les identités de la garantir (p. 102). Il tiendrait sa propre liste des adresses prises, interdite aux autres, et rejetterait la commande par un événement RegistrationRejected (p. 102). Pour Video Tutorials, l'équipe métier a tranché : l'unicité des adresses n'est pas une propriété fondamentale des identités (p. 102). La leçon reste entière, car la discussion « montre pourquoi on ne veut pas s'appuyer sur les données de vue pour prendre des décisions » (p. 102).
Le livre donne une méthode pour trancher ces cas, quatre questions à poser (p. 100) :
- Quelle est la probabilité de ce qu'on veut empêcher ?
- Quelle est sa gravité si cela arrive ?
- À qui appartient la contrainte, à l'Application ou au composant propriétaire de la donnée ?
- Que veut dire « correct » ici ?
Pour la dernière, l'auteur renvoie à l'équipe métier : « Travaillez avec votre équipe métier. C'est de toute façon une bonne chose à faire. » (p. 103)
Pour aller plus loin. Si votre métier refuse tout doublon, la fiche d'Understanding Eventsourcing décrit le Reservation Pattern. L'adresse elle-même devient l'identifiant d'un stream, et la version attendue de l'idée 3 refuse le second inscrit.
À retenir : une contrainte d'unicité sur des données cohérentes à terme n'est pas un bug à cacher. C'est un risque à chiffrer puis à décider avec le métier, et une réparation simple suffit souvent. Le même réflexe vaut, en plus grave, quand l'effet sort du système : c'est l'idée suivante.
8Un e-mail parti ne revient pas : zéro envoi ou deux, c'est au métier de choisir
Dans un projet Rails, l'auteur a vu l'e-mail de bienvenue accroché à la création de la ligne utilisateur (p. 133). Pourquoi l'écriture d'une ligne déclencherait-elle un envoi ? Le livre sort l'envoi dans son propre composant, send-email. Celui-ci reçoit une commande Send et répond par un événement Sent ou Failed (pp. 134-135).
Première règle, c'est l'appelant qui choisit l'identifiant de l'e-mail. « Pour que l'idempotence fonctionne, le client d'un composant idempotent doit fournir l'identifiant. » (p. 135) Le composant d'identité le calcule à partir de l'adresse de l'utilisateur (p. 148). Une commande renvoyée porte donc le même identifiant. send-email relit alors le stream de cet e-mail, sendEmail- suivi de l'identifiant, et y trouve Sent s'il est déjà parti (pp. 140-141).
Le vrai problème est ailleurs. SMTP, le protocole du courrier électronique, ne dit pas toujours si l'envoi a réussi. Avec un fournisseur tiers, la requête peut échouer alors que l'e-mail est déjà parti, et une nouvelle tentative l'enverra deux fois (p. 135). Dans le livre, « envoyer » consiste à écrire un fichier, puis l'événement de réussite. Un arrêt entre les deux fait réécrire le fichier au redémarrage (p. 136).
Pour Garofolo, ce choix n'appartient pas aux développeurs. « Quel échec est le pire ? Ne pas envoyer un e-mail, ou l'envoyer plus d'une fois ? » (p. 136) Les développeurs connaissent les détails techniques, l'équipe métier connaît l'effet de chaque cas (p. 136). Pour l'inscription, la décision est écrite dans le contrat du composant : « envoyer nos e-mails plus d'une fois est préférable à ne pas les envoyer du tout » (p. 136).
La même question donne la réponse inverse pour une carte bancaire, où un double débit est inacceptable. Le livre écrirait alors l'intention avant d'agir : « on voudrait écrire qu'on tente de débiter la carte avant de la débiter, pour ne jamais tenter de la débiter deux fois » (p. 153). Un débit oublié se rattrape ensuite par un outil d'administration ou à la main (p. 153). L'auteur reste lucide : « Il n'y a aucun moyen de garantir que notre système sera d'accord avec un système externe sur l'état du monde » (p. 153). Garder une trace de chaque étape aide au moins à déboguer.
Reste à savoir qui déclenche l'envoi. Le livre déroule le trajet en six étapes (p. 146) :
identityvoit passer son propre événementRegistered.- Il écrit une commande
Senddans le stream de commandes desend-email. send-emaillit la commande.- Il envoie l'e-mail et écrit
Sent. identityvoit passerSent.- Il écrit
RegistrationEmailSentdans le stream de l'identité.
L'étape 5 demande un tri. identity s'abonne aux événements de send-email (catégorie sendEmail). Or d'autres composants peuvent aussi demander des e-mails, et identity ne veut que les siens. Les métadonnées sont les informations de service jointes à un message. identity y glisse une clé originStreamName, le nom de son propre stream. send-email recopie cette clé dans les événements qu'il écrit. L'abonnement de identity filtre sur elle et ne lui remet que les événements dont l'origine est de catégorie identity (pp. 142-151).
Ce schéma s'appelle l'orchestration : un composant écrit une commande explicite à un autre. Le livre range dans cette famille deux noms qu'on croise souvent, le process manager et la saga, deux variantes d'orchestration (p. 154). La chorégraphie serait l'inverse : send-email observerait Registered et saurait seul quoi faire, comme des danseurs qui connaissent chacun leur partie (p. 154). Garofolo choisit l'orchestration : « Nous privilégierons l'orchestration, parce qu'elle rend les processus plus explicites. » (p. 154) On lit en un seul endroit qu'une inscription envoie un e-mail.
En pratique. L'identifiant fourni par l'appelant est ce que les API appellent une clé d'idempotence. Mon article L'idempotence expliquée, partie 1 : idempotency key et middleware Go la montre côté serveur HTTP. Pour une orchestration en plusieurs étapes, voyez la fiche d'Event-Driven Architecture in Golang. L'état de la saga y vit dans une ligne de base, qui avance à chaque réponse.
À retenir : devant un effet qu'on ne peut pas reprendre, posez la question du livre, « zéro fois ou deux fois ? ». Faites trancher le métier, puis écrivez la réponse dans le contrat. Le montage de l'envoi, une commande, un composant et un événement de fin, a une autre vertu : il rend inutile une brique d'infrastructure entière.
9Les tâches de fond n'ont pas besoin d'une file de jobs
Dans une application web classique, on confie tout travail long à une file de tâches de fond (background jobs). Cet outil exécute le travail hors de la requête. Le livre cite DelayedJob, qui s'appuie sur la base, et node-resque, qui demande Redis, une base en mémoire (p. 158). L'auteur ouvre sur une image : commander une pizza au téléphone et rester en ligne jusqu'à la livraison. Certains travaux n'ont rien à faire dans le temps d'une requête web. Les réseaux tombent de temps en temps, et « si vous n'en avez pas tenu compte dans votre système, ces pannes externes deviendront des pannes internes » (p. 157).
Garofolo définit alors une tâche de fond au plus simple : un travail asynchrone qui rend probablement compte de son avancement en chemin (p. 158). Une commande qui lance le travail, des événements qui en rendent compte : cette définition rappelle quelque chose. L'auteur cite Greg Young : « Qu'est-ce qu'une file, sinon une forme dégénérée de stream ? » (p. 158) Faut-il Kafka ou RabbitMQ, un autre broker, pour des tâches de fond ? Sa réponse est « non », puis « sérieusement, non », car « vous avez déjà construit toutes les briques nécessaires » (p. 159).
Le cas d'école est la mise en ligne d'une vidéo, qu'il faut transcoder, c'est-à-dire convertir dans les formats que lisent les navigateurs. L'Application écrit une commande PublishVideo. Un composant video-publishing la lit, transcode, puis écrit VideoPublished avec l'adresse du fichier converti, ou VideoPublishingFailed avec la raison (pp. 159-161). La vidéo n'est officiellement publiée qu'au moment où VideoPublished est écrit (p. 160). Le composant se protège des doublons comme celui des e-mails, car sa projection retient qu'une publication a déjà été tentée (p. 167). L'auteur conclut : « Publier une vidéo n'est pas vraiment différent d'envoyer un e-mail. » (p. 167)
La question de l'idée 8 revient, avec une autre réponse. Un arrêt entre la fin du transcodage et l'écriture de VideoPublished fera refaire le travail. Cette fois, le métier accepte le double : « Transcoder des vidéos ne coûte pas cher, et nous ne pouvons absolument pas accepter de ne pas les transcoder » (p. 171). Le travail refait écrit au même endroit, donc aucun fichier orphelin ne traîne (p. 171).
Une nuance limite la démonstration : le livre simule le transcodage (pp. 160, 168). On ne voit donc jamais un vrai travail de plusieurs minutes. La thèse, elle, est claire : « Tout est déjà une tâche "de fond". » (p. 163)
À retenir : avant d'installer une file de jobs, vérifiez si vos messages ne font pas déjà le travail. Une commande, un composant abonné et un événement de fin suffisent. Ce que l'asynchrone simplifie côté serveur, il le fait payer à l'écran : c'est l'idée suivante.
10Un écran asynchrone répond avant de connaître la réponse
Le livre ne cache pas que les composants coûtent plus cher à construire qu'un monolithe. Son image est celle des portes étanches d'un navire : elles demandent plus de métal, mais une fuite reste dans un seul compartiment. L'auteur la chiffre, à titre d'image et non de mesure. Construire le monolithe coûterait dix points de complexité, le système en composants quinze. Mais ce total n'est pas ce qui épuise. Ce qui compte, c'est ce qu'il faut garder en tête pendant qu'on travaille. Avec les composants, « vous n'avez à garder que deux points de complexité en tête à un moment donné » (p. 173), car le contrat de messages ferme les portes entre les parties. Avec le monolithe, il en faut huit sur dix, parce que rien ne sépare une partie d'une autre (p. 173). L'écran est l'endroit où l'on paie la différence.
L'exemple est un créateur qui veut nommer sa vidéo, intitulée « Untitled » par défaut. Le formulaire envoie une commande NameVideo, et le composant répond par VideoNamed ou VideoNameRejected. Le livre fait valider le nom par le composant, pas par l'Application, et la raison tient dans une distinction. Une adresse e-mail mal formée se refuse dans l'Application, car la règle vient du monde extérieur, pas du système : Video Tutorials « n'a pas inventé l'e-mail » (p. 175). Un nom vide, lui, est une règle propre au système, donc elle revient au composant (p. 175).
L'écran change de forme en conséquence. Au lieu d'un formulaire « modifier la vidéo », il en propose un par tâche, un pour nommer et un pour décrire : « Ils ne sont pas là pour modifier une vidéo, ils sont là pour nommer une vidéo. » (p. 176) Un événement VideoUpdated, lui, ne dirait même pas ce qui a changé (p. 191).
Le hic arrive à la fin de la requête HTTP. La commande n'a peut-être pas encore été traitée : « À vrai dire, nous ne savons même pas si elle va être traitée. » (p. 178) L'aveu suit : « C'est dans ce type d'interaction que notre architecture est plus difficile à manier qu'un monolithe. » (p. 178)
La réponse du livre tient en trois pièces, dans l'ordre où elles servent (pp. 178-181) :
- Chaque message porte dans ses métadonnées un traceId, l'identifiant de l'action de l'utilisateur qui l'a déclenché. Les événements écrits en réponse à
NameVideoreprennent le sien. - Une table de vue,
video_operations, garde pour chaquetraceIdle succès de l'opération ou la raison de son échec. Un agrégateur la remplit. - Après l'envoi du formulaire, l'Application redirige vers une page d'attente qui porte ce
traceIddans son adresse. Tant qu'aucune ligne n'existe, la page affiche « Operation pending » et se recharge chaque seconde. Ensuite, elle mène à la vidéo ou affiche l'échec.
Le livre signale un piège. Chaque requête HTTP reçoit son propre traceId (p. 31). Celui qu'on cherche est celui de la requête qui a écrit la commande, pas celui de la page d'attente (p. 179).
L'auteur ne vend pas ce motif comme universel : « Est-ce toujours le bon motif ? Non. Mais il convient à ce cas. » (p. 181) Il admet que ce parcours « n'est pas joli » (p. 190). Garder la requête ouverte jusqu'au résultat immobiliserait le serveur (p. 190). Une interface JavaScript riche ferait la même attente en arrière-plan, mais le livre ne la construit pas (p. 191).
En pratique. Le rechargement complet de la page se remplace sans toucher à l'architecture. Une petite requête JavaScript en arrière-plan, chaque seconde, sur la même adresse suffit. Les Server-Sent Events font mieux : par ce flux HTTP, le serveur pousse la réponse dès que la ligne existe. Prévoyez aussi une attente maximale, au-delà de laquelle l'écran dit que la demande est toujours en cours.
À retenir : dans un système asynchrone, l'écran accuse réception, puis lit le résultat dans une vue en suivant l'identifiant de l'action. Reste à sortir ce système d'un seul processus, puis à le dépanner quand il casse. Ce même traceId y devient l'outil principal : c'est l'idée suivante.
11Distribuer sur une raison observable, déboguer par les messages (survol)
Pour l'instant, tout le système tourne dans un seul processus (p. 201). Faut-il découper le déploiement ? Le livre cite Tyler Treat : « La première règle des systèmes distribués est de ne pas distribuer votre système tant que vous n'avez pas de raison observable de le faire. » (p. 201) La raison retenue par l'auteur est la disponibilité : un composant en panne ne doit pas faire tomber le site que voient les utilisateurs (p. 202). Quand il sépare le système en deux déploiements, il ne touche ni aux Applications, ni aux composants, ni aux agrégateurs. Seuls la configuration et le point d'entrée bougent : « L'autonomie a rendu la stratégie de déploiement indépendante du modèle du système. » (p. 204)
Une fois le système déployé, le livre montre ce que le Message Store apporte quand il tombe en panne. Il en met une en scène : beaucoup d'inscriptions, mais peu de connexions. La colonne « e-mail d'inscription envoyé » du tableau d'administration répond « non » pour chaque utilisateur (p. 219), et le livre suit cette piste. Une inscription réussie produit cinq messages, de Register à RegistrationEmailSent. Une requête qui filtre les messages sur le traceId de leurs métadonnées n'en trouve que trois, signe que l'e-mail ne part pas. La cause est une ligne commentée dans la configuration, qui empêche le composant d'envoi de démarrer (pp. 220-224). L'auteur précise que sa requête n'utilise pas d'index, et qu'elle ne suivrait pas sur des millions de messages (p. 222).
Il sépare enfin les journaux techniques de l'état métier. Les premiers vont dans les logs, mais l'état métier n'y va pas, « parce qu'il va dans le Message Store » (p. 223).
À retenir : un traceId recopié dans chaque message coûte une ligne de code et rend une panne lisible en une requête. Reste à vérifier, avant la production, que chaque pièce fait son travail, et le livre montre que l'autonomie rend ces tests simples.
12Tester un composant, c'est écrire un message et lire ceux qu'il produit
Beaucoup pensent qu'il faut l'échelle d'un géant du web et un outillage de test compliqué pour faire des microservices. Garofolo répond : « Balivernes et sornettes ! » (p. 232) Son argument vient de l'autonomie de l'idée 2. Un composant ne communique qu'à travers le Message Store, et « il n'a absolument aucune idée si un message vient d'un système en production ou d'un test » (p. 232). On le teste donc exactement comme il tourne.
Le livre teste ainsi les trois pièces qui écrivent (pp. 233-237) :
- Un agrégateur : on construit un événement
VideoViewed, on vide les tables de vue, on appelle le handler deux fois, puis on vérifie que le compteur vaut 1. - Un composant : on lui donne une commande
PublishVideoet un Message Store truqué. Seule sa fonctionfetch, qui charge l'état de la vidéo, échoue exprès, l'écriture marche. Le composant doit noter l'échec : le stream de la vidéo contient alors un seulVideoPublishingFailed. - Une Application : on envoie le formulaire d'inscription, puis on lit le stream de commandes pour y trouver une seule commande
Register.
Le test de l'agrégateur montre l'idempotence en une ligne. Voici son enchaînement, raccourci (p. 234) :
reset()
.then(() => aggregator.init())
.then(() => aggregator.handlers.VideoViewed(event))
// Call it a second time to verify idempotence
.then(() => aggregator.handlers.VideoViewed(event))
// puis lire la ligne 'home' : videosWatched vaut 1
Le même événement passe deux fois, et la vue ne compte qu'une vue. Une tension apparaît pourtant. Les mocks et les stubs sont les faux objets qui remplacent une dépendance dans un test. L'auteur les qualifie de « choses impies » (p. 232). Son test de composant remplace pourtant fetch par une fonction qui échoue exprès (p. 236).
Le livre va plus loin : surveiller la production vaut parfois mieux que prévoir chaque cas, et « il est tout à fait acceptable de surveiller la production plutôt que d'essayer de prédire chaque cas de test » (p. 238). L'historique complet le permet : « On peut prélever des enchaînements de messages en production et vérifier après coup qu'ils sont corrects. » (p. 238) Exemple : une commande Register restée sans RegistrationEmailSent au-delà d'un délai déclenche une alerte (p. 238).
En Go. Le substitut du livre simule une panne de l'infrastructure, jamais le comportement d'un autre composant, et c'est la frontière utile. En Go, le même test appelle deux fois le handler avec le même message, sur une base de test. Il compte ensuite les messages écrits ou lit la ligne de vue. Mon article CQRS en Go, partie 2 : command handlers sans effet de bord montre des handlers testables sans mock.
À retenir : quand chaque pièce ne parle que par messages, un test se résume à des messages en entrée et des messages en sortie. Appeler deux fois le même handler prouve l'idempotence. C'est la dernière pièce du livre : un système qu'on comprend, qu'on modifie et qu'on vérifie morceau par morceau.
Mon avis, honnêtement
Un livre drôle et concret, qui construit tout à la main pour qu'on voie enfin chaque rouage, avec un code qui a six ans. Je fais de l'Event Sourcing en Go, mais je reste un développeur web curieux, pas un architecte qui juge les choix d'Eventide. Ma question était : que m'apprend un livre qui bâtit tout sur PostgreSQL ? Réponse : moins sur les briques que sur les conversations autour.
Sa force : les compromis se disent à voix haute. L'adresse en double (idée 7), l'e-mail envoyé deux fois (idée 8), la page d'attente qu'il trouve lui-même « pas jolie » (idée 10). À chaque fois, l'auteur pose le problème, estime le risque et rend la décision au métier. Le livre renvoie le développeur vers l'équipe métier à chaque compromis, et c'est la leçon que je garde. Le ton aide : on lit des pages entières de JavaScript en souriant, ce qui n'est pas rien.
Ses faiblesses sont deux. D'abord, le code a vieilli. C'est du JavaScript de 2020, avec Bluebird et knex (un constructeur de requêtes SQL), déployé sur Heroku, un hébergeur dont l'offre gratuite n'existe plus. Une bonne part des pages est du listing, que vous devrez traduire dans votre langue. La valeur est dans la prose autour.
Ensuite, la montée en charge reste à peine effleurée. Le dernier chapitre ne donne que quelques pages à la concurrence, aux instantanés et à l'évolution des messages (changer le contrat d'un message déjà publié). Pour ces sujets, voici où aller :
- Les écritures concurrentes et la version attendue : l'idée 4 de la fiche Implementing DDD, CQRS and Event Sourcing.
- Les instantanés et la publication fiable des messages, en Go : la fiche Event-Driven Architecture in Golang.
- L'unicité sans transaction distribuée : la fiche Understanding Eventsourcing.
Mon conseil : lisez les chapitres 1 à 7, du premier compteur au premier composant. Enchaînez avec les chapitres 9 à 11 (l'e-mail, les tâches de fond, l'écran asynchrone). Ajoutez le 14 sur les tests. Survolez le 13 (déboguer par les messages) et la section du 12 sur la distribution (idée 11). Sautez le 8 (l'authentification) et le pas-à-pas Heroku du 12. Si vous débutez en Event Sourcing, commencez par celui-ci avant Lawrence : il part du CRUD que vous écrivez déjà.
Odilon
Toujours valable en 2026 ?
Le fond, oui. Une colonne écrasée perd toujours de l'information, une table partagée couple toujours les services, et un message peut toujours arriver deux fois. Rien de cela ne dépend d'une version de Node. La pile, elle, a bougé.
- Heroku a annoncé la fin de ses offres gratuites à partir du 28 novembre 2022 (billet d'Heroku). Le pas-à-pas du déploiement, qui repose sur ces heures gratuites (p. 204), ne se suit plus tel quel.
- Bluebird sert partout dans le code pour les promesses, alors que Node les gère nativement, ce que le livre reconnaît (p. 16). Un lecteur d'aujourd'hui écrira du
async/await, que le livre réserve à la boucle d'abonnement (pp. 72-73). - Message DB existe toujours, et sa fonction d'écriture garde la version attendue de l'idée 3. Son dépôt n'a plus bougé depuis janvier 2023 (version 1.3.0). C'est un outil stable ou en sommeil, à évaluer avant de le mettre en production.
À qui s'adresse-t-il ?
Lisez-le si
- Vous écrivez du CRUD en PHP, Rails ou Node et vos tables partagées ralentissent chaque évolution.
- Vous entendez « Event Sourcing », « CQRS » ou « idempotent » en réunion et voulez voir chaque rouage sans framework.
- Vous hésitez à ajouter Kafka ou une file de jobs à un projet qui a déjà PostgreSQL.
- Vous cherchez des arguments pour discuter des doublons avec votre équipe métier.
Passez si
- Vous cherchez la montée en charge : répartition de la charge, instantanés, évolution des messages.
- Vous voulez du code à reprendre tel quel en 2026 : il faudra tout traduire.
- Vous avez déjà construit un système event-sourcé de bout en bout : vous y retrouverez surtout ce que vous savez.
Pour aller plus loin
- Implementing DDD, CQRS and Event Sourcing : les mêmes briques avec les agrégats du Domain-Driven Design, codées à la main en JavaScript.
- Understanding Eventsourcing : dessiner le système avant le code, et l'unicité par réservation.
- Event-Driven Architecture in Golang : publier un message sans le perdre, dédoublonner à la réception, orchestrer une saga, en Go.
- L'idempotence expliquée, partie 2 : commands, projections et outbox : les gardes d'idempotence de ce livre, transposées en Go.
- Livres Event Sourcing et CQRS : dans quel ordre les lire : où placer ce livre dans un parcours de lecture.
- Message DB : le dépôt et la documentation du magasin utilisé dans le livre.
Commentaires (0)