Événements de domaine
VoletTactique Le mécanisme qui rend possible la quatrième règle des agrégats — et, plus largement, le découplage entre contextes.
Un événement de domaine est un fait métier significatif qui s’est produit. Trois caractéristiques, toutes conséquentes :
Il est au passé. ReservationConfirmee, PaiementRefuse, ColisExpedie. Un
nom au présent ou à l’impératif — ConfirmerReservation — désigne une commande,
c’est-à-dire une intention qui peut échouer. Un événement, lui, est déjà arrivé : on
ne le refuse pas, on en tient compte.
Il est immuable. Le passé ne se modifie pas. Une erreur se corrige par un
nouvel événement — ReservationAnnulee — jamais en réécrivant l’ancien.
Il porte les faits, pas des références à résoudre plus tard. L’identifiant de la réservation, le montant, l’instant. Un consommateur doit pouvoir décider sans rappeler l’émetteur.
Pourquoi ils changent tout
Trois bénéfices, du plus local au plus structurant.
Ils rendent explicite ce qui était implicite. Dans un modèle sans événements, le fait qu’une confirmation crédite des points de fidélité est enfoui dans un service. Avec un événement, le fait « une réservation a été confirmée » existe comme concept à part entière, et les réactions qu’il déclenche se lisent séparément.
Ils réalisent la cohérence différée. C’est le mécanisme concret de la règle « une transaction, un agrégat ». L’agrégat modifié enregistre un événement ; un autre morceau du système met à jour les autres agrégats ensuite.
Ils découplent les contextes. Un contexte publie ce qui s’est passé chez lui, sans savoir qui écoute. Le contexte Fidélité peut apparaître, disparaître ou changer de modèle sans que Réservation en sache quoi que ce soit — un renversement total par rapport à un appel direct.
Événement de domaine et événement d’intégration
Distinction essentielle, et rarement faite explicitement. Les confondre produit soit un couplage inter-contextes par les entrailles du modèle, soit une inflation de messages sur le bus.
| Événement de domaine | Événement d’intégration | |
|---|---|---|
| Portée | Interne à un contexte | Publié vers l’extérieur |
| Contenu | Riche, avec les objets du domaine | Plat, types primitifs, sérialisable |
| Contrat | Libre, refactorable à volonté | Stable et versionné |
| Transport | En mémoire, dans le processus | Bus de messages, file |
| Nommage | Le langage du contexte | Le langage publié |
| Exemple | ReservationConfirmee(Reservation, Montant) | reservation.confirmee.v1 {id, clientId, montantCentimes} |
La règle qui en découle : un événement de domaine ne sort jamais du contexte tel quel. Il est traduit en événement d’intégration à la frontière, exactement comme une couche d’anticorruption traduit dans l’autre sens. Sans cette traduction, tout renommage interne casse les consommateurs, et le modèle se fossilise.
Émettre, collecter, publier
L’agrégat enregistre ses événements sans les publier — il n’a pas à connaître de bus. Le cas d’usage les récupère et les publie après validation de la transaction : publier avant reviendrait à annoncer un fait qui pourrait ne jamais avoir eu lieu.
Tip
prelever — et non lire — n’est pas un détail de nommage. Une méthode qui
retourne la liste sans la vider laisse la porte ouverte à une double publication, et
la double publication est le bug le plus pénible de cette famille : le client est
crédité deux fois, et personne ne comprend pourquoi.
Le problème de la double écriture
Voici le piège que presque tout le monde rencontre, et rarement avant la production.
Le cas d’usage ci-dessus fait deux écritures dans deux systèmes différents : la base de données, puis le bus de messages. Ces deux écritures ne sont pas atomiques. Trois issues possibles :
- tout se passe bien ;
- la base est validée, puis le processus meurt avant la publication → l’événement est perdu, le client ne sera jamais crédité, et aucune trace n’existe ;
- l’inverse, si l’on publie d’abord → un événement annonce un fait qui n’a pas eu lieu.
Aucun ordre des deux opérations ne résout le problème. La solution standard est le patron outbox : on écrit l’événement dans une table de la même base, dans la même transaction que l’agrégat. L’atomicité est alors garantie par la base. Un processus séparé lit ensuite cette table et publie sur le bus.
La conséquence à accepter est que la livraison devient au moins une fois : en cas de panne entre la publication et le marquage, l’événement est republié. Il n’y a pas de « exactement une fois » dans un système distribué, et prétendre le contraire coûte plus cher que de l’admettre.
D’où l’obligation symétrique côté consommateur : être idempotent. Le moyen le plus simple est de faire porter un identifiant à chaque événement et de tenir une table des identifiants déjà traités — la règle devenant « si je l’ai déjà vu, je n’en tiens pas compte ».
Warning
Une conséquence souvent découverte trop tard : les événements peuvent arriver
dans le désordre. Un consommateur qui suppose que ReservationConfirmee précède
toujours ReservationAnnulee se trompera. Soit on ordonne par clé de partition —
tous les événements d’une même réservation sur la même partition — soit on rend les
gestionnaires insensibles à l’ordre, ce qui est plus robuste et se conçoit dès le
départ.
Ce que les événements ne sont pas
Ce n’est pas de l’event sourcing. Confusion très fréquente. Publier des événements de domaine consiste à notifier des faits, tout en conservant l’état courant dans une table classique. L’event sourcing est une décision bien plus lourde : le flux d’événements devient la source de vérité, l’état n’est plus stocké mais reconstruit par rejeu. Les deux se combinent, mais l’un ne requiert absolument pas l’autre — et adopter les événements de domaine est infiniment moins engageant.
Ce n’est pas un remplaçant des appels synchrones. Une décision qui a besoin d’une réponse immédiate — autoriser un paiement, vérifier une disponibilité — n’est pas un événement. Tout passer en asynchrone par principe transforme un enchaînement lisible en une chorégraphie que personne ne peut suivre.
Ce n’est pas un journal technique. CacheInvalide ou LigneMiseAJour ne sont
pas des événements de domaine : personne dans le métier ne les nommerait. Le test
est le même que pour le reste du modèle — l’expert reconnaît-il le nom ?
Références
- Vaughn Vernon, Implementing Domain-Driven Design, chapitre 8 — Domain Events
- https://martinfowler.com/eaaDev/DomainEvent.html
- https://microservices.io/patterns/data/transactional-outbox.html
- https://martinfowler.com/articles/201701-event-driven.html — les quatre styles d’architecture événementielle, à lire avant de choisir