L'event sourcing s'impose comme un modèle architectural puissant dans les systèmes distribués modernes, offrant une piste d'audit immuable et permettant des requêtes temporelles complexes. Cependant, son implémentation efficace soulève des défis majeurs, notamment pour la gestion des projections et la gestion de l'évolution du schéma. Contrairement aux applications CRUD traditionnelles où les données sont modifiées sur place, l'event sourcing vous oblige à reconstruire l'état à partir d'un flux d'événements, rendant la séparation des commandes, des événements et des projections cruciale.
Le défi central : Événements immuables vs Projections modifiables
Dans un système basé sur l'event sourcing, la « source de vérité » est la séquence d'événements. Ces événements doivent rester immuables pour préserver l'intégrité de l'historique du système. Cependant, les vues projetées — les modèles en lecture consommés par votre interface frontend ou votre API — doivent évoluer au fur et à mesure que les exigences métier changent. Cette dichotomie crée un problème d'ingénierie unique : comment mettre à jour vos modèles en lecture sans réécrire l'historique ou provoquer des incohérences de données lors des mises à jour ?
La solution consiste à traiter les projections comme des citoyens de première classe dans votre modèle de données. Elles ne sont pas de simples tables de base de données, mais des composants distincts qui doivent être versionnés, migrés et potentiellement reconstruits.
Gérer l'évolution du schéma dans les événements
L'un des problèmes les plus persistants dans l'event sourcing est la dérive du schéma. Si votre application évolue pour inclure de nouveaux champs dans un événement, comment garantir la rétrocompatibilité avec les services plus anciens qui n'ont pas encore été mis à jour ?
La règle d'or est la suivante : Ne modifiez jamais le nom ou le type d'une propriété d'événement existante. Ajoutez toujours de nouvelles propriétés. Si vous devez modifier une propriété, créez un nouveau type d'événement. Pour faciliter cela, votre schéma d'événement doit inclure un numéro de version.
Considérons un événement simple de création de commande. Dans son état initial, il pourrait ressembler à ceci :
{
"eventId": "123e4567-e89b-12d3-a456-426614174000",
"aggregateId": "order-001",
"timestamp": "2023-10-27T10:00:00Z",
"type": "OrderCreated",
"version": 1,
"customerId": "cust-123",
"totalAmount": 150.00,
"currency": "USD"
}
Plus tard, si vous devez suivre l'adresse de livraison au moment de la création de la commande, vous ne devez pas modifier l'événement existant. Au lieu de cela, vous pourriez introduire un nouveau type d'événement ou ajouter un champ nullable tout en maintenant le support de l'ancienne structure dans votre logique de désérialisation. L'utilisation d'une bibliothèque comme Newtonsoft.Json ou System.Text.Json avec des attributs ignoreDataMember peut aider à gérer ces variations avec élégance.
Concevoir des projections robustes
Les projections sont le mécanisme par lequel nous transformons des événements immuables en modèles en lecture interrogeables. Elles doivent être idempotentes et isolées de la logique métier qui produit les événements.
Un gestionnaire de projection bien conçu doit :
- S'abonner à des types d'événements spécifiques.
- Transformer les données de l'événement dans un format adapté à la base de lecture (souvent une base relationnelle ou documentaire).
- Gérer les événements manquants ou hors ordre avec souplesse.
Voici un exemple conceptuel d'un gestionnaire de projection en C# :
public class OrderTotalProjection : IHandleEvents<OrderCreated>, IHandleEvents<OrderCancelled>
{
private readonly IDocumentSession _session;
public OrderTotalProjection(IDocumentSession session)
{
_session = session;
}
public void Handle(OrderCreated @event)
{
var projection = new OrderProjection
{
OrderId = @event.AggregateId,
Status = "Open",
Total = @event.TotalAmount
};
// Logique d'upsert pour garantir l'idempotence
_session.Store(projection);
}
public void Handle(OrderCancelled @event)
{
var projection = await _session.LoadAsync<OrderProjection>(@event.AggregateId);
if (projection != null)
{
projection.Status = "Cancelled";
projection.Total = 0;
_session.Store(projection);
}
}
}
Migration des projections en production
Lorsque le schéma de votre modèle en lecture change — par exemple, en ajoutant un champ shippingAddress à OrderProjection — vous ne pouvez pas simplement exécuter une migration de base de données. Vous devez également reconstruire les données de projection à partir du flux d'événements. Cela se fait généralement en :
- Déployant le nouveau code de projection aux côtés de l'ancien.
- Exécutant un travailleur en arrière-plan qui rejoue tous les événements pour les agrégats concernés.
- Mettant à jour la base de lecture avec le nouveau schéma.
- Basculant l'API vers la nouvelle vue une fois la rétrocompatibilité (backfill) terminée.
Conclusion
La modélisation des données pour l'event sourcing nécessite un changement d'état d'esprit. Vous ne concevez plus des tables pour le stockage ; vous concevez des flux pour l'historique et des vues pour l'accès. En séparant strictement l'évolution des événements des mises à jour des projections et en garantissant que les projections sont idempotentes et versionnées, vous pouvez construire des systèmes distribués qui sont non seulement résilients face aux changements, mais qui maintiennent également un historique clair et auditable de toutes les activités métier. Embrassez la complexité, et votre système vous récompensera par une flexibilité et une traçabilité sans pareil.