Transactional Outbox (deep dive)
Une variante détaillée : comment dimensionner la table, choisir polling ou CDC, ordonner, garantir l'idempotence et purger sans perdre l'historique.
Problème
Le pattern Outbox de base résout le dual-write (DB + broker). Mais en production réelle, plusieurs sous-problèmes pratiques émergent : Comment ordonner les messages ? Comment dimensionner le poller pour 10k events/s ? Comment purger sans perdre l'audit ? Comment éviter qu'un retry de poller ne re-publie 100 messages déjà sortis ?
Forces
- Le poller peut être à la traîne — un backlog non-traité accroît la latence end-to-end.
- Plusieurs instances du poller en HA peuvent doublonner si pas de lock.
- L'ordre des évènements compte pour certains agrégats (l'
OrderConfirmeddoit précéderOrderShipped). - La table grossit : sans purge, elle dégrade les requêtes applicatives partageant la même base.
- Un message_id unique est requis pour l'idempotence côté broker.
Solution
L'Outbox détaillé tient en trois rôles distincts : (1) la transaction métier insère dans outbox(id, aggregate_id, payload, status, created_at, headers) ; (2) un relais (polling périodique ou CDC sur le WAL) lit les lignes PENDING et publie ; (3) un purge job archive les lignes SENT plus anciennes que N jours dans un cold storage (S3) puis les supprime. Le relais utilise un SELECT ... FOR UPDATE SKIP LOCKED (Postgres) pour permettre la HA. L'ordre par aggregate_id + created_at est garanti si le broker est partitionné par cette clé.
Schéma de la table outbox
CREATE TABLE outbox (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aggregate_type TEXT NOT NULL, -- 'Order', 'Invoice', ...
aggregate_id TEXT NOT NULL, -- routing key for broker
event_type TEXT NOT NULL, -- 'OrderConfirmed'
payload JSONB NOT NULL,
headers JSONB, -- correlation, source
status TEXT NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING','SENT','FAILED')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
sent_at TIMESTAMPTZ,
attempts INT NOT NULL DEFAULT 0,
next_attempt_at TIMESTAMPTZ
);
CREATE INDEX outbox_pending_idx ON outbox (status, next_attempt_at)
WHERE status = 'PENDING';
CREATE INDEX outbox_agg_idx ON outbox (aggregate_id, created_at); Polling vs CDC
Deux familles de relais existent :
- Polling — un job lit
SELECT * FROM outbox WHERE status = 'PENDING' AND next_attempt_at <= NOW() FOR UPDATE SKIP LOCKED LIMIT 100toutes les 1-2 s. Simple, mais latence ≥ intervalle. Utilisé par les hubs EDI modestes (≤ 100 msg/s). - CDC — Debezium ou un connector Kafka lit le WAL Postgres / binlog MySQL en push. Sub-second latency. La complexité opérationnelle augmente (replication slot, lag monitoring). Standard chez Walmart, Stellantis, Vinted pour les hubs > 1 000 msg/s.
Implémentation EDI
Cas type d'un hub EDI : l'EDIFACT INVOIC arrivant en AS2 est persisté en DB ; dans la même transaction, on insère un record outbox { eventType: 'EdiMessageReceived', payload: { unbRef, sender, recipient, raw: claim_check_url }, headers: { correlationId } }. Debezium publie sur le topic edi.inbound.received. Un consommateur transformation traduit l'INVOIC en JSON canonique ; un consommateur archive stocke en S3 immuable pour l'audit fiscal 10 ans. Aucun risque que la confirmation AS2 (MDN) soit envoyée sans que le hub interne ait journalisé.
Anti-patterns
- Outbox dans la même base que l'application sans isolation — un long poller verrouille les écritures applicatives.
- Pas de
FOR UPDATE SKIP LOCKEDen HA — deux pollers publient le même message. - Pas d'
aggregate_iddans le partitionnement broker — l'ordre se perd. - Purge directe sans archive — l'audit fiscal n'est plus reconstituable.
- Réutilisation du
idoutbox comme message_id sans suffixe attempt — un message déjà SENT puis reset PENDING duplique côté broker.
Patterns liés
- Outbox (architectural) — la version synthétique du pattern.
- CDC pipeline — variante CDC du relais.
- Transactional Inbox — symétrique côté consommateur.
- Idempotency Key (sender) — clé stable pour le broker.
Sources
- Microservices.io — Pattern: Transactional Outbox (Chris Richardson). microservices.io/patterns/data/transactional-outbox.html
- Debezium — Outbox Event Router transformation. debezium.io/documentation/reference/transformations/outbox-event-router.html
- Kleppmann M. — Designing Data-Intensive Applications, O'Reilly 2017, chap. 9 « Consistency and Consensus ».