1. Service de scénario : chef d’orchestre et moissonneuse-batteuse
Si l’on regarde ContextFlow d’un point de vue architectural, nos OrderPlacementService et OrderCancellationService ne sont pas « des services en général », mais des services de scénarios. Ils dirigent le processus : collectent les données, changent l’état, appellent les dépendances nécessaires. Et c’est normal. Le problème commence lorsque le chef d’orchestre décide soudainement de jouer de tous les instruments de l’orchestre – et en plus de vendre les billets à l’entrée.
Dans une application réelle, un service de scénario démarre presque toujours « proprement » : il fait une ou deux choses, a l’air soigné et inspire même de l’optimisme. Mais ensuite apparaît l’audit (et il est « absolument nécessaire »), puis les notifications (« et si on faisait aussi SMS et email »), puis les statistiques (« il nous faut un rapport sur le tableau de bord »), puis encore un effet secondaire (« c’est rapide, il suffit d’appeler une méthode »). Et voilà que le service de scénario en sait trop et dépend de la moitié de l’application.
Regardons le tableau typique « avant les événements » – lorsque nous appelons tout directement.
Imaginons que le scénario de création d’une commande ressemble à ceci (simplifié, sans détails du domaine) :
public void placeOrder(CreateOrderCommand cmd) {
// 1) Création du modèle de commande à partir de la commande d’entrée
Order order = orderFactory.create(cmd);
// 2) Sauvegarde de la commande – c’est le cœur du scénario (sans cela, le fait "commande créée" n’existe pas)
orderStore.save(order);
// 3) Ensuite viennent les réactions au fait déjà produit (pas le cœur du scénario)
auditService.recordCreated(order.getId());
notificationDispatchService.sendCreated(order.getId());
}
Du point de vue du résultat du scénario métier, nous avons fait « tout bien » : la commande est sauvegardée, l’audit enregistré, la notification envoyée. Mais architecturalement, le problème est déjà caché : OrderPlacementService sait qu’après la création de la commande, il faut obligatoirement appeler l’audit et les notifications, il est donc directement lié à des services spécifiques et à leurs contrats. Tout nouveau « après la création de la commande, il faut aussi… » obligera à modifier ce service.
2. Scénario principal et réactions
Pour comprendre où un événement est approprié, nous devons d’abord nous mettre d’accord sur les termes. Sinon, nous allons débattre dans le style « l’audit, c’est important ou secondaire ? », puis nous ferons de toute façon comme dans la tâche, mais avec un sentiment de culpabilité. Dans cette leçon, nous nous intéressons à une frontière simple et très pratique : ce qui est le cœur du scénario et ce qui est la réaction à un fait déjà accompli.
Par « scénario principal », nous entendrons les étapes sans lesquelles le fait métier n’est pas considéré comme réalisé. Pour la création d’une commande, cela ressemble généralement à : nous avons assemblé la commande, attribué un id, calculé le montant, sauvegardé. On peut discuter de l’ordre et des détails, mais le sens est le même : si la commande n’est pas sauvegardée et n’est pas passée dans l’état attendu, il est trop tôt pour dire « commande créée ».
Par « action secondaire », nous entendrons la réaction à un fait qui a déjà eu lieu. Par exemple, « commande créée » est un fait, tandis que « enregistre l’audit », « notifie le client », « incrémente le compteur de statistiques » sont des réactions. Et ici, il y a une nuance importante : une action secondaire n’est pas forcément « sans importance ». L’audit peut être critique pour le métier, mais il peut néanmoins être une réaction au fait de la création de la commande, et non une partie du mécanisme « créer la commande en tant qu’objet ».
Pour que ce soit plus facile à garder en tête, il est utile de voir une comparaison sous forme de tableau.
| Question | Scénario principal | Action secondaire (réaction) |
|---|---|---|
| De quoi s’agit-il ? | « Faire en sorte que le fait se produise » | « Réagir au fait » |
| Qui doit le savoir ? | Le service de scénario (chef d’orchestre) | N’importe quels « réacteurs » distincts : audit, notifications, statistiques |
| Que se passe-t-il si cela n’est pas fait ? | Le fait ne se produit pas | Le fait s’est produit, mais le système n’a pas réagi complètement |
| Que se passe-t-il généralement lors de l’extension des exigences ? | Change rarement, car le cœur est plus stable | Croît constamment (« encore une notification », « encore un rapport ») |
Dans ContextFlow, les faits naturels autour desquels tout gravite sont « commande créée » et « commande annulée ». Ce sont précisément ces faits qui sont les meilleurs candidats pour être des événements. Pas « nous avons commencé à créer la commande », pas « nous avons mis le statut NEW », pas « nous avons calculé le montant » (bien que dans des domaines complexes, cela puisse aussi arriver), mais des choses compréhensibles qui se lisent comme des affirmations métier achevées.
3. Les appels directs augmentent le couplage
Revenons maintenant à notre « moissonneuse-batteuse » et formulons ce qui commence à faire mal. Important : il ne s’agit pas d’« esthétique » ni d’« architecture propre pour des likes ». La douleur ici est pratique : ces services sont difficiles à modifier, à tester et à étendre sans effets secondaires accidentels. De plus, ils accumulent dépendance sur dépendance, comme un arbre de dépendances qui fait pousser de nouvelles branches – et un jour, vous ne pourrez plus suivre où est le tronc.
Regardons un fragment plus réaliste : le service de scénario connaît le dépôt de commandes, la tarification, l’audit, les notifications, et (à un moment donné) les statistiques.
public void placeOrder(CreateOrderCommand cmd) {
// Cœur du scénario : assembler la commande, calculer le montant, sauvegarder
Order order = orderFactory.create(cmd);
Money total = pricingService.calculateTotal(order);
// Mise à jour du modèle de commande avec le montant calculé
order = order.withTotal(total);
// Sauvegarde – partie de la définition du fait "commande créée"
orderStore.save(order);
// Réactions au fait : elles ne changent pas le résultat "commande créée", mais complètent le système
auditService.recordCreated(order.getId());
notificationDispatchService.sendCreated(order.getId());
statisticsService.onOrderCreated(order.getId());
}
Au niveau « ça marche », c’est parfait. Mais au niveau « architecture », nous avons trois conséquences typiques.
Première conséquence – l’effet boule de neige. Toute nouvelle réaction (« et si on écrivait aussi dans un fichier », « et si on ajoutait aussi au rapport », « et si on ajoutait aussi au message localisé ») entraîne une nouvelle dépendance et un nouvel appel dans cette méthode. Et à un moment donné, la méthode se transforme en série : 12 saisons, 300 épisodes, et vous ne vous souvenez plus qui est le frère de qui.
Deuxième conséquence – un couplage fort : le service de scénario dépend de réactions spécifiques. Même si l’audit et les notifications sont conçus comme de beaux services, le fait même de « qui réagit » est inscrit dans le scénario. Le service de scénario devient l’endroit où « tous les fils se rejoignent ».
Troisième conséquence – la complexité des tests. Pour tester « la création de commande sauvegarde la commande », vous devez soit mettre en place un tas de dépendances, soit simuler (mocker) l’audit, les notifications et les statistiques, alors que le test ne porte pas du tout sur cela. Résultat : les tests deviennent fragiles : on change une réaction – les tests de création de commande cassent.
4. Appel direct : honnête et simple
À ce moment, chez les débutants naît souvent la tentation : « Oh, alors faisons tout avec des événements ! » Et c’est un excellent moyen de remplacer un problème par un autre, simplement plus à la mode. L’appel direct n’est pas un mal, mais un outil d’ingénierie normal. Il est honnête, transparent et se lit bien lorsque la dépendance participe effectivement au résultat principal du scénario.
L’appel direct est particulièrement approprié lorsque le service de scénario ne peut pas terminer son travail sans le résultat de la dépendance. Par exemple, sans OrderStore, la commande ne sera pas sauvegardée. Sans génération d’id, la commande ne deviendra pas un objet valide. Sans calcul du prix, vous ne pourrez pas former le montant final. Ce sont tous des éléments du cœur du scénario – on ne veut pas les cacher derrière des événements, car le scénario deviendrait alors « implicite ».
Dans ContextFlow, le cœur du scénario de création de commande pourrait ressembler à ceci :
public Order placeOrder(CreateOrderCommand cmd) {
// Création de l’objet domaine – partie du scénario
Order order = orderFactory.create(cmd);
// Sauvegarde – partie de la définition du fait "commande créée"
orderStore.save(order);
// Retourne le résultat du scénario au code appelant
return order;
}
On peut discuter de où calculer le prix et quand attribuer l’id, mais le point clé est simple : orderStore.save(order) n’est pas une réaction, c’est l’essence du scénario. Si vous déplacez la « sauvegarde de la commande » dans une réaction à un événement, le scénario se transforme en « nous avons en quelque sorte créé la commande, mais en réalité elle n’est pas encore créée tant que quelqu’un n’a pas fait quelque chose ». Et c’est un brouillard architectural qui n’a l’air beau que dans une présentation.
En fin de compte, la règle utile a l’air ennuyeuse mais fonctionne : si une étape fait partie de la définition du fait (« commande créée »), alors qu’elle reste un appel direct. Si une étape est une réaction au fait (« après la création de la commande, fais encore ceci »), alors c’est un candidat pour le découplage.
5. Événement : un fait et des réactions
Passons maintenant à l’endroit pour lequel les événements sont vraiment conçus. Ils sont utiles là où un seul fait doit déclencher plusieurs réactions, et où le service de scénario ne doit pas connaître ni la liste de ces réactions, ni leur ordre, ni combien il en existe. Le service de scénario doit faire son travail et annoncer : « Le fait s’est produit ». Ensuite, que le système réagisse.
En langage humain, cela ressemble à une annonce au bureau : « Collègues, la commande est créée ». Quelqu’un entend et va enregistrer l’audit. Quelqu’un entend et va envoyer une notification. Quelqu’un entend et met à jour les statistiques. Pendant ce temps, la personne qui a créé la commande ne court pas dans les bureaux en criant « Pierre, enregistre l’audit ! Marie, envoie le SMS ! Vasili, calcule les statistiques ! ». Il a fait l’action et a informé du fait.
Si nous restons dans le monde des appels directs, l’« annonce » ressemble généralement à une chaîne d’appels :
// Cœur du scénario
orderStore.save(order);
// Réactions (actions secondaires) – leur liste et leur ordre sont directement inscrits dans le scénario
auditService.recordCreated(order.getId());
notificationDispatchService.sendCreated(order.getId());
statisticsService.onOrderCreated(order.getId());
En attendant de passer à l’API Spring et en gardant seulement l’idée :
// Cœur du scénario : la commande doit être sauvegardée ici, directement et explicitement
orderStore.save(order);
// Déclaration du fait "commande créée" une seule fois (les réactions commenceront ensuite)
orderEvents.orderCreated(order.getId());
Ensuite, orderEvents n’est pas un « service qui fait tout », mais précisément un mécanisme de distribution du fait à ceux qui sont inscrits pour réagir.
Petit pont : l’utilité des événements sans l’API Spring
Pour voir ce que nous voulons obtenir, on peut imaginer un instant une variante « semi-événementielle » qui ne nécessite aucune magie : on injecte simplement une liste de réactions (nous savons le faire depuis le thème de l’injection de collection).
// Spring peut injecter une liste de tous les beans implémentant OrderCreatedReaction
private final List<OrderCreatedReaction> reactions;
public void placeOrder(CreateOrderCommand cmd) {
// Cœur du scénario
Order order = orderFactory.create(cmd);
orderStore.save(order);
// Réactions : le service de scénario ne sait pas combien il y en a, ni quelles classes exactement
for (OrderCreatedReaction r : reactions) {
// On ne transmet à l’extérieur que le "fait" (données minimales nécessaires à la réaction)
r.react(order.getId());
}
}
Ce ne sont pas encore les événements Spring, mais on voit déjà où nous voulons en venir : OrderPlacementService ne connaît plus les réactions spécifiques. Il sait seulement qu’après la création de la commande, il faut transmettre le fait à l’extérieur, et ensuite le mécanisme standard des événements du conteneur peut prendre en charge ce découplage.
Limites des événements d’application
Il est très important de délimiter immédiatement les frontières, sinon le mot « événements » évoque instantanément l’image de courtiers, de files d’attente, de microservices et de cohérence éternelle. Dans notre cours, il s’agit d’événements d’application à l’intérieur d’un même ApplicationContext, c’est-à-dire au sein d’un seul processus, d’une seule application. Ce n’est pas un système distribué. C’est un moyen d’organiser la communication entre les beans de manière à ce que le scénario ne dépende pas des détails des réactions secondaires.
Il ne faut donc pas attendre des événements d’application ce qu’ils n’ont pas promis. Ils ne transforment pas automatiquement l’application en asynchrone. Ils n’offrent pas les garanties de livraison « comme dans une file d’attente ». Ils ne font pas d’« event sourcing » (et ce n’est pas nécessaire, nous voudrions d’abord créer une commande). Ils permettent simplement de dire : « un fait s’est produit à l’intérieur de l’application » et de permettre à d’autres parties de l’application d’y réagir, sans inscrire ces réactions dans le scénario.
En pratique, cela change la sensation du code. Le service de scénario devient plus court, a moins de dépendances, et l’ajout d’une nouvelle réaction se transforme en « ajouter un nouveau gestionnaire » plutôt qu’en « aller dans le scénario et insérer prudemment un appel supplémentaire sans rien casser ». Et c’est là un bénéfice d’ingénierie réel, pas « nous avons rendu l’architecture à la mode ».
Comment ne pas faire de la soupe d’événements
Les événements ont un revers : il est très facile de les aimer trop fort. C’est comme les épices : une pincée améliore le plat, une demi-boîte transforme tout en un malentendu épicé. Le principal danger du modèle événementiel à l’intérieur d’une application est la perte de transparence : une action ne se produit pas « ici et maintenant », mais « quelque part là-bas », et un débutant ouvre le projet, voit un placeOrder(), et ne comprend pas pourquoi la création d’une commande crée aussi un fichier de rapport et imprime des messages sur la console.
Pour éviter cela, il est utile de maintenir une discipline simple. Un événement est un fait significatif, pas « chaque éternuement à l’intérieur d’une méthode ». Si vous commencez à publier « OrderValidatedEvent », « OrderPriceCalculatedEvent », « OrderSavedEvent », « OrderAuditPlannedEvent » et « OrderNotificationPreparedEvent », vous obtiendrez un système où la logique métier principale s’est dispersée sur une dizaine de gestionnaires, et il est impossible de reconstituer la chaîne dans sa tête sans un débogueur et un café fort.
Une autre erreur fréquente est de faire d’un événement une commande. Quand un événement s’appelle comme un ordre (« SendEmailNowEvent »), c’est presque toujours le signe que vous avez mélangé « fait » et « action ». Un bon événement est au passé : « commande créée », « commande annulée ». Un mauvais événement est à l’impératif : « envoie », « enregistre », « calcule ».
Et enfin : les événements ne sont pas une excuse pour cacher les étapes importantes du scénario. Si une étape détermine de manière critique le résultat (« la commande doit être sauvegardée »), ne la sortez pas dans un événement simplement parce que « c’est plus à la mode ». Laissez le scénario rester honnête et lisible, et que les événements servent les réactions.
6. Erreurs typiques : appel vs événement
Erreur n°1 : Essayer de faire de chaque étape d’une méthode un événement.
Un débutant pense que « événements = beauté architecturale » et se met à publier des événements pour chaque micro‑commande : « objet créé », « prix calculé », « sauvegardé ». Résultat : le scénario principal cesse d’être un scénario et se transforme en un ensemble de signaux. Pour ce premier cours, restons sur une idée simple : nous publions des événements uniquement au niveau des faits métier compréhensibles, par exemple « commande créée » et « commande annulée ».
Erreur n°2 : Transférer dans un événement ce sans quoi le scénario n’est pas terminé.
Parfois, on essaie de « décharger » OrderPlacementService et on va trop loin : on commence à sauvegarder la commande « quelque part dans le gestionnaire d’événement ». Alors l’appel placeOrder() ne garantit plus que la commande a réellement été créée. On obtient un marécage architectural : la méthode s’est terminée, mais le fait n’a pas encore eu lieu. Si une étape définit le fait lui‑même, qu’elle reste un appel direct.
Erreur n°3 : Percevoir l’événement comme une file de messages externe.
Après le mot « événements », beaucoup passent en mode « pensée Kafka » : comme si publier un événement signifiait « envoyé en arrière‑plan ». Dans notre cours, c’est un mécanisme interne d’une seule application, pas un transport distribué. Nous parlerons encore du comportement au runtime, mais il est déjà important de ne pas se faire des attentes qui ne concernent pas les événements d’application.
Erreur n°4 : L’événement est nommé comme une commande, pas comme un fait.
Des noms comme SendEmailEvent sonnent comme « fais une action », pas comme « un fait s’est produit ». En conséquence, les gestionnaires commencent à dépendre de la commande, et le publisher les contrôle en fait via l’événement. Il est plus correct de nommer les événements « au passé » : OrderCreated..., OrderCancelled.... Ainsi l’événement décrit la réalité, il ne donne pas d’ordre.
Erreur n°5 : Le service de scénario commence à « connaître » les écouteurs.
Si vous avez déjà décidé d’utiliser des événements, mais que vous avez quand même laissé dans OrderPlacementService des appels directs « au cas où », alors le découplage ne fonctionne pas. On obtient un hybride : une partie des réactions est cachée dans les écouteurs, une autre dans le service, et le nouveau développeur devra les chercher dans tout le projet. Soit la réaction fait partie du cœur (alors appel direct), soit la réaction est un abonné (alors événement). On ne peut mélanger que consciemment et rarement.
GO TO FULL VERSION