Aller au contenu

Architecture Event Sourcing

Introduction

Dans une application classique, une ligne d'une table d'une base de données représente généralement l’état actuel d’une entité. Lorsqu’une commande change de statut, on met à jour la colonne correspondante pour la ligne concernée. Nous pouvons savoir qu'elle est désormais expédiée, cependant cette ne ligne ne contient pas les états précédents qui l'ont menées à cet état.

L'architecture Event Sourcing adopte une autre approche : chaque changement significatif est enregistré sous la forme d’un événement. L’état actuel est obtenu en rejouant ces événements dans leur ordre d'apparition. Cette approche est intéressante lorsque l’historique des changements compte autant que la valeur actuelle. Cependant, il apporta des contraintes importantes sur la conception, le stockage des données et leur exploitation.

Présentation

L'architecture Event Sourcing est un modèle de persistance dans lequel est historisé est une suite des événements survenus sur certains objets métier. Appliqué à une commande, chaque événement décrit un fait déjà survenu et s’ajoute au flux de modification du statut de la commande.

Architecture Event Sourcing - Evolution du statut d'une commande

L’architecture Event Sourcing décrit comment conserver l’historique des modifications. Il ne prescrit ni microservices, ni broker de messages, ni séparation obligatoire des bases de lecture et d’écriture.

Comment est réalisée la mise à jour du statut ? Prenons une commande dont le statut est déjà passé de OrderCreated à ItemAdded. Un utilisateur demande sa validation :

  1. L’application charge les événements de cette commande et reconstruit son état.
  2. Elle vérifie les règles métier : la commande existe, contient des articles et n’a pas encore été validée.
  3. Elle ajoute le statut OrderValidated.
  4. Le magasin d’événements ajoute un nouvel événement au flux.
  5. Une vue est actualisée affichant les commandes validées.

Le magasin d’événements est considéré comme la source de vérité et la vue est un état visuel construit à partir de cette source.

Un exemple simple en C#

Voici la logique métier d’une commande. L’exemple illustre la reconstruction de l’état et la production d’événements ; il ne constitue pas à lui seul un magasin d’événements prêt pour la production.

public abstract record OrderEvent(Guid OrderId);

public sealed record OrderCreated(Guid OrderId)
    : OrderEvent(OrderId);

public sealed record ItemAdded(Guid OrderId, string Reference, int Quantity)
    : OrderEvent(OrderId);

public sealed record OrderValidated(Guid OrderId)
    : OrderEvent(OrderId);

public sealed class Order
{
    public Guid Id { get; private set; }
    public int ItemCount { get; private set; }
    public bool IsValidated { get; private set; }

    public static Order Rehydrate(
        IEnumerable<OrderEvent> events)
    {
        var order = new Order();

        foreach (var domainEvent in events)
            order.Apply(domainEvent);

        return order;
    }

    public OrderValidated Validate()
    {
        if (Id == Guid.Empty || ItemCount == 0 || IsValidated)
            throw new InvalidOperationException(
                "La commande ne peut pas être validée.");

        return new OrderValidated(Id);
    }

    public void Apply(OrderEvent domainEvent)
    {
        switch (domainEvent)
        {
            case OrderCreated created:
                if (Id != Guid.Empty)
                    throw new InvalidOperationException("Commande déjà créée.");

                Id = created.OrderId;
                break;

            case ItemAdded added
                when added.OrderId == Id && added.Quantity > 0
                     && !IsValidated:
                ItemCount += added.Quantity;
                break;

            case OrderValidated validated
                when validated.OrderId == Id && !IsValidated:
                IsValidated = true;
                break;

            default:
                throw new InvalidOperationException(
                    "Événement incompatible avec l'état de la commande.");
        }
    }
}

Nous aurions pu utiliser le design pattern [[state]] pour implémenter le passage d'un état à un autre. J'utilise cette implémentation dans cet article pour simplifier le code et mettre l'accent sur l'architecture.

Le traitement charge les événements du flux, appelle la méthode Validate(), puis demande au magasin de persister le nouvel événement. Il est important de noter que le nouvel état métier ne est pris en compte qu’après cet ajout réussi. Dans une implémentation plus complète, on pourrait aussi distinguer les contrôles réalisés sur les commandes des événements historiques rejoués : une ancienne donnée doit rester lisible même si les règles métier évoluent.

Avec SQL Server et Entity Framework Core, une table d’événements pourrait contenir les champs EventId, StreamId, Version, EventType, SchemaVersion, OccurredAt et Payload, avec une contrainte d’unicité sur les champs (StreamId, Version) pour renforcer l'intégrité des données. L’ajout doit être atomique et conditionné à la version attendue du flux. Entity Framework Core peut être utilisé pour enregistrer les événements, mais ses mécanismes classiques de mise à jour d’entités ne constituent pas à eux seuls une solution.

Les avantages

Conserver l’historique

Les événements retracent les changements et leur ordre, ce qui facilite l’analyse et l’audit, à condition de conserver les données associées (métadonnées) nécessaires.

Reconstruire l’état à une date ou une version donnée

Appliquer les événements permet de retrouver un état passé, sous réserve de pouvoir interpréter les anciens événements.

Créer des vues d'affichage des données

Un même historique peut alimenter des vues pour le suivi, les statistiques ou le support, si les événements contiennent les données nécessaires.

Les limites

Une conception parfois complexe

La gestion des événements, des projections et des reprises demande un investissement qui peut être disproportionné pour une simple application.

Des lectures moins directes

Les recherches nécessitent souvent des projections supplémentaires, qui peuvent présenter un léger retard lorsqu’elles sont mises à jour de façon asynchrone.

L’évolution des événements

Les anciens formats doivent rester interprétables, ajoutant de la complexité

Un historique qui grossit

Jouer d'un historique important ralentit la reconstruction de l’état. Des snapshots permettent de réduire le nombre d’événements à appliquer.

Bonnes pratiques avec .NET

Choisir un périmètre métier précis

Commencer par un agrégat réduit dont l’historique apporte une valeur métier.

Décrire des faits métier durables

Nommer les événements selon les faits survenus et conservez les données nécessaires à leur relecture.

Protéger l’ordre et la concurrence

Numéroter les événements par flux et vérifiez la version attendue lors de chaque ajout.

Rendre les projections rejouables et idempotentes

Prévoir leur reconstruction et évitez qu’un événement traité plusieurs fois produise des effets différents.

Tester la relecture et les transitions

Vérifier la reconstruction de l’état et le rejet des commandes invalides.

Mesurer avant d’ajouter des snapshots

Ajouter des snapshots selon la longueur des flux et les durées de reconstruction mesurés.

Quand cette approche est-elle pertinente ?

L’architecture Event Sourcing mérite d’être étudié lorsque l’histoire des actions est un aspect essentiel : opérations financières, parcours de commande, gestion de contrats, gestion des stocks ou processus métier comportant des transitions significatives.

Pour un simple référentiel de données ou une application principalement composée d’écrans CRUD, son coût est souvent difficile à justifier. Une base relationnelle classique, complétée au besoin par un journal d’audit ciblé, peut répondre plus simplement au besoin. Le critère décisif est la valeur métier de l’historique complet, comparée au coût de sa conception et de son utilisaton.