Schema evolution : corriger sans réécrire l'histoire
Quelle stratégie appliquer à chaque type de changement de schéma, comment tester les upcasters, et pourquoi un événement correctif n'est pas un événement compensatoire.
Par Elias VarenMis à jour le 6 min de lectureMembres
Soit le scénario suivant : six mois après le déploiement, le régulateur impose un champ reason obligatoire sur tous les retraits. On ouvre MoneyWithdrawn, on ajoute le champ, on déploie.
Au rebuild suivant, TransactionHistoryProjection casse. Les anciens événements MoneyWithdrawn n'ont pas de reason, le handler accède à event.reason.toUpperCase() et lève TypeError: Cannot read properties of undefined. Si le runner de projection attrape l'erreur sans la remonter, la projection reste dans un état partiel sans qu'aucune alerte ne parte.
La tentation est alors de modifier les événements historiques pour leur donner un reason. Ce serait détruire ce que l'Event Sourcing vous a apporté, puisque c'est la contrainte append-only qui rend le système auditable.
Les événements persistés sont des faits
Un événement dans l'EventStore décrit ce qui s'est produit, au moment où ça s'est produit. MoneyWithdrawn { amount: 300, accountId: "alice" } persisté en 2023 est un fait : 300 € ont été retirés du compte alice à cette date. Le modifier rétroactivement changerait l'histoire.
Une modification directe a des conséquences en cascade :
- tous les snapshots et projections construits depuis cet événement deviennent potentiellement incorrects ;
- l'audit trail ne peut plus être considéré comme fiable ;
- si le store chaîne les événements par hash ou les signe, la modification casse la vérification.
Les changements de schéma d'un événement n'ont pas tous le même profil de risque ; le tableau en fin d'article les range un par un. Deux cas suffisent à poser le problème.
Nouveau champ avec valeur par défaut. C'est le changement le moins risqué : event.reason ?? 'unspecified' dans le handler suffit, rétrocompatible immédiatement et sans migration.
Champ renommé. Les bugs silencieux apparaissent avec lui. Renommer amount en amountCents dans la classe TypeScript sans migration produit des événements historiques avec amount et des nouveaux avec amountCents, et les handlers voient undefined sur tous les événements antérieurs. Je ne fais donc jamais un renommage en place : il passe par un upcaster, qui ajoute le nouveau nom et retire l'ancien à la lecture.
Sources et historique
- Correction : Le rollback d'un upcaster n'est trivial que tant qu'aucun événement n'a été écrit au nouveau format : l'ancien code ne sait pas lire les événements v2. Le texte présentait le rollback comme trivial sans condition.
- Correction : L'exemple d'upcaster remplaçait un montant absent par 0 (un retrait effacé en silence) et mélangeait les deux migrations de la chaîne ; il est scindé en v1→v2 et v2→v3, et un montant illisible lève une erreur.
- Correction : « L'ancien stream ne se supprime jamais » est conditionné : un effacement imposé par le RGPD peut l'exiger si les événements portent des données personnelles.
- Correction : Un échantillon de 1 000 payloads était présenté comme filet de sécurité avant migration sans réserve ; il peut manquer une forme rare, que seul un passage sur tous les événements du type trouve à coup sûr.
Vérification technique : 6 octobre 2026. Une erreur ? Voici comment elle est corrigée.
Le dossier complet : Systèmes event-sourcés
À lire ensuite
Toute la rubrique ArchitectureArchitecture
Ne branchez pas vos effets de bord sur les domain events
Pourquoi un subscriber in-process sur un domain event perd des effets de bord, et comment décider entre subscriber direct, integration event via Outbox ou workflow.
6 minMembres
Architecture
Durable execution : moteur de workflow ou table d'état ?
Un moteur de durable execution et une table d'état résolvent le même problème avec des contraintes opposées, surtout le jour où vous modifiez un workflow dont des instances sont en cours. Les stratégies de versionnement et le critère pour choisir.
8 minMembres
Architecture
Sagas : la compensation que personne ne teste
Une compensation s'exécute rarement, dans le pire état possible, et peut elle-même échouer. Comment énumérer les états de panne partielle d'une saga, ordonner les étapes autour d'un pivot, et tester chaque chemin de retour par injection de pannes.
8 minMembres