Aller au contenu principal
Architecture

Event Sourcing

7 min de lecture
  • architecture
  • design-patterns
  • scalabilité
Event Sourcing : l'historique est la source de vérité

Imaginez une banque qui ne stockerait que votre solde actuel. Pas d'historique de transactions. Pas de relevé. Juste un chiffre : "Votre solde est 1 247,30€." Comment savoir si un prélèvement a été effectué deux fois ? Comment prouver qu'un virement a bien été envoyé ? Impossible. C'est absurde — et pourtant, c'est exactement ce que fait la majorité des applications : écraser l'état précédent à chaque modification.

L'Event Sourcing prend l'approche inverse : stocker chaque événement qui a conduit à l'état actuel.

Le principe

Au lieu de stocker l'état final dans une base de données et de le modifier par des UPDATE, l'Event Sourcing stocke une séquence immuable d'événements. L'état actuel est recalculé en rejouant ces événements.

Approche classique (CRUD) :
  UPDATE comptes SET solde = 1247.30 WHERE id = 42

Event Sourcing :
  CompteOuvert      { id: 42, solde_initial: 0 }
  DépôtEffectué     { id: 42, montant: 2000 }
  RetraitEffectué   { id: 42, montant: 500 }
  VirementReçu      { id: 42, montant: 150 }
  PrélèvementExécuté { id: 42, montant: 402.70 }

  → État recalculé : solde = 0 + 2000 - 500 + 150 - 402.70 = 1247.30

Le résultat est le même. Mais avec l'Event Sourcing, vous savez comment vous êtes arrivé là.

L'Event Store

Les événements sont stockés dans un Event Store — un journal append-only (on ne modifie jamais, on ne supprime jamais, on ajoute toujours).

interface DomainEvent {
  eventId: string;
  aggregateId: string;
  type: string;
  data: Record<string, unknown>;
  timestamp: Date;
  version: number;
}
 
// Exemple d'événements pour un compte bancaire
const events: DomainEvent[] = [
  {
    eventId: "evt-1", aggregateId: "compte-42",
    type: "CompteOuvert",
    data: { titulaire: "Nicolas", soldeInitial: 0 },
    timestamp: new Date("2026-01-15"), version: 1
  },
  {
    eventId: "evt-2", aggregateId: "compte-42",
    type: "DépôtEffectué",
    data: { montant: 2000, origine: "Virement employeur" },
    timestamp: new Date("2026-01-31"), version: 2
  },
  {
    eventId: "evt-3", aggregateId: "compte-42",
    type: "RetraitEffectué",
    data: { montant: 500, distributeur: "ATM-Paris-12" },
    timestamp: new Date("2026-02-03"), version: 3
  }
];

Chaque événement est immuable — une fois écrit, il ne change jamais. C'est la règle fondamentale.

Reconstruire l'état

Pour obtenir l'état actuel d'un agrégat, on rejoue tous ses événements :

class CompteBancaire {
  private solde: number = 0;
  private statut: string = "inactif";
  private titulaire: string = "";
 
  // Appliquer un événement pour mettre à jour l'état
  apply(event: DomainEvent): void {
    switch (event.type) {
      case "CompteOuvert":
        this.titulaire = event.data.titulaire as string;
        this.solde = event.data.soldeInitial as number;
        this.statut = "actif";
        break;
      case "DépôtEffectué":
        this.solde += event.data.montant as number;
        break;
      case "RetraitEffectué":
        this.solde -= event.data.montant as number;
        break;
      case "CompteFermé":
        this.statut = "fermé";
        break;
    }
  }
 
  // Reconstruire depuis l'historique
  static fromEvents(events: DomainEvent[]): CompteBancaire {
    const compte = new CompteBancaire();
    events.forEach(e => compte.apply(e));
    return compte;
  }
}
 
// Reconstruire l'état actuel
const compte = CompteBancaire.fromEvents(events);
// → { solde: 1500, statut: "actif", titulaire: "Nicolas" }

Les bénéfices

Audit complet

Chaque action est tracée. Qui a fait quoi, quand, pourquoi. Pour la finance, la santé, le juridique — partout où la traçabilité est une obligation — l'Event Sourcing est naturel.

Replay et debug

Un bug en production ? Rejouez les événements pour reproduire exactement l'état qui a causé le problème. Pas besoin de deviner — vous avez l'historique complet.

// Reproduire l'état au moment du bug
const eventsUntilBug = events.filter(e => e.timestamp <= bugTimestamp);
const stateAtBug = CompteBancaire.fromEvents(eventsUntilBug);

Voyage dans le temps

Vous pouvez reconstruire l'état du système à n'importe quel moment dans le passé. "Quel était le solde de ce compte le 15 février ?" — filtrez les événements jusqu'à cette date et rejouez.

Nouvelles projections

Besoin d'un nouveau rapport ? D'une nouvelle vue ? Rejouez tous les événements avec une nouvelle logique de projection. Pas besoin de migration de données.

Les Snapshots : optimiser la reconstruction

Rejouer 10 000 événements pour reconstruire un état est lent. La solution : les snapshots — des captures périodiques de l'état.

Événements : [1] [2] [3] ... [5000] [SNAPSHOT: solde=8420] [5001] [5002] ... [5050]

Pour reconstruire l'état actuel :
1. Charger le snapshot (version 5000)
2. Rejouer seulement les événements 5001 à 5050

Au lieu de 5050 événements, vous n'en rejouez que 50.

Event Sourcing + CQRS

L'Event Sourcing se combine naturellement avec CQRS. Les événements alimentent le côté écriture (la source de vérité), et des projections construisent des vues optimisées pour la lecture.

Commande → Aggregate → Événement → Event Store
                                        │
                              ┌─────────┼─────────┐
                              ▼         ▼         ▼
                         Projection  Projection  Projection
                         "Soldes"   "Relevé"    "Stats"
                              │         │         │
                              ▼         ▼         ▼
                          Read DB   Read DB    Read DB

Chaque projection lit les événements et construit une vue dénormalisée adaptée à un besoin de lecture spécifique.

Quand utiliser l'Event Sourcing

SituationEvent Sourcing ?
Audit réglementaire obligatoireOui
Systèmes financiers (transactions, paiements)Oui
Besoin de replay ou voyage dans le tempsOui
Domaine métier complexe avec beaucoup de règlesOui
Application CRUD simpleNon
Équipe peu expérimentéeNon
Données à faible durée de vie (cache, sessions)Non

Les difficultés

La complexité. L'Event Sourcing ajoute une couche d'abstraction significative. Le modèle mental est différent du CRUD classique. L'équipe doit être prête à investir dans cette courbe d'apprentissage.

L'évolution du schéma. Que se passe-t-il quand la structure d'un événement change ? Les anciens événements sont immuables — vous ne pouvez pas les modifier. Il faut gérer le versioning des événements : upcasting, événements de migration.

La cohérence éventuelle. Les projections sont mises à jour de manière asynchrone. Il y a un délai entre l'écriture d'un événement et sa visibilité dans les vues de lecture.

Le volume de données. Un système actif génère des millions d'événements. Le stockage, l'indexation et la performance de replay doivent être planifiés.


L'Event Sourcing change la façon dont vous pensez le stockage : l'historique est la source de vérité, l'état actuel n'en est qu'une projection. Cette approche donne un audit complet, la capacité de rejouer et de débugger, et la flexibilité de créer de nouvelles vues à tout moment. Mais c'est un investissement — réservez-le aux domaines où la traçabilité et le replay justifient la complexité supplémentaire.