Event Sourcing

L'état est le résultat des événements

Bootcode IWA-S04 — Semaine 18, Jour 2

Objectifs de la leçon

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

Plan du cours

1

L'analogie clé

Event sourcing = relevé bancaire

2

Snapshot vs Event Sourcing

Comparer deux façons de stocker l'état

3

Le mécanisme

applyChange enregistre ET mute

4

Live coding

BankAccount avec deposit/withdraw

5

Avantages

Audit, historique, reconstruction

Module 1

L'analogie du relevé bancaire

La clé de compréhension de l'event sourcing

Votre relevé bancaire

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.

Qu'est-ce que l'event sourcing ?

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

Snapshot vs Event Sourcing

Deux façons de stocker l'état

Approche classique : le snapshot

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

Approche event sourcing

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

Comparaison directe

❌ 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

Le mécanisme

applyChange & @Handle

Le flux complet

1

L'action est appelée (ex: deposit(100))

2

applyChange crée l'événement et le pousse dans uncommittedChanges

3

@Handle appelle la méthode qui mute l'état

4

Le repository sauvegarde uniquement les événements

applyChange fait DEUX choses

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

Live coding : BankAccount

Un agrégat event-sourcé complet

1. Les événements

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é.

2. L'agrégat BankAccount

// 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;

}

}

3. Le script de test

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

Avantages & Immutabilité

Pourquoi accepter la complexité ?

Les événements sont immuables

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

Les avantages de l'event sourcing

📜

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.

Pièges courants

❌ 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.

À retenir !

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.