Idempotent Sender
L'émetteur qui sait que ses retries ne créeront pas de doublons — le complément indispensable de l'idempotency receiver.
Problème
Un hub EDI envoie un INVOIC à Walmart via AS2. L'envoi part, la
connexion timeout après 60s. Walmart a-t-il reçu ? Le hub
retent. Walmart répond MDN pour les deux envois : l'ERP de
Walmart a deux factures, lance deux paiements identiques. Si on
n'a pas mis d'idempotency
receiver côté Walmart, l'erreur de timeout côté hub fait
payer deux fois. Mais : même si Walmart fait l'effort de
dédoublonner, encore faut-il que le hub envoie deux fois la même clé d'idempotence. Un hub qui regénère un
MessageId aléatoire à chaque retry produit deux
messages « différents » pour Walmart.
Forces
- At-least-once est la sémantique réaliste. Tout réseau distribué retent en cas de doute.
- La clé d'idempotence doit être stable au retry. Si elle change, le receiver ne peut pas dédoublonner.
- La clé doit être persistée avant l'envoi. Si on la regénère à chaque tentative en mémoire, un crash entre deux tentatives détruit la cohérence.
- Le retry peut être déclenché par un autre nœud. En cluster, le node A envoie, crash, node B retent — il doit retrouver la même clé.
Solution
Avant toute tentative d'envoi : (1) Générer une clé
d'idempotence stable et déterministe à partir du domaine : send-{invoiceNumber}-to-{partnerId}, ou ULID
persisté avec la facture en base. (2) Persister la clé + un hash
du payload + status pending dans une table sender_log
en transaction avec la création business. (3) Envoyer le message
en transportant la clé dans un en-tête stable : Message-ID
AS2, RFF EDIFACT, header HTTP Idempotency-Key (IETF
draft) ou X12 BIG02. (4) En cas de retry, lire le sender_log et
réutiliser la même clé, le même hash, sans regénération. (5) Le
receiver, qui a son idempotency
receiver, dédoublonne sur la clé reçue.
Idempotent Sender :
1. Génération clé "send-INV-2026-0001234-attempt-1"
(stable et déterministe, jamais regenerée au retry)
│
▼
2. Persistance clé + payload en DB transaction T1
│ sender_log {
│ key, payload_hash,
│ status='pending',
│ attempt_count, created_at
│ }
▼
3. Envoi AS2 / AS4 / API REST message porte la clé en
Message-ID / RFF
│
▼ ack reçu → status='delivered'
▼ timeout / 5xx → status='pending', attempt++
▼
4. Retry → lookup clé en DB
si status='delivered' → ne pas renvoyer (idempotent)
si status='pending' → renvoyer même message, même clé
Implémentation EDI
Cas concret : hub EDI génère un INVOIC EDIFACT D.96A pour
Walmart. Le pattern Outbox stocke en DB : {invoice_id, message_id: "EDI-INV-2026-0001234-v1", payload_hash: "sha256:abc...", status: "pending", attempt: 0}.
Le sender lit la queue Outbox, envoie via AS2 avec Message-Id: EDI-INV-2026-0001234-v1@hub.local. Si
MDN reçu : status='delivered'. Si timeout : status reste
pending, attempt++. Le retry réutilise exactement la même
Message-Id. Walmart, qui dédoublonne sur Message-Id (cf. RFC 4130
§10), reconnaît le doublon et n'envoie qu'une seule INVOIC à son
ERP. Variante REST : header HTTP Idempotency-Key:
EDI-INV-2026-0001234-v1 (cf. IETF draft Stripe, Square). En
cas de retry, le serveur retourne la réponse cachée. Pour
EDIFACT : utiliser BGM C002 1004 (Document
Number) + UNB 0020 (Interchange Control Reference) —
les deux doivent être stables au retry. AS4 / ebMS3 utilise
eb:MessageId + eb:RefToMessageId pour les
duplicate eliminations.
Anti-patterns
- Clé d'idempotence aléatoire. UUID regénéré à chaque tentative : deux UUID différents = deux messages pour le receiver.
- Clé en mémoire seulement. Si le service redémarre, la clé est perdue, retry produit un doublon.
- Pas de hash du payload. On peut vouloir tester qu'un retry envoie le même payload — si modifié entretemps (bug), détecter et alerter.
- Clé non transportée dans le message. Si le receiver ne voit pas la clé, il ne peut pas dédoublonner.
- Sender qui ignore les ACK. Si on ne lit pas le MDN/CONTRL/200OK, on retent à l'infini. Toujours coupler acquittements et idempotent sender.
Patterns liés
- Idempotency Key (émetteur) — pattern proche, parfois confondu.
- Idempotency (receiver) — pattern complémentaire côté receveur.
- Outbox — typiquement utilisé pour stocker la clé atomiquement avec le business.
- Retry + backoff — le sender retent en réutilisant la clé.
- At-Least-Once — la sémantique que l'idempotent sender rend tolérable.
- Correlation Identifier — souvent la même clé sert aux deux.
Sources
- Kleppmann M. — Designing Data-Intensive Applications, O'Reilly 2017. Chap. 11 « Stream Processing » §« Producer Idempotency ». oreilly.com — DDIA
- RFC 4130 §10 — AS2 Duplicate elimination. La gestion du Message-ID AS2 pour la déduplication. rfc-editor.org/rfc/rfc4130
- IETF draft — The Idempotency-Key HTTP Header Field. Standard émergent pour API REST. datatracker.ietf.org — Idempotency-Key
- Stripe — Idempotent Requests. L'implémentation de référence en API REST commerciale. stripe.com — Idempotent Requests
- OASIS ebMS 3.0 Core Specification — Duplicate
detection. AS4/ebMS3 duplicate elimination via
eb:MessageId. oasis-open.org — ebMS3 - Apache Kafka — Idempotent producer.
enable.idempotence=true, PID + sequence number par partition. kafka.apache.org — Idempotent producer