Publier des domain events depuis un hexagone

Même si elle est décrite de manière succincte dans Spring Boot Java dans l'hexagone la publication des DomainEvents depuis une architecture hexagonale est un sujet qui mérite un article dédié, dont acte !

C'est quoi un DomainEvent ?

Ce sont des objets du domain model contenant les informations relatives à des événements importants pour le Métier. Comme ils font partie intégrante du domain model, ils sont :

  • Intégrés dans les discussions avec les différents acteurs ;
  • Maîtres de leurs états (ils ne peuvent pas être dans un état impossible pour le Métier) ;
  • Agnostiques à toute solution technique autre que le langage de développement.

Par nature, les événements doivent être publiés (par un publisher) pour ensuite pouvoir être consommés. Si le fait que ces DomainEvents soient générés par les Aggregates fait consensus ce n'est pas le cas pour la manière de les publier.

Publication depuis les Aggregates

Quand on regarde les responsabilités des différents éléments d'un domain model, faire publier les DomainEvents aux Aggregates semble être la solution la plus évidente. Je vois deux manières de le faire :

  • Avec une mécanique de publisher static ;
  • En injectant un port du publisher dans les Aggregates.

Avec un publisher static

Dans Implementing Domain-Driven Design  (pages 297 - 298) Vaughn Vernon définit un DomainEventPublisher static permettant la publication de DomainEvents depuis les Aggregates.

Je ne suis pas d'accord avec l'implémentation en elle-même, celle du livre et celle du repo qui a évolué depuis, mais c'est un détail.

Ce qui me gêne vraiment c'est le design en lui-même de cette solution : passer par un élément static, a fortiori basé sur un ThreadLocal, va coupler trop fortement le domain model à cet élément qui, à mon sens, est de l'infrastructure. Ce couplage va rendre inutilement complexe un nombre important d'opérations, que ce soit pour les tests, le design ou le refactoring.

De manière générale, je limite l'utilisation de static methods à des utilitaires dont je pourrais valider simplement le fonctionnement en testant la method appelante. Je ne me vois pas maintenir une application dans laquelle des appels static doivent être faits dans le bon ordre pour obtenir le résultat attendu.

À mon sens, cette solution n'est pas viable !

En injectant le publisher

Pour que les Aggregates puissent accéder au publisher il faut qu'ils connaissent ce publisher (merci Captain Obvious).

Injecter le port du publisher dans le constructeur de notre Aggregate est une très mauvaise idée : les Aggregates représentent le métier, on doit pouvoir les construire sans avoir besoin de leur injecter un port. D'autant qu'on n’aura pas forcément sous la main une instance de l'adapter au moment de la construction de l'Aggregate. Bref, pas vraiment faisable !

On peut injecter ce port dans les methods de notre Aggregate susceptible de générer des DomainEvents. Et ça fonctionne plutôt bien : notre ApplicationService est un très bon candidat pour avoir une instance du publisher et c'est lui qui fera appel aux methods de l'Aggregate. Ensuite, c'est à l'Aggregate de gérer la publication de ces événements, là aussi, les responsabilités sont claires et logiques même si le fait d'injecter un port en paramètre d'une method Métier peut être discuté.

Pour publier un événement lors d'un lancé (roll) dans la gouttière (gutter) dans une partie de bowling cela pourrait donner, dans l'ApplicationService :

@Service
@Transactional
public class GamesApplicationService {
  private final GamesRepository games;
  private final GameEventsPublisher events;
 
  public GamesApplicationService(GamesRepository games, GameEventsPublisher events) {
    this.games = games;
    this.events = events;
  }
 
  @PreAuthorize("can('roll', #gameId)")
  public Game roll(GameId gameId, int roll) {
    Game game = games.get(gameId).orElseThrow(UnknownGameException::new);
 
    game.roll(roll, events);
 
    games.save(game);
 
    return game;
  }
}

Et dans l'Aggregate Game :

void roll(int pinsDown, GameEventsPublisher events) {
  Assert.notNull("events", events);
  // roll logic
  
  if(pinsDown == 0) {
    events.publish(new Guttered(this.player));
  }
}

Publication depuis l'application service

Avec une équipe, nous étions partis sur l'injection des publishers dans les methods mais l'aspect responsabilités nous gênait : nous avons donc soumis le sujet pendant une session DDD des Software Crafters Lyon.

Une autre idée est sortie de la discussion : on peut faire renvoyer un ou plusieurs DomainEvents par les methods des Aggregates et c'est ensuite l'ApplicationService qui va appeler le port pour faire le dispatch.

C'est la méthode que j'utilise aujourd'hui, même si elle n'est pas parfaite car elle augmente les responsabilités des ApplicationServices, c'est celle qui me plait le mieux !

Pour publier les évènements liés à un lancé (roll) dans une partie de bowling cela pourrait donner :

@Service
@Transactional
public class GamesApplicationService {
  private final GamesRepository games;
  private final GameEventsPublisher events;
 
  public GamesApplicationService(GamesRepository games, GameEventsPublisher events) {
    this.games = games;
    this.events = events;
  }
 
  @PreAuthorize("can('roll', #gameId)")
  public Game roll(GameId gameId, int roll) {
    Game game = games.get(gameId).orElseThrow(UnknownGameException::new);
 
    Collection<Guttered> rollEvents = game.roll(roll);
    events.publish(rollEvents);
 
    games.save(game);
 
    return game;
  }
}

D'autres approches ?

Il existe probablement d'autres manières de faire auxquelles je n'ai pas encore pensé.

L'approche idéale est peut-être juste là, sous mes yeux et je ne la vois pas... N'hésitez pas à le faire savoir en commentaire si vous la connaissez !