Aller au contenu
Outbox Pattern : pourquoi votre EventBus ne suffit pasLecture : 0 %

Outbox Pattern : pourquoi votre EventBus ne suffit pas

Pourquoi publier sur un bus après le commit perd des événements, et comment l'Outbox, l'Inbox, le CDC et la Dead Letter Queue garantissent une livraison au moins une fois.

Par Elias VarenMis à jour le 5 min de lectureMembres

Le serveur a committé la transaction, et l'événement est dans l'EventStore. Le processus essaie de publier vers le broker, et à ce moment précis la machine crashe.

L'événement est persisté, mais le broker ne l'a jamais reçu et les consommateurs ne le verront jamais. Les projections sont désynchronisées, les mails ne partent pas, le service externe ignore que quelque chose s'est passé. Un système dans lequel un événement peut être persisté sans être publié donne aux autres une version fausse de ce qui est arrivé.

Deux écritures, aucune transaction commune

Publier un événement vers un broker externe implique deux opérations distinctes :

  1. Écrire dans l'EventStore, une transaction SQL durable et atomique ;
  2. Publier vers le broker, un appel réseau hors de toute transaction.

Ces deux opérations ne peuvent pas tenir dans la même transaction atomique, sauf à recourir à une transaction distribuée (commit à deux phases) que la plupart des brokers ne proposent pas et que je n'ajouterais pas pour ce problème. Les enchaîner l'une après l'autre ouvre une fenêtre de défaillance entre les deux.

Séquence naïve :
  1. BEGIN
  2. INSERT INTO events ...
  3. COMMIT       ← succès, l'event est en base
  4. broker.publish(event) ← crash ici → event perdu pour toujours

Inverser l'ordre ne fait que déplacer le problème :

Séquence inverse :
  1. broker.publish(event) ← succès
  2. BEGIN
  3. INSERT INTO events ...
  4. COMMIT       ← crash ici → event publié mais jamais persisté

Dans les deux cas, la base de données et le broker sont désynchronisés, et l'un des deux détient une information que l'autre n'a pas.

Les limites d'AsyncEventBus, un bus in-process

Dans l'implémentation de référence, AsyncEventBus est un bus in-process :

TypeScript
// src/core/event/AsyncEventBus.ts
export class AsyncEventBus implements IEventBus {
  async publish(envelopes: EventEnvelope[]): Promise<void> {
    for (const envelope of envelopes) {
      const handlers = this.registry.getHandlers(envelope.eventType);
      for (const handler of handlers) {
        await handler.handle(envelope);
      }
    }
  }
}

Sources et historique

  1. Correction : L'inbox ne rend un doublon sans effet que si son insertion et l'effet du traitement sont commités ensemble ; pour un mail ou un appel à une API externe, il faut une clé d'idempotence. Le texte laissait croire l'inverse.
  2. Correction : Ajout de deux conditions omises : plusieurs pollers en SKIP LOCKED ne préservent pas l'ordre de publication, et le slot de réplication du CDC retient le WAL tant que Debezium ne l'a pas consommé.

Vérification technique : 6 octobre 2026. Une erreur ? Voici comment elle est corrigée.

Le dossier complet : Systèmes event-sourcés

Outbox Pattern : pourquoi votre EventBus ne suffit pas · Deepstack