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.

— 01

Fondamentaux

— 02

Standards

— 03

Messages phares

— 04

Outils

Transactional Inbox

Le consumer enregistre l'identifiant du message reçu dans une table inbox dans la même transaction que le side effect métier. Si le broker re-livre le message après crash, la requête vu/pas-vu prévient la double exécution.

Problème

Tous les brokers modernes (Kafka, RabbitMQ, SQS, AS4 eDelivery) garantissent au mieux at-least-once : après un crash consumer ou un timeout d'ack, le message est re-livré. Si le consumer traite un événement InvoiceIssued en envoyant un INVOIC à un partenaire et que le process crash après l'envoi mais avant l'ack broker, le retry délivre une seconde INVOIC au partenaire — facture en double. Le pattern Idempotent Receiver au niveau métier (référence contrôle UNB) ne suffit pas : il faut détecter le doublon avant d'exécuter le side effect.

Forces

  • Sémantique broker : at-least-once est universel — exactly-once n'existe pas vraiment, c'est at-least-once + idempotence consumer.
  • Atomicité critique : l'écriture de l'idempotency key et l'exécution du side effect doivent être atomiques sinon le pattern ne tient pas.
  • Espace mémoire : la table inbox grossit linéairement, doit être purgée (typiquement après 30 jours, par event partition).
  • Cardinalité : une inbox par bounded context, indexée par message_id pour lookup O(log n).
  • Performance : une lecture supplémentaire par message — coût acceptable si la table est correctement indexée et purgée.

Solution

Le consumer démarre une transaction DB. Première étape : tenter d'insérer l'id du message dans la table inbox (avec contrainte unique). Si l'insert réussit, c'est un message neuf — exécuter le side effect dans la même transaction, puis commit. Si l'insert échoue (violation de contrainte unique), c'est un doublon — skip le side effect, commit l'ack broker. Le coup de génie : side effect ET marque « déjà traité » sont atomiques par la même transaction DB locale, sans XA.

Structure

Consumer pseudo-code:

while (msg = broker.poll()) {
  BEGIN TX;
  TRY {
    INSERT INTO inbox (message_id, source, received_at)
    VALUES (msg.id, msg.source, now());
    // Succès → message neuf

    // Side effect métier dans la même TX
    INSERT INTO partner_invoices (...) VALUES (...);
    INSERT INTO outbox (...) VALUES (...);

    COMMIT;
    broker.ack(msg);
  } CATCH UNIQUE_VIOLATION {
    // Doublon → skip silencieusement
    ROLLBACK;  // pas de side effect
    broker.ack(msg);  // mais ack quand même
    metrics.duplicates.inc();
  }
}

Implémentation EDI

Pour un hub EDI consumer d'événements Kafka, la table inbox doit identifier de façon unique chaque message livré. Sur Kafka, la clé naturelle est (topic, partition, offset) :

-- Table inbox PostgreSQL
CREATE TABLE inbox (
  message_id     VARCHAR(128) PRIMARY KEY,
  source_topic   VARCHAR(80) NOT NULL,
  source_offset  BIGINT NOT NULL,
  consumer_group VARCHAR(80) NOT NULL,
  payload_hash   VARCHAR(64),         -- SHA-256 du payload pour detect drift
  received_at    TIMESTAMPTZ DEFAULT now(),
  processed_at   TIMESTAMPTZ,
  partition_key  VARCHAR(80)
);
CREATE INDEX inbox_received_at_idx ON inbox (received_at);

-- Purge automatique après 30 jours
DELETE FROM inbox WHERE received_at < now() - INTERVAL '30 days';

-- Consumer transactionnel Node.js (pseudo)
async function processMessage(msg) {
  const tx = await db.beginTransaction();
  try {
    await tx.query(
      'INSERT INTO inbox (message_id, source_topic, source_offset, consumer_group)
       VALUES ($1, $2, $3, $4)',
      [msg.id, msg.topic, msg.offset, 'edi-invoicer']
    );
    // Si on arrive ici, message neuf

    await tx.query('INSERT INTO partner_invoices ...');
    await tx.query('INSERT INTO outbox ...');

    await tx.commit();
    await kafkaConsumer.commitOffsets([msg]);
  } catch (err) {
    await tx.rollback();
    if (err.code === '23505') {  // unique_violation PostgreSQL
      // Doublon connu → ack pour ne pas le revoir
      await kafkaConsumer.commitOffsets([msg]);
      metrics.duplicatesSkipped.inc();
    } else {
      throw err;  // Erreur autre → DLQ
    }
  }
}

Pour les flux EDI ingérés depuis AS4, la MessageId AS4 (UUID dans le header WS-Addressing) est la clé naturelle. Pour EDIFACT, c'est la 0020 Interchange Control Reference du UNB combinée à (0004 Sender Identification, 0010 Recipient Identification). L'inbox doit conserver l'historique au moins aussi longtemps que la fenêtre maximale de retry du partenaire (typiquement 7-30 jours).

Anti-patterns

  • Vérifier l'inbox après le side effect — perd l'atomicité, doublon possible si crash entre side effect et insert inbox.
  • Inbox dans Redis (mémoire volatile) — un crash Redis perd l'idempotence, doublons possibles.
  • Pas de purge — la table grossit linéairement, performance dégrade après quelques mois.
  • Confondre inbox et journal d'audit — l'inbox est tactique (anti-doublon), pas légal (cf Event Sourcing).
  • Ne pas hasher le payload — un attaquant pourrait re-soumettre le même message_id avec un payload différent ; le hash détecte le drift.

Patterns liés

Sources

  • Vasters C. — Idempotent Receiver Pattern, Microsoft Azure Architecture. learn.microsoft.com
  • Helland P. — Idempotence Is Not a Medical Condition, ACM Queue 2012. La référence philosophique du pattern. queue.acm.org
  • Kleppmann M. — Designing Data-Intensive Applications, O'Reilly 2017, §11.5 (« Faults and Idempotence »).
  • Confluent — Exactly-Once Semantics Are Possible: Here's How Kafka Does It, 2017. Détaille pourquoi exactly-once chez Kafka est at-least-once + idempotence consumer.
  • RFC 7240 — HTTP Idempotency Key Header Field (Snell, 2014). Norme HTTP transposable aux brokers messages.