L'état est le résultat des événements
Bootcode IWA-S04 — Semaine 18, Jour 2
1. L'analogie du relevé bancaire
Comprendre l'event sourcing par une métaphore concrète
2. Snapshot vs Event Sourcing
Différencier état stocké et état calculé
3. applyChange & @Handle
Muter l'état ET enregistrer l'événement
4. Implémenter un agrégat
BankAccount event-sourcé de A à Z
L'analogie clé
Event sourcing = relevé bancaire
Snapshot vs Event Sourcing
Comparer deux façons de stocker l'état
Le mécanisme
applyChange enregistre ET mute
Live coding
BankAccount avec deposit/withdraw
Avantages
Audit, historique, reconstruction
Module 1
La clé de compréhension de l'event sourcing
Chaque ligne est un événement. Le solde est le résultat du calcul.
| Date | Description | Montant | Solde |
|---|---|---|---|
| 01/07 | Dépôt initial | +1000 € | 1000 € |
| 03/07 | Achat courses | -85 € | 915 € |
| 05/07 | Salaire | +2500 € | 3415 € |
| 07/07 | Loyer | -1200 € | 2215 € |
| AUJOURD'HUI | Solde final | 2215 € | |
💡 Le solde n'est pas stocké : il est calculé à partir des transactions.
Au lieu de stocker l'état final, on stocke les événements et on reconstruit l'état en les rejouant.
📜
Événements
Immutables, horodatés, numérotés
🔄
Rejouer
Appliquer les événements un par un
🧮
État calculé
Résultat du replay
Module 2
Deux façons de stocker l'état
On stocke juste l'état final. On perd l'historique.
// Table accounts
{
"id": "acc-123",
"balance": 2215,
"updatedAt": "2024-07-07"
}
✅ Avantages
Simple, rapide à lire, moins de stockage
❌ Inconvénients
Pas d'historique, pas d'audit, pas de reconstruction temporelle
On stocke toutes les transactions. L'état est calculé.
// Table events
[
{ type: "MoneyDeposited", amount: 1000 },
{ type: "MoneyWithdrawn", amount: 85 },
{ type: "MoneyDeposited", amount: 2500 },
{ type: "MoneyWithdrawn", amount: 1200 }
]
// balance = 1000 - 85 + 2500 - 1200 = 2215
✅ Avantages
Historique complet, audit, replay, debug
❌ Inconvénients
Plus de code, plus de stockage, courbe d'apprentissage
❌ Snapshot
{ balance: 2215 }
On ne sait pas d'où vient le solde. Pas d'historique.
✅ Event Sourcing
[+1000, -85, +2500, -1200]
On sait tout. On peut recalculer, auditer, rejouer.
💡 L'event sourcing ne remplace pas la base de données : c'est une façon différente de stocker l'état.
Module 3
applyChange & @Handle
L'action est appelée (ex: deposit(100))
applyChange crée l'événement et le pousse dans uncommittedChanges
@Handle appelle la méthode qui mute l'état
Le repository sauvegarde uniquement les événements
C'est le coeur du mécanisme. Ne jamais l'oublier.
protected applyChange(event: DomainEvent): void {
// 1. ENREGISTRER l'événement
this.uncommittedChanges.push(event);
// 2. MUTER l'état via @Handle
this.handleEvent(event);
}
1. Enregistrer
L'événement est ajouté aux uncommitted changes (à sauver).
2. Muter
L'état de l'agrégat est mis à jour pour refléter l'événement.
Module 4
Un agrégat event-sourcé complet
Nom au passé + payload. Héritent de DomainEvent.
// domain/events.ts
export class MoneyDeposited extends DomainEvent {
constructor(aggregateId: string, amount: number) {
super(aggregateId, { amount });
}
}
export class MoneyWithdrawn extends DomainEvent {
constructor(aggregateId: string, amount: number) {
super(aggregateId, { amount });
}
}
💡 MoneyDeposited, MoneyWithdrawn — tout est au passé.
// domain/BankAccount.ts
export class BankAccount extends AggregateRoot<{ id: string; balance: number }> {
static init(id: string): BankAccount {
const account = new BankAccount({ id, balance: 0 });
account.applyChange(new MoneyDeposited(id, 0));
return account;
}
deposit(amount: number): void {
this.applyChange(new MoneyDeposited(this.props.id, amount));
}
withdraw(amount: number): void {
this.applyChange(new MoneyWithdrawn(this.props.id, amount));
}
@Handle(MoneyDeposited)
onMoneyDeposited(event: MoneyDeposited) {
this.props.balance += event.payload.amount;
}
@Handle(MoneyWithdrawn)
onMoneyWithdrawn(event: MoneyWithdrawn) {
this.props.balance -= event.payload.amount;
}
}
On crée, on agit, on vérifie l'état et les événements non commités.
// main.ts
const account = BankAccount.init("acc-123");
account.deposit(1000);
account.withdraw(85);
account.deposit(2500);
console.log("Solde :", account.props.balance); // 3415
const events = account.getUncommittedEvents();
console.log(events.map(e => e.constructor.name));
// [MoneyDeposited, MoneyWithdrawn, MoneyDeposited]
💡 Le solde est 3415 et il y a 3 événements en attente de persistence.
Module 5
Pourquoi accepter la complexité ?
On ne modifie jamais un événement. On ne le supprime jamais.
🚫
Pas de DELETE
On ne supprime pas le passé
🚫
Pas de UPDATE
On ne modifie pas un événement
➕
Uniquement APPEND
On ajoute un événement compensateur
// Mauvais : on ne corrige JAMAIS un événement passé
// Bon : on ajoute un événement correctif
account.withdraw(85); // erreur !
account.deposit(85); // correction par un nouvel événement
📜
Audit complet
On sait qui a fait quoi, quand, et dans quel ordre.
🕰️
Reconstruction temporelle
"Quel était le solde le 15 juin ?" On peut le recalculer.
🐛
Debug
On peut rejouer la séquence d'événements pour comprendre un bug.
📊
Analytique
Les événements alimentent des projections et des dashboards.
❌ Remplacer la base de données
L'event sourcing ne remplace pas une BDD relationnelle. C'est une façon de stocker.
✅ Compléter la BDD
On stocke les événements comme source de vérité, souvent dans une table dédiée.
❌ Oublier le double rôle d'applyChange
Si on ne mute pas l'état, l'agrégat reste vide. Si on n'enregistre pas, rien n'est persisté.
✅ applyChange = enregistrer + muter
Les deux étapes sont essentielles. L'une sans l'autre ne marche pas.
❌ "C'est plus simple"
Non. C'est plus de code, plus de concepts. C'est le prix de l'audit et de la traçabilité.
✅ Être honnête sur les trade-offs
Plus complexe, mais plus de traçabilité. À utiliser quand l'historique compte.
1. Event sourcing = relevé bancaire
On stocke les transactions, le solde est calculé.
2. applyChange fait 2 choses
Enregistrer l'événement + muter l'état via @Handle.
3. Événements immuables
On ne modifie jamais le passé. On ajoute des événements correcteurs.
4. Trade-off conscient
Plus complexe mais audit, historique et reconstruction possibles.
Demain : on implémente un agrégat Task event-sourcé de A à Z.