Event Storming : du mur de post-its au code .NET
L’Event Storming expliqué avec le back-office d’une guilde d’aventuriers : des événements collés sur un mur, des désaccords qui sortent avant le sprint, et chaque couleur de post-it qui retrouve sa place dans le code.
Avant de coder un nouveau domaine, je veux voir le métier coller des post-its orange sur un mur. C’est le principe de l’Event Storming, et je ne connais pas de meilleur point de départ pour un projet métier.
Le souci, c’est que la plupart des articles s’arrêtent au mur. On voit de belles photos de post-its, tout le monde a l’air content, et puis… comment on passe de ça à du code ? C’est ce que je veux te montrer ici : chaque couleur de post-it a sa place dans le code, et une fois que tu le sais, le mur devient ton plan de travail.
On reprend la guilde d’aventuriers de mon article sur la vertical slice : un tableau de quêtes, des aventuriers classés par rang, de l’or à la clé. Sauf que cette fois, on commence par le mur.
L’Event Storming en deux mots
La méthode a été inventée par Alberto Brandolini en 2013. Le principe tient en une phrase : tu réunis les gens du métier et les devs devant un très grand mur, et tout le monde raconte le fonctionnement du business sous forme d’événements, écrits au passé sur des post-its orange.
Un événement, c’est quelque chose qui s’est passé et qui compte pour le métier : « Quête publiée », « Récompense versée ». Le passé oblige à parler de faits plutôt que d’écrans. Personne ne colle « Cliquer sur valider », on colle « Quête acceptée ».
Côté matériel, il te faut un long rouleau de papier, des post-its et un feutre par personne. Brandolini conseille même de retirer les chaises : debout, les gens bougent, se mélangent et discutent beaucoup plus.
La méthode existe en trois formats, du plus large au plus précis. Le Big Picture met tout le business sur un mur, avec un maximum de monde, pour comprendre l’ensemble. Le Process Modeling zoome sur un processus, de bout en bout, avec ses règles et ses décisions. Le Software Design descend jusqu’au modèle, pour préparer le code. On va faire les trois sur la guilde, en accéléré.
Premier passage : on raconte l’histoire
Au début, c’est le chaos, et c’est voulu. Chacun écrit ses événements dans son coin et les colle à peu près là où il pense qu’ils vont. Le trésorier de la guilde colle ce qui touche à l’or, le recruteur ce qui touche aux aventuriers, et le maître de guilde un peu de tout.
Ensuite, on remet de l’ordre : on aligne tout sur une seule ligne du temps, de gauche à droite, et on supprime les doublons. Pour la guilde, ça donne à peu près ça :
Aventurier inscrit
→ Quête publiée
→ Quête acceptée
→ Trophée rapporté
→ Quête terminée
→ Récompense versée
→ Aventurier promuPour arriver à cette ligne, il a pourtant fallu régler une dispute.
Les désaccords sortent sur le mur
Au premier passage, il n’y avait pas de « Trophée rapporté ». Juste un post-it « Quête terminée », que deux personnes avaient collé à deux endroits différents.
Pour l’aventurier, la quête est terminée quand le dragon est mort. Pour le trésorier, elle est terminée quand le maître de guilde a vérifié le trophée, parce que c’est à ce moment-là qu’il ouvre le coffre. Même mot, deux moments différents sur le mur.
Sans atelier, c’est un dev qui l’aurait découvert, en plein sprint, en codant le versement de la récompense. Ou pire, il ne l’aurait pas vu, et la guilde aurait payé ses aventuriers sur parole.
Sur le mur, ce genre de désaccord se marque avec un post-it rose fluo : un hot spot, une question que le métier doit trancher. Ici, le maître de guilde a tranché en séparant les deux moments. « Trophée rapporté » quand l’aventurier revient, et « Quête terminée » quand le maître de guilde a validé le trophée. L’or ne sort du coffre qu’après le second.
Tous les hot spots ne sont pas des disputes. Certains sont juste des questions que personne ne s’était posées. Sur le mur de la guilde, quelqu’un a demandé : « Un aventurier blessé peut-il accepter une quête ? » Silence dans la pièce, puis le maître de guilde a répondu non. Garde ce post-it en tête, il va revenir dans le code.
Deuxième passage : qui déclenche quoi
Une fois l’histoire claire, on ajoute ce qui provoque chaque événement. C’est là que les autres couleurs arrivent :
| Couleur | Ce que c’est | Dans la guilde |
|---|---|---|
| Orange | L’événement, au passé | « Quête acceptée » |
| Bleu | La commande, l’intention qui déclenche l’événement | « Accepter la quête » |
| Petit jaune | L’acteur, celui qui lance la commande | L’aventurier |
| Vert | Le read model, l’info dont l’acteur a besoin pour décider | Le tableau des quêtes |
| Grand jaune | L’aggregate, celui qui fait respecter les règles | La quête |
| Lilas | La policy, une réaction automatique : « quand X, alors Y » | « Quand une quête est terminée, mettre à jour le palmarès de l’aventurier » |
| Rose fluo | Le hot spot, une question ou un désaccord | « Un aventurier blessé peut-il accepter une quête ? » |
Les couleurs varient un peu d’une équipe à l’autre. Peu importe, tant que la légende est collée sur le mur et que tout le monde s’y tient.
Mis bout à bout, le cas « accepter une quête » se lit comme une phrase :
Acteur : Aventurier
Read model : Tableau des quêtes
Commande : Accepter la quête
Aggregate : Quête
Événement : Quête acceptéeL’aventurier consulte le tableau des quêtes et décide d’en accepter une. La quête vérifie qu’il en a le droit, et l’événement « Quête acceptée » se produit.
Du mur au code
C’est la partie que je préfère. Si tu as lu l’article sur la vertical slice, tu vas reconnaître pas mal de choses.
Une commande, une slice
Chaque post-it bleu devient une slice d’écriture, et chaque read model vert une slice de lecture. Mets le mur et l’arborescence côte à côte :
Features/
├── Quests/
│ ├── PostQuest/ ← « Publier une quête »
│ ├── AcceptQuest/ ← « Accepter la quête »
│ ├── ReportTrophy/ ← « Rapporter le trophée »
│ ├── CompleteQuest/ ← « Terminer la quête »
│ └── ListQuests/ ← read model « Tableau des quêtes »
└── Adventurers/
├── RegisterAdventurer/ ← « Inscrire un aventurier »
└── UpdateAdventurerRecord/ ← policy « Mettre à jour le palmarès »Tu lis les post-its bleus et verts, tu as la liste des dossiers à créer. Et dans l’autre sens, quelqu’un du métier qui ouvre Features/ retrouve son mur.
L’aggregate fait respecter les règles et produit l’événement
Le grand post-it jaune « Quête » devient l’aggregate Quest. C’est lui qui garde les règles, y compris celle du hot spot de l’aventurier blessé. Et quand tout est bon, il lève l’événement orange :
public sealed record QuestAccepted(Guid QuestId, Guid AdventurerId) : IDomainEvent;public void AcceptBy(Adventurer adventurer, DateTimeOffset acceptedAt)
{
if (Status != QuestStatus.Posted)
throw new DomainException(QuestErrors.NotOnTheBoard);
if (adventurer.Rank < RequiredRank)
throw new DomainException(QuestErrors.RankTooLow);
// Le hot spot du mur, tranché par le maître de guilde
if (adventurer.IsInjured)
throw new DomainException(QuestErrors.AdventurerInjured);
Status = QuestStatus.Accepted;
AdventurerId = adventurer.Id;
AcceptedAt = acceptedAt;
AddDomainEvent(new QuestAccepted(Id, adventurer.Id));
}Le post-it « Quête acceptée » est devenu QuestAccepted, avec le même sens, juste traduit. Quand le maître de guilde parle d’une quête acceptée, le dev sait exactement de quelle ligne de code il s’agit.
Une policy, un handler
Le post-it lilas « Quand une quête est terminée, mettre à jour le palmarès de l’aventurier » devient un handler qui réagit à l’événement QuestCompleted. Avec Wolverine, c’est une simple méthode Handle :
public static class UpdateAdventurerRecordHandler
{
public static async Task Handle(QuestCompleted @event, AppDbContext db, CancellationToken ct)
{
var adventurer = await db.Adventurers.SingleAsync(x => x.Id == @event.AdventurerId, ct);
// La règle de promotion vit dans l'aggregate, la policy se contente de la déclencher
adventurer.RecordCompletedQuest(@event.QuestRank);
}
}Si l’aventurier a assez de quêtes de son rang à son actif, RecordCompletedQuest lève à son tour l’événement AdventurerPromoted. On retrouve la fin de la ligne du temps du mur : « Quête terminée », puis « Aventurier promu ».
C’est aussi comme ça que deux slices collaborent sans s’appeler. CompleteQuest ne sait même pas que le palmarès existe : elle termine la quête, l’événement part, et la policy fait le reste. La règle « une slice n’appelle jamais une autre slice » tient sans effort.
Un hot spot, un test
Le post-it rose de l’aventurier blessé a une dernière vie : il devient un test. La question posée en atelier donne le cas à tester, et la réponse du maître de guilde donne ce que le test vérifie :
[Fact]
public void AcceptBy_ShouldRefuse_WhenAdventurerIsInjured()
{
var quest = Quest.Post("Retrouver le chat de la boulangère", requiredRank: Rank.E, reward: 5);
var gerard = Adventurer.Register("Gérard", Rank.E);
gerard.Injure();
var act = () => quest.AcceptBy(gerard, DateTimeOffset.UtcNow);
act.Should().Throw<DomainException>()
.Which.Error.Should().Be(QuestErrors.AdventurerInjured);
}Oui, c’est encore Gérard. Il avait enfin trouvé une quête de son rang, et il s’est fait une entorse en descendant de son cheval.
Même mot, deux sens : parfois, il n’y a rien à trancher
Attention au réflexe de vouloir tout résoudre. Pour « Quête terminée », il fallait trancher : c’est la même quête, dans le même processus, et l’or ne sort du coffre qu’à un seul moment. Mais parfois, deux personnes utilisent le même mot pour deux choses différentes, et elles ont raison toutes les deux.
Le grand classique, c’est le mot « client ». Pour le commercial, le client existe dès le premier appel : il a un interlocuteur, un historique d’échanges, des devis en cours. Pour la compta, il existe à la première facture : il a un numéro de TVA, des conditions de paiement, des factures à relancer. Les deux définitions sont justes. Si tu forces tout le monde à se mettre d’accord sur une seule, tu finis avec une classe Customer de quarante propriétés, dont la moitié ne sert qu’au commercial et l’autre moitié qu’à la compta.
La bonne réponse, ce sont deux bounded contexts. Un bounded context, c’est une zone du métier où chaque mot a un seul sens. Dans le contexte Ventes, « client » veut dire ce que dit le commercial. Dans le contexte Comptabilité, ce que dit la compta. Chacun a son propre modèle, et ce n’est pas de la duplication.
Sur le mur, ça se repère au vocabulaire. Dans la guilde, le recruteur parle d’inscription, de rang et de blessures. Le trésorier parle de solde et de versements. Les deux disent « aventurier », sans parler de la même chose. Pour le recruteur, un aventurier, c’est un rang, un palmarès et un état de santé. Pour le trésorier, c’est quelqu’un à qui la guilde doit de l’or.
Dans le code, chaque contexte a donc son propre Adventurer :
namespace Guild.Recruitment.Domain;
public sealed class Adventurer
{
public Guid Id { get; private set; }
public string Name { get; private set; } = null!;
public Rank Rank { get; private set; }
public bool IsInjured { get; private set; }
public int CompletedQuestsAtRank { get; private set; }
}namespace Guild.Treasury.Domain;
public sealed class Adventurer
{
private readonly List<Payout> _payouts = [];
// Le même identifiant que côté recrutement, et c'est tout ce qu'ils partagent
public Guid Id { get; private set; }
public decimal Balance { get; private set; }
public IReadOnlyList<Payout> Payouts => _payouts;
}Le lien entre les deux, c’est l’identifiant et les événements. Quand un aventurier s’inscrit, le recrutement lève AdventurerRegistered, et une policy côté trésorerie lui ouvre un compte. Sur le mur, c’est un post-it lilas posé pile à la frontière : « Quand un aventurier est inscrit, lui ouvrir un compte ». Le trésorier n’a jamais besoin de savoir si Gérard est blessé, et le recruteur se fiche de son solde.
Le hot spot reste utile dans ce cas : il te signale que le mot change de sens à cet endroit du mur. Simplement, la question à poser n’est plus « qui a raison ? », mais « où passe la frontière ? ». Tu n’es pas obligé d’en faire des modules séparés dès le premier jour. Garde au moins un dossier par contexte, et ne laisse jamais un contexte aller lire les classes de l’autre.
Ce qu’il faut savoir avant de te lancer
- Sans les bonnes personnes, ça ne sert à rien. Un Event Storming entre devs, ce sont des devs qui devinent le métier en groupe. Il te faut ceux qui connaissent le business, et idéalement ceux qui le vivent au quotidien, pas seulement leur chef.
- Il faut quelqu’un pour animer. Sans animateur, la personne la plus bavarde raconte sa version et les autres hochent la tête. L’animateur pose les questions, fait coller les hot spots et protège la ligne du temps.
- Ne descends pas trop vite dans le détail. Si les devs dessinent des aggregates pendant que le métier raconte encore son histoire, tu perds le métier. Le format Software Design vient après.
- Le mur n’est pas une spec. Il sert à faire parler les gens. Prends des photos, puis reporte les événements, les règles et les décisions dans ta doc, idéalement à côté du code.
- En visio, ça marche moins bien. Un tableau blanc en ligne comme Miro fait le job, mais tu perds l’énergie du mur. Prévois des sessions plus courtes.
- Sur une appli CRUD, c’est du temps perdu. Cinq formulaires de saisie sans règle métier, tu n’as pas besoin d’un atelier pour les comprendre.
Le mot de la fin
L’Event Storming, c’est une après-midi debout devant un mur. Tu en repars avec tes slices, tes aggregates, tes événements et les frontières entre tes contextes. Et surtout avec les questions que le métier doit trancher avant que tu écrives la première ligne, en sachant lesquelles n’ont pas besoin de l’être.
Et quand le métier change d’avis six mois plus tard, tu sais exactement quel post-it a bougé, et donc quel dossier ouvrir.
Si tu veux un coup de main pour passer de ton mur de post-its à ton code, ou un avis sur ton découpage, écris-moi. On en parle volontiers.