Les événements asynchrones

EventDispatcher, EventManager, polling et handlers

Bootcode IWA-S04 — Semaine 20, Jour 4

Objectifs de la leçon

1. Comprendre le flux complet d'un événement de domaine asynchrone

De l'agrégat jusqu'au handler, en passant par la queue Postgres

2. Identifier les handlers existants et les événements qu'ils traitent

Cartographier les handlers du codebase

3. Comprendre le rĂŽle du MessageModule et du polling

Le polling toutes les 3s lit la queue et dispatch aux handlers

4. Savoir créer un nouveau handler pour un événement existant

Classe @injectable() avec une méthode handle(event)

Plan du cours

1

Rappel : événements synchrones vs asynchrones

@Handle dans l'agrégat vs handlers entre composants

2

Le flux complet asynchrone

agrĂ©gat → EventDispatcher → DB queue → polling → EventManager → Handler

3

Lire le code : PostgresEventRepository, MessageModule, EventManager

Les trois briques du systĂšme asynchrone

4

Un vrai handler : UserRoleUpgradedHandler

Classe @injectable() avec une méthode handle(event)

5

Le pattern webhook

PersistWebhook → WebhookPersisted → handler filtre et traite

Synchrone vs asynchrone

Deux types d'événements, deux mécanismes

⚡

Synchrone (@Handle)

Dans l'agrégat. Exécuté immédiatement pendant la commande.

📹

Asynchrone (handlers)

Entre composants. Persisté en DB, traité plus tard par polling.

@Handle — l'Ă©vĂ©nement synchrone

L'agrégat réagit à ses propres événements pendant l'exécution de la commande.

export class UserAggregate extends AggregateRoot {

private email: string;

upgradeRole(newRole: Role) {

this.role = newRole;

this.raise(new UserRoleUpgraded(this.id, newRole));

}

// @Handle = réaction synchrone DANS l'agrégat

@Handle(UserRoleUpgraded)

private onRoleUpgraded(event: UserRoleUpgraded) {

// Mettre Ă  jour un compteur interne, etc.

this.lastRoleChange = new Date();

}

}

💡 @Handle = l'agrĂ©gat se met Ă  jour lui-mĂȘme. Pas de communication entre composants.

Le flux asynchrone complet

De l'agrégat jusqu'au handler, en passant par la queue Postgres.

1ïžâƒŁ AgrĂ©gat

user.upgradeRole(newRole) → raise(UserRoleUpgraded)

2ïžâƒŁ EventDispatcher

Persiste l'événement dans la queue Postgres (table events)

3ïžâƒŁ DB queue (Postgres)

Les Ă©vĂ©nements attendent d'ĂȘtre traitĂ©s (status: pending)

4ïžâƒŁ Polling (toutes les 3s)

Le MessageModule lit les événements pending

5ïžâƒŁ EventManager

Dispatch chaque événement aux handlers enregistrés

6ïžâƒŁ Handler

UserRoleUpgradedHandler.handle(event) → action (email, webhook...)

Pourquoi asynchrone ?

Découplage

L'agrégat ne sait pas qui va traiter l'événement. Il ne connaßt pas les handlers.

Résilience

Si un handler plante, l'événement reste en queue. Il sera retraité au prochain polling.

Performance

La requĂȘte HTTP rĂ©pond immĂ©diatement. Les side effects (email, webhook) se font en arriĂšre-plan.

Idempotence

Chaque événement a un refId unique. Un handler peut détecter les doublons.

PostgresEventRepository

Persiste et lit les événements dans la table events.

export class PostgresEventRepository implements EventRepository {

constructor(@inject(DataSource) private ds: DataSource) {}

async save(event: DomainEvent) {

await this.ds.getRepository(EventEntity)

.save({ refId: event.refId, type: event.type, payload: event, status: 'pending' });

}

async findPending(limit: number) {

return this.ds.getRepository(EventEntity)

.find({ where: { status: 'pending' }, take: limit });

}

}

MessageModule + EventManager

MessageModule

Le poller. Toutes les 3s, lit les événements pending et les passe à l'EventManager.

Marque les événements processing puis done.

EventManager

Le dispatcher. Connaßt la liste des handlers enregistrés par type d'événement.

Pour chaque événement, appelle handler.handle(event).

// MessageModule — la boucle de polling

async poll() {

const events = await this.repo.findPending(50);

for (const event of events) {

await this.eventManager.dispatch(event);

await this.repo.markDone(event.refId);

}

}

// setInterval(poll, 3000) au démarrage

UserRoleUpgradedHandler

Un vrai handler du codebase : classe @injectable() avec une méthode handle(event).

@injectable()

export class UserRoleUpgradedHandler implements DomainEventHandler<UserRoleUpgraded> {

constructor(

@inject(TYPES.Mailer) private mailer: Mailer,

@inject(TYPES.UserRepository) private userRepo: UserRepository,

) {}

async handle(event: UserRoleUpgraded) {

const user = await this.userRepo.findById(event.userId);

await this.mailer.send(user.email, 'Votre rÎle a été mis à jour');

}

}

💡 Le handler reçoit ses dĂ©pendances par injection (Inversify). Il ne connaĂźt pas l'agrĂ©gat qui a Ă©mis l'Ă©vĂ©nement.

Enregistrer un handler

Le handler doit ĂȘtre enregistrĂ© dans l'EventManager via configureHandler.

// Dans le Build du domaine

export class UserBuild implements Build {

build(container: Container) {

// Enregistrer le handler dans le container

container.bind(UserRoleUpgradedHandler).toSelf();

// L'enregistrer dans l'EventManager

const eventManager = container.get(EventManager);

eventManager.configureHandler(

UserRoleUpgraded, // le type d'événement

container.get(UserRoleUpgradedHandler) // l'instance

);

}

}

⚠ Si tu oublies configureHandler, l'Ă©vĂ©nement sera persistĂ© mais jamais traitĂ©. Silencieusement.

Le pattern webhook

Persiste d'abord, traite ensuite — idempotence par refId.

1ïžâƒŁ PersistWebhook (use case)

Reçoit le webhook HTTP. Persiste le payload immédiatement avec un refId unique.

raise(WebhookPersisted) → rĂ©pond 200 OK tout de suite

2ïžâƒŁ WebhookPersisted (Ă©vĂ©nement)

Persisté en queue Postgres. Attend le polling.

3ïžâƒŁ WebhookHandler (handler)

Filtre par type de webhook. Traite seulement si pas déjà traité (refId).

Exécute la logique métier (créer user, mettre à jour commande...)

💡 Pourquoi ? Si le handler plante, le webhook n'est pas perdu. Le refId garantit qu'un retraitement ne crĂ©e pas de doublon.

PiĂšges courants

❌ Confondre @Handle et DomainEventHandler

@Handle = synchrone, dans l'agrégat. DomainEventHandler = asynchrone, entre composants.

⚠ Oublier d'enregistrer le handler

Sans configureHandler dans l'EventManager, l'événement est persisté mais jamais traité.

❌ Ne pas comprendre le polling

Dessiner le flux au tableau : agrĂ©gat → DB → polling (3s) → EventManager → handler.

À retenir !

✅ ÉvĂ©nements synchrones = dans l'agrĂ©gat (@Handle). Asynchrones = entre composants (handlers).

✅ Le polling toutes les 3s lit la queue Postgres et dispatch aux handlers enregistrĂ©s.

✅ Un handler est une classe @injectable() avec une mĂ©thode handle(event).

✅ Le pattern webhook : persiste d'abord, traite ensuite — idempotence par refId.

Demain : le flux complet, de A à Z 🚀