Database Engineering

Modélisation des données pour les systèmes basés sur l'Event Sourcing : Gestion de la cohérence temporelle et de l'évolution du schéma

Dans le domaine de l'ingénierie des bases de données modernes, l'Event Sourcing s'est imposé comme un modèle puissant pour construire des systèmes évolutifs, auditables et résilients. Contrairement aux applications CRUD traditionnelles qui stockent l'état actuel des données, les systèmes basés sur l'Event Sourcing stockent la séquence d'événements ayant conduit à cet état. Ce changement offre des avantages profonds, tels que des journaux d'audit complets et la possibilité de rejouer l'historique, mais il introduit une complexité significative lors de la gestion de la cohérence temporelle et de l'évolution du schéma.

Pour les développeurs de niveau intermédiaire à avancé, maîtriser ces défis ne consiste pas seulement à écrire du code ; il s'agit de concevoir un système qui reste cohérent au fur et à mesure de sa croissance. Cet article explore les stratégies de modélisation des données dans les environnements basés sur l'Event Sourcing, garantissant que vos agrégats restent cohérents dans le temps tout en s'adaptant aux exigences métier changeantes.

Le défi central : Évolution des événements vs État stable

La tension fondamentale dans l'Event Sourcing réside dans l'immuabilité des événements et la mutabilité de la logique métier. Une fois qu'un événement est écrit dans le magasin d'événements, il est permanent. Cependant, le modèle de domaine de votre application, les règles de validation et les formats de sérialisation évoluent inévitablement. Si vous mettez à jour votre code pour s'attendre à un nouveau champ dans la charge utile d'un événement qui n'était pas présent dans les événements plus anciens, vous risquez des échecs de désérialisation lors du replay de l'historique.

Pour gérer cela, vous devez découpler le schéma des événements du schéma des projections. Le magasin d'événements est la source de vérité, mais vos modèles de lecture (projections) sont des vues dérivées. Par conséquent, l'évolution du schéma doit se concentrer sur le contrat d'événement, et non sur l'état interne de vos agrégats.

Stratégies pour l'évolution du schéma

Il existe plusieurs modèles établis pour gérer l'évolution du schéma dans les systèmes basés sur l'Event Sourcing. L'approche la plus courante est l'expansion rétrocompatible.

1. Modifications rétrocompatibles

Lors de l'ajout de nouveaux champs à un événement, assurez-vous que les anciennes versions du code consommateur peuvent toujours traiter l'événement en traitant les nouveaux champs comme optionnels ou en fournissant des valeurs par défaut. Ne supprimez jamais ou ne renommez jamais de champs dans les événements existants. À la place, dépréciez-les en les marquant comme inutilisés et introduisez la nouvelle convention de nommage dans les futures versions d'événements.

2. Versionnement des événements

Attribuez un numéro de version à chaque type d'événement. Par exemple, UserCreatedV1 pourrait contenir un champ name, tandis que UserCreatedV2 divise cela en firstName et lastName. Lors de la lecture de l'historique, le consommateur doit acheminer les événements vers le gestionnaire approprié en fonction de la version.

Voici un exemple pratique utilisant une structure conceptuelle de type Python pour gérer les événements versionnés :

class EventDispatcher:
    def dispatch(self, event):
        if event.type == "UserCreated":
            if event.version == 1:
                self.handle_v1(event)
            elif event.version == 2:
                self.handle_v2(event)
        # ... autres types d'événements

    def handle_v1(self, event):
        # Logique héritée pour le champ name
        user = User.create(full_name=event.data['name'])

    def handle_v2(self, event):
        # Nouvelle logique pour firstName et lastName
        user = User.create(
            first_name=event.data['firstName'],
            last_name=event.data['lastName']
        )

Gestion de la cohérence temporelle avec des instantanés

Rejouer des millions d'événements pour reconstruire l'état actuel est coûteux en termes de calcul et peut entraîner des goulots d'étranglement de performance. C'est là que les instantanés (snapshots) entrent en jeu. Un instantané capture l'état d'un agrégat à un moment donné (par exemple, tous les 100 événements).

Cependant, les instantanés introduisent un nouveau défi de cohérence : la cohérence temporelle. Si vous prenez un instantané d'un agrégat qui est modifié simultanément, ou si vous restaurez un instantané qui est plus ancien que les événements actuellement traités, vous pouvez rencontrer des conditions de concurrence ou des états incohérents.

Pour maintenir la cohérence temporelle :

  • Instantanés versionnés : Associez toujours un numéro de version à un instantané. Lors du chargement d'un agrégat, chargez le dernier instantané jusqu'à la version actuelle, puis rejouez uniquement les événements survenus après cet instantané.
  • Validation des mutations : Lors de l'application des événements à un instantané, vérifiez que la version de l'événement est strictement supérieure à la version de l'instantané pour éviter d'écraser ou de manquer des mises à jour.

Exemple pratique : Implémentation d'un gestionnaire d'instantanés

Voici une implémentation conceptuelle simplifiée d'un gestionnaire d'instantanés qui assure la cohérence temporelle en suivant les séquences d'événements.

class SnapshotManager:
    def __init__(self, event_store):
        self.event_store = event_store

    def save_snapshot(self, aggregate_id, state, event_count):
        """Sauvegarde l'état actuel de l'agrégat."""
        self.db.save({
            'aggregate_id': aggregate_id,
            'state': state,
            'last_event_id': event_count,
            'timestamp': datetime.now()
        })

    def load_aggregate(self, aggregate_id, factory):
        """Charge l'agrégat depuis l'instantané et rejoue les événements manquants."""
        snapshot = self.db.find(aggregate_id)
        
        if snapshot:
            aggregate = factory.create_empty()
            aggregate.restore_snapshot(snapshot['state'])
            last_known_event = snapshot['last_event_id']
        else:
            aggregate = factory.create_empty()
            last_known_event = 0

        # Rejoue uniquement les événements survenus après le dernier instantané
        events = self.event_store.get_since(last_known_event, aggregate_id)
        for event in events:
            aggregate.apply_event(event)
            
        return aggregate

Conclusion

La modélisation des données pour les systèmes basés sur l'Event Sourcing nécessite un changement de paradigme, passant de schémas statiques à des contrats évolutifs. En adoptant des définitions d'événements rétrocompatibles, en mettant en œuvre un versionnement robuste et en utilisant des instantanés versionnés, vous pouvez garantir que votre système reste cohérent et performant. Rappelez-vous, dans l'Event Sourcing, le passé est immuable, mais votre capacité à l'interpréter doit être flexible. Concevez vos modèles pour résister à l'épreuve du temps, et votre application évoluera avec confiance.

Lorsque vous mettez en œuvre ces modèles, n'oubliez jamais que l'objectif n'est pas seulement de stocker des données, mais de capturer l'histoire de votre entreprise. Un système basé sur l'Event Sourcing bien modélisé fournit cette histoire avec une haute fidélité, vous permettant de répondre aujourd'hui à des questions que vous ne saviez même pas poser hier.

Share: