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
- Inbox (architectural) — version synthétique.
- Transactional Outbox (deep dive) — pendant côté producteur.
- Idempotent Receiver — pattern parent EIP.
- Idempotence (récepteur) — formulation côté EDI.
Sources
- Microservices.io — Idempotent Consumer pattern (Chris Richardson). microservices.io/patterns/communication-style/idempotent-consumer.html
- Kleppmann M. — Designing Data-Intensive Applications, O'Reilly 2017, chap. 11 « Stream Processing » (section « Fault tolerance »).
- Confluent — Exactly Once Semantics in Kafka. confluent.io — Exactly Once Semantics