ediverse Explorer la plateforme

À la une PEPPOL BIS Billing 3.0 L’obligation européenne d’e-invoicing arrive : France sept 2026, Belgique janv 2026, Allemagne 2025.

Transactional Inbox (deep dive)

Le pendant consommateur de l'Outbox : on enregistre la clé du message reçu dans une table inbox au sein de la même transaction que ses effets, garantissant qu'un retry n'applique l'effet qu'une fois.

Problème

Tous les brokers modernes (Kafka, RabbitMQ, NATS, SQS) garantissent at-least-once : un message peut être livré plusieurs fois. Si le consommateur écrit en base à chaque réception, un retret broker double l'écriture (deux factures, deux mouvements de stock). Comment garantir que les side effects d'un message soient appliqués au plus une fois ?

Forces

  • Les brokers offrent rarement exactly-once en bout-de-chaîne ; il faut un mécanisme applicatif.
  • Un Set en mémoire ne survit pas aux restarts et ne fonctionne pas en multi-instance.
  • L'écriture inbox + l'écriture métier doivent être atomiques pour la garantie.
  • La table inbox grossit ; il faut une rétention (fenêtre de déduplication).
  • Le message_id doit être unique côté producteur (cf. Idempotency Key).

Solution

Avant tout side effect, le consommateur tente INSERT INTO inbox (message_id, ...) VALUES (...) ; si l'INSERT échoue avec contrainte d'unicité, le message a déjà été traité — on l'ACK sans rien faire. Sinon, on applique l'effet métier dans la même transaction que l'INSERT inbox. Au COMMIT : soit tout est appliqué et journalisé, soit rien. Le broker peut retenter, on est protégé.

Schéma de la table inbox

CREATE TABLE inbox (
  message_id   TEXT PRIMARY KEY,         -- producer-side stable id
  source       TEXT NOT NULL,            -- 'order-service', 'as2-partner-x'
  event_type   TEXT NOT NULL,
  received_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  processed_at TIMESTAMPTZ,
  result       TEXT
);
-- Optional GC partition by month to keep deletes cheap.
CREATE INDEX inbox_received_idx ON inbox (received_at);

Flow de traitement

Broker delivers message (message_id, payload)
        │
        ▼
BEGIN TX
  INSERT INTO inbox (message_id, ...) ── conflict? ──► ROLLBACK + ACK (no-op)
        │ no conflict
  apply business effect (INSERT order, UPDATE stock, ...)
COMMIT  ── success ──► ACK broker
        │ failure
        ▼
   ROLLBACK + NACK (broker retries)

Implémentation EDI

Un récepteur AS2 reçoit deux MDN identiques (le partenaire a renvoyé suite à un timeout). Le consommateur de l'évènement MdnReceived calcule la clé { as2_message_id, mdn_disposition_hash } et tente l'INSERT inbox. Le deuxième passage tombe sur la contrainte d'unicité ; on ACK silencieusement. Conséquence : un seul mouvement de statut « livré » sur la commande, pas deux. Idem pour les CONTRL/997 reçus deux fois après un retry partenaire — l'inbox dédup.

Anti-patterns

  • Inbox sans rétention — la table gonfle, les inserts ralentissent.
  • Inbox sans même transaction que l'effet — un crash entre les deux casse la garantie.
  • Fenêtre d'unicité trop courte — un message rejoué après la fenêtre est réappliqué.
  • message_id non stable côté producteur — un retry producteur produit un id différent, on ne dédup pas.
  • Inbox partagé entre services différents sans préfixe source — collisions accidentelles.

Patterns liés

Sources