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.
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.
La plupart des introductions à Event Sourcing s'arrêtent au stockage d'événements. En production, les défis réels commencent après :
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;
}
}
Les projections ne doivent jamais être un point de défaillance unique. Pattern recommandé :
L'évolution des événements est inévitable. Voici comment gérer les changements sans casser l'historique :
// 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);
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)
};
}
}
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)
);
}
}
L'Event Sourcing génère des défis uniques en matière de gestion d'erreurs :
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);
}
}
}
}
Event Sourcing se marie naturellement avec CQRS. Patterns essentiels :
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; }
}
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);
}
}
L'Event Sourcing nécessite une observabilité spécifique :
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();
}
}
Migrer vers Event Sourcing depuis un système traditionnel :
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);
}
}
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 :
Event Sourcing n'est pas une solution miracle, mais avec ces patterns, vous serez équipés pour construire des systèmes robustes et évolutifs.
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.
Comment concevoir des aggregates robustes qui maintiennent les invariants métier sans sacrifier la performance. Exemples de dimensionnement, composition et gestion du cycle de vie.
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 ?