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.

Message Deduplication Ledger

Quand 12 microservices d'un hub ingèrent AS2, AS4, SFTP, REST, chacun avec son propre cache de dédup, on finit avec 12 sources de vérité divergentes. Un ledger centralisé partagé corrige le problème — et fournit une vue audit cross-flow.

Problème

Un hub EDI multi-tenant typique 2026 ingère depuis 6 canaux (AS2, AS4, OFTP2, SFTP, PEPPOL, REST API), routé vers 8-12 microservices internes (parser EDIFACT, parser X12, normalizer UBL, validator XSD/Schematron, mapping engine, publish vers Kafka, etc.). Chaque microservice doit dédupliquer ce qu'il reçoit — sinon un message rejoué côté ingestion crée une cascade de doublons en aval. Maintenir 12 caches de dédup par microservice est ingérable : les fenêtres de purge divergent, les schémas diffèrent, l'audit cross-flow est impossible (« ce message a-t-il déjà été traité ailleurs dans le hub ? »).

Forces

  • Cohérence cross-flow : une décision dédup unique partagée par tous les composants du hub.
  • Performance : lookup O(1) attendu — typiquement Redis Cluster pour le hot path, persistance PostgreSQL pour l'audit.
  • Capacité : pour un hub à 100M messages/mois, le ledger doit gérer ~3M ops/s de lecture, ~400 ops/s d'écriture.
  • Disponibilité : le ledger devient un SPOF — il doit être HA (Redis Sentinel/Cluster, PostgreSQL streaming replication).
  • Fenêtre rétention : dépend du flux. AS2 = 7j, AS4 PEPPOL = 30j, X12 = 60j, archive audit = 7-10 ans.

Solution

Définir une API unique dedup.checkAndRegister(scope, key, payload_hash) consommée par tout composant du hub. Le ledger combine deux étages :

  • Hot tier (Redis) : SETNX scope:key avec TTL aligné sur la fenêtre dédup. Retourne NEW ou SEEN. Latence p99 < 1ms.
  • Cold tier (PostgreSQL partitionné par mois) : même clé persistée avec timestamps détaillés, payload hash, source service, scope. Latence p99 ~5ms. Source de vérité auditable.
  • Sync écriture : Redis et PostgreSQL en write-through (Redis d'abord, fallback PostgreSQL si Redis down). Cohérence éventuelle acceptable car la dédup est tolérante : un faux négatif (message vu mais Redis répond NEW) crée un doublon → le consumer downstream a un idempotent receiver.

Structure

                  ┌── Hub EDI ──────────────────────┐
                  │                                  │
   AS4 ingestion ─┼─► dedupClient.check("as4", id) ─┼─► HIT/MISS
   AS2 ingestion ─┼─► dedupClient.check("as2", id) ─┼─►
   PEPPOL inbound ┼─► dedupClient.check("peppol", id)┤
   SFTP poll      ─┼─► dedupClient.check("sftp", id) ┤
   Parser EDIFACT ┼─► dedupClient.check("edifact",ref)┤
   Validator UBL  ─┼─► dedupClient.check("ubl", uuid) ┤
                  │                                  │
                  └──────────┬───────────────────────┘
                             ▼
              ┌──── Deduplication Ledger ────┐
              │                               │
              │  Redis Cluster (hot, 30d TTL) │
              │           │                   │
              │           ▼ writes            │
              │  PostgreSQL (cold, 7y, partitioned)
              │           │                   │
              │           ▼ async S3 archive  │
              │  S3 Glacier (deep archive, audit fiscal)
              └───────────────────────────────┘

Implémentation EDI

// API client (Node.js / TypeScript)
interface DedupResult {
  status: 'NEW' | 'SEEN';
  firstSeenAt?: string;       // ISO 8601
  payloadHashDrift?: boolean; // true si hash différent
  retryCount: number;
}

async function checkAndRegister(
  scope: string,         // "as4", "edifact-invoic", "peppol-ubl", ...
  key: string,           // MessageId AS4, UNB 0020, BT-1 UBL, ...
  payloadSha256: string,
  ttlSeconds = 86400 * 30  // 30 days default
): Promise<DedupResult> {
  // Hot path Redis
  const redisKey = "dedup:" + scope + ":" + key;
  const setResult = await redis.set(
    redisKey, payloadSha256, 'NX', 'EX', ttlSeconds
  );

  if (setResult === 'OK') {
    // NEW: insert async dans cold tier
    queueColdInsert({ scope, key, payloadSha256, firstSeenAt: now() });
    return { status: 'NEW', retryCount: 0 };
  }

  // SEEN: lookup cold pour stats détaillées
  const existing = await pg.query(
    'SELECT first_seen_at, payload_sha256, retry_count
     FROM dedup_ledger WHERE scope = $1 AND key = $2',
    [scope, key]
  );

  await pg.query(
    'UPDATE dedup_ledger SET retry_count = retry_count + 1
     WHERE scope = $1 AND key = $2',
    [scope, key]
  );

  return {
    status: 'SEEN',
    firstSeenAt: existing.rows[0].first_seen_at,
    payloadHashDrift: existing.rows[0].payload_sha256 !== payloadSha256,
    retryCount: existing.rows[0].retry_count + 1
  };
}

-- Schema PostgreSQL partitionné par mois
CREATE TABLE dedup_ledger (
  scope            VARCHAR(40) NOT NULL,
  key              VARCHAR(200) NOT NULL,
  payload_sha256   CHAR(64) NOT NULL,
  source_service   VARCHAR(40),
  first_seen_at    TIMESTAMPTZ NOT NULL,
  last_seen_at     TIMESTAMPTZ DEFAULT now(),
  retry_count      INT DEFAULT 0,
  PRIMARY KEY (scope, key, first_seen_at)
) PARTITION BY RANGE (first_seen_at);

Cas particulier multi-tenant : préfixer chaque scope par tenant_id pour éviter les collisions cross-tenant. scope = "tenant42:as4". Le ledger doit aussi exposer une vue operatorial (« ce message a-t-il transité dans le hub ces 30 derniers jours ? ») via une recherche par (scope, key) ou par payload_sha256.

Anti-patterns

  • Bloomfilter en mémoire process — faux positifs (rejette des vrais messages), perte sur crash, pas auditable.
  • Pas de TTL — Redis explose en mémoire, finit par évictions LRU silencieuses.
  • Ledger sans archive cold — les anciens messages sont purgés, perte d'auditabilité fiscale.
  • Synchrone uniquement vers PostgreSQL — latence p99 ~50ms tue le débit du hub.
  • Pas de monitoring sur le ratio NEW/SEEN — une explosion soudaine de SEEN signale un partenaire en boucle de retry à investiguer urgemment.

Patterns liés

Sources

  • Helland P. — Idempotence Is Not a Medical Condition, ACM Queue, vol. 10 n° 4 (2012). queue.acm.org/detail.cfm?id=2187821
  • Bloom B.H. — Space/Time Trade-offs in Hash Coding with Allowable Errors, CACM 1970. Le papier fondateur des Bloom filters, à connaître pour comprendre pourquoi ne pas les utiliser ici.
  • AWS — Amazon SQS FIFO Queues Deduplication. Documentation du mécanisme dédup SQS 5-minute window. docs.aws.amazon.com
  • Apache Kafka — Idempotent Producer and Exactly-Once Semantics. La référence du producer-side dédup au niveau broker.
  • Brewer E. — CAP Twelve Years Later: How the "Rules" Have Changed, IEEE Computer 2012. Justifie pourquoi un ledger multi-tier est un bon compromis CAP pour la dédup.