Event Sourcing avec journal EDI append-only
Plutôt que de muter une ligne orders.status='SHIPPED', on persiste un événement OrderShipped immuable dans un journal append-only — l'état courant devient une projection rejouable, et l'historique légal est natif.
Problème
Le flux EDI typique (ORDERS → ORDRSP → DESADV → INVOIC → REMADV) est intrinsèquement
une séquence d'événements métier qui se passent dans le monde réel. Stocker
seulement l'état courant (table orders en CRUD) détruit l'information
historique : pourquoi le statut est-il SHIPPED ? À quelle heure
précise l'INVOIC a-t-il été acquitté ? Quelle version du mapping a généré ce
DESADV ? Quand l'audit fiscal demande des preuves 10 ans après, on n'a plus que
la dernière ligne. Pire, en cas de saga compensée, l'état mutable rend impossible
de reconstruire la version intermédiaire pour comprendre ce qui s'est passé.
Forces
- Conformité fiscale et légale : le code des impôts français et l'EU eIDAS imposent une preuve immuable conservée 10 ans (factures, attestations).
- Audit forensique : en cas de litige partenaire (« vous n'avez jamais reçu mon ORDERS »), il faut prouver l'enchaînement précis des événements.
- Replay et debugging : pouvoir rejouer une saga complète dans un environnement de staging avec les événements exacts de production.
- Projections multiples : l'état courant n'est plus unique — on peut matérialiser différentes vues sans collision.
- Coût stockage : un journal append-only croît linéairement (~5-50 GB par million de messages selon richesse du payload).
Solution
Définir un set restreint d'événements métier (typiquement 20-50 par bounded
context EDI), chacun immuable, daté, versionné, et porteur du payload minimal
décrivant ce qui s'est passé. Le journal est le système de référence : aucune
update, aucun delete. Les agrégats reconstruisent leur état en consommant
séquentiellement les événements qui les concernent (par aggregate_id).
Les vues read-side sont des projections matérialisées asynchrones (cf.
CQRS).
Pour les agrégats lourds (commandes avec milliers d'événements), on insère des
snapshots périodiques.
Structure
Domain events EDI (extrait):
┌─────────────────────────┬────────────────────────┐
│ MessageReceived │ AS4 ingestion │
│ MessageDecoded │ EDIFACT/UBL parsé │
│ MessageValidated │ Schématron OK │
│ MessageMapped │ Canonique généré │
│ OrderReceived │ Commande métier créée │
│ OrderConfirmed │ ORDRSP envoyé │
│ ShipmentDispatched │ DESADV envoyé │
│ InvoiceIssued │ INVOIC envoyé │
│ InvoiceAcked │ REMADV reçu │
│ MessageRejected │ ContractViolation │
│ SagaCompensated │ Étape annulée │
└─────────────────────────┴────────────────────────┘
Stream par agrégat:
order-2026-12345 → [
OrderReceived(t=10:01:23),
OrderValidated(t=10:01:24),
OrderConfirmed(t=10:02:11),
ShipmentDispatched(t=14:33:09),
InvoiceIssued(t=18:45:01),
InvoiceAcked(t=2026-05-21:09:12:55)
] Implémentation EDI
Trois choix de stockage en 2026 : (1) PostgreSQL append-only
(table simple avec contraintes NO UPDATE via row-level security ou
triggers), simple à opérer, requêtable en SQL. (2) Kafka log compacté
avec rétention infinie sur S3 via tiered storage, parfait pour streaming temps réel
mais difficile à requêter ad-hoc. (3) EventStoreDB, base spécialisée
avec API par stream et projections natives — idéal pour event-sourced DDD pur.
-- Schema PostgreSQL append-only EDI
CREATE TABLE edi_event_journal (
event_id UUID PRIMARY KEY,
aggregate_id VARCHAR(80) NOT NULL,
aggregate_type VARCHAR(40) NOT NULL, -- 'Order', 'Shipment', 'Invoice'
sequence_no BIGINT NOT NULL, -- ordre par agrégat (1, 2, 3, ...)
event_type VARCHAR(80) NOT NULL,
payload JSONB NOT NULL,
metadata JSONB NOT NULL, -- correlation_id, user, source_msg
schema_version SMALLINT NOT NULL,
occurred_at TIMESTAMPTZ NOT NULL,
recorded_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (aggregate_id, sequence_no)
);
-- Empêcher updates/deletes (row-level)
REVOKE UPDATE, DELETE ON edi_event_journal FROM PUBLIC;
-- Rebuild d'un agrégat
SELECT event_type, payload, occurred_at
FROM edi_event_journal
WHERE aggregate_id = 'ORDER-2026-12345'
ORDER BY sequence_no; Pour l'archivage fiscal, partitionner la table par mois et déplacer vers un stockage froid (S3 Glacier, Azure Archive) après 13 mois. Les événements y restent accessibles via Athena/BigQuery External Tables. La signature cryptographique (XAdES, eIDAS) des événements à la frontière garantit la non-répudiation et l'intégrité durable.
Anti-patterns
- Stocker le message EDI brut comme « événement » — un événement doit être une intention métier, pas un blob technique. Stocker l'EDIFACT à part dans un object store.
- Versioner les événements en place (
UPDATE) — détruit la propriété append-only et invalide les projections. - Pas de
schema_versiondès le jour 1 — quand la définition d'un événement change (renommer un champ), impossible de rejouer les anciens. - Confondre événement et commande —
OrderShippedest un événement (passé, fait),ShipOrderest une commande (intention non encore réalisée). - Migrer en mode « big bang » d'un système CRUD vers event sourcing — préférer une migration progressive bounded context par bounded context.
Patterns liés
- Event Sourcing (vue architecturale) — la version générique du pattern.
- CQRS pour streams B2B — pattern complémentaire pour exposer les projections.
- Message Store — version EIP du même besoin de persister tout ce qui passe.
- Compensating Transactions — chaque compensation est elle-même un événement dans le journal.
Sources
- Fowler M. — Event Sourcing, martinfowler.com, décembre 2005. La page fondatrice. martinfowler.com/eaaDev/EventSourcing.html
- Young G. — Versioning in an Event Sourced System, Leanpub 2017. Le manuel de référence sur la gestion des versions d'événements.
- Vernon V. — Implementing Domain-Driven Design, Addison-Wesley 2013, chap. 8 (« Domain Events »).
- Stopford B. — Designing Event-Driven Systems, O'Reilly 2018. confluent.io
- Règlement eIDAS 910/2014 — Annexe I sur les signatures électroniques avancées requises pour l'archivage légal des documents B2B. eur-lex.europa.eu