Context Scope

Patterns Event Sourcing : Au-Delà des Bases

📅 Publié le 08 January 2025 • ⏱️ 4 min de lecture

Patterns Event Sourcing : Au-Delà des Bases

Event Sourcing est bien plus qu'un simple pattern de persistance. C'est une approche fondamentalement différente de la gestion d'état qui, bien implémentée, offre des avantages considérables en termes d'auditabilité, de scalabilité et de flexibilité.

Voici les patterns avancés qui font la différence entre un POC et un système de production robuste.

Au-delà du stockage d'événements

La plupart des introductions à Event Sourcing s'arrêtent au stockage d'événements. En production, les défis réels commencent après :

1. Snapshotting intelligent

Rejouer 100 000 événements à chaque reconstruction d'agrégat n'est pas viable. Le snapshotting devient essentiel, mais il faut le faire intelligemment :

public class SnapshotStrategy
{
    // Snapshot tous les N événements, avec backoff exponentiel
    public bool ShouldCreateSnapshot(int eventCount, int lastSnapshotEvent)
    {
        var eventsSinceSnapshot = eventCount - lastSnapshotEvent;
        var threshold = Math.Min(1000, Math.Pow(2, Math.Floor(eventCount / 1000.0)) * 100);
        
        return eventsSinceSnapshot >= threshold;
    }
}

2. Projections résilientes

Les projections ne doivent jamais être un point de défaillance unique. Pattern recommandé :

  • Projections idempotentes : Même événement rejouée = même résultat
  • Checkpoint persistant : Position courante dans le stream d'événements
  • Reconstruction automatique : Capacité à reconstruire depuis zéro
  • Monitoring actif : Alertes sur la lag des projections

Gestion de l'évolution des schémas

L'évolution des événements est inévitable. Voici comment gérer les changements sans casser l'historique :

Versioning d'événements

// Version 1
public record CustomerRegistered(string CustomerId, string Name, string Email);

// Version 2 - Ajout optionnel
public record CustomerRegisteredV2(
    string CustomerId, 
    string Name, 
    string Email, 
    string? PhoneNumber = null
) : CustomerRegistered(CustomerId, Name, Email);

Upcasting pattern

Pour maintenir la compatibilité, implémentez l'upcasting :

public class EventUpcastingService
{
    public IEvent UpcastEvent(StoredEvent storedEvent)
    {
        return storedEvent.EventType switch
        {
            "CustomerRegistered" when storedEvent.Version == 1 
                => UpcastV1ToV3(storedEvent),
            "CustomerRegistered" when storedEvent.Version == 2 
                => UpcastV2ToV3(storedEvent),
            _ => DeserializeEvent(storedEvent)
        };
    }
}

Patterns de performance

Parallel projection processing

Pour les gros volumes, parallélisez intelligemment :

public class ParallelProjectionProcessor
{
    public async Task ProcessEvents(IEnumerable<Event> events)
    {
        // Groupe par aggregate ID pour maintenir l'ordre
        var eventsByAggregate = events.GroupBy(e => e.AggregateId);
        
        // Traite chaque agrégat en parallèle
        await Task.WhenAll(
            eventsByAggregate.Select(ProcessAggregateEvents)
        );
    }
}

Gestion des erreurs et reprise

L'Event Sourcing génère des défis uniques en matière de gestion d'erreurs :

Poison events

Certains événements peuvent systématiquement faire planter vos projections. Pattern de gestion :

public class ResilientProjectionEngine
{
    private readonly IDeadLetterQueue _deadLetterQueue;
    private readonly Dictionary<string, int> _retryCount = new();
    
    public async Task ProcessEvent(Event evt)
    {
        try
        {
            await _projectionHandlers.Handle(evt);
            _retryCount.Remove(evt.Id);
        }
        catch (Exception ex)
        {
            var retries = _retryCount.GetValueOrDefault(evt.Id, 0);
            
            if (retries >= MaxRetries)
            {
                await _deadLetterQueue.Send(evt, ex);
                _retryCount.Remove(evt.Id);
            }
            else
            {
                _retryCount[evt.Id] = retries + 1;
                await ScheduleRetry(evt, retries);
            }
        }
    }
}

Patterns CQRS avancés

Event Sourcing se marie naturellement avec CQRS. Patterns essentiels :

Read model optimization

public class OptimizedCustomerReadModel
{
    // Dénormalisation agressive pour les performances de lecture
    public string CustomerId { get; set; }
    public string FullSearchText { get; set; } // Nom + Email + Téléphone
    public decimal TotalOrderValue { get; set; }
    public int OrderCount { get; set; }
    public DateTime LastOrderDate { get; set; }
    
    // Index pré-calculés
    public string[] Tags { get; set; }
    public CustomerSegment Segment { get; set; }
}

Eventual consistency patterns

Gérer la cohérence éventuelle entre write et read models :

public class EventualConsistencyHandler
{
    public async Task<CommandResult> ExecuteCommand(ICommand command)
    {
        var events = await _commandHandler.Handle(command);
        await _eventStore.AppendEvents(events);
        
        // Retourne immédiatement avec un token de suivi
        return new CommandResult
        {
            Success = true,
            TrackingToken = events.Last().EventId,
            EstimatedConsistencyTime = TimeSpan.FromMilliseconds(500)
        };
    }
    
    public async Task<bool> IsConsistent(string trackingToken)
    {
        return await _readModelStore.HasProcessedEvent(trackingToken);
    }
}

Monitoring et observabilité

L'Event Sourcing nécessite une observabilité spécifique :

Métriques clés

  • Event throughput : Événements/seconde par stream
  • Projection lag : Retard des read models vs write models
  • Snapshot efficiency : Réduction du temps de reconstruction
  • Storage growth : Croissance de l'event store

Health checks

public class EventSourcingHealthCheck : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context)
    {
        var checks = await Task.WhenAll(
            CheckEventStoreHealth(),
            CheckProjectionHealth(),
            CheckSnapshotHealth()
        );
        
        var failures = checks.Where(c => !c.IsHealthy).ToList();
        
        return failures.Any() 
            ? HealthCheckResult.Degraded($"Issues: {string.Join(", ", failures.Select(f => f.Description))}")
            : HealthCheckResult.Healthy();
    }
}

Patterns de migration

Migrer vers Event Sourcing depuis un système traditionnel :

Strangler Fig pattern

public class LegacyEventSourcingAdapter
{
    public async Task MigrateAggregate(string aggregateId)
    {
        // 1. Lit l'état actuel depuis le legacy
        var currentState = await _legacyRepository.GetById(aggregateId);
        
        // 2. Génère un événement de migration
        var migrationEvent = new AggregrateMigrated(aggregateId, currentState, DateTime.UtcNow);
        
        // 3. Stocke dans l'event store
        await _eventStore.AppendEvent(aggregateId, migrationEvent);
        
        // 4. Redirige les futures opérations vers Event Sourcing
        await _routingService.SetRoute(aggregateId, RoutingTarget.EventStore);
    }
}

Conclusion

Event Sourcing en production va bien au-delà du stockage d'événements. Les patterns présentés ici sont le fruit d'années d'expérience sur des systèmes à fort volume.

Les points clés à retenir :

  1. Snapshotting intelligent pour les performances
  2. Projections résilientes pour la fiabilité
  3. Evolution des schémas pour la maintenabilité
  4. Monitoring spécialisé pour l'observabilité

Event Sourcing n'est pas une solution miracle, mais avec ces patterns, vous serez équipés pour construire des systèmes robustes et évolutifs.

📚 À lire aussi

Pattern Repository : Approche Moderne

Repenser le pattern repository pour les applications contemporaines. Quand l'utiliser, comment l'implémenter efficacement, et les alternatives à considérer à l'ère des ORMs et architectures cloud-native.

#tactical #patterns #infrastructure

Aggregates : La Frontière de Cohérence Qu'On Vous Cache

Les livres DDD vous disent de créer des Aggregates pour protéger vos invariants. Ce qu'ils ne vous disent pas : comment savoir où placer la frontière. Entre Aggregates trop gros qui ralentissent tout et Aggregates trop fins qui ne protègent rien, où est le bon curseur ?

#tactical #aggregate #design