Dans une architecture CQRS, la consistance entre écriture et lecture n’est pas toujours immédiate : les commandes modifient le modèle d’écriture, puis des événements alimentent des projections qui mettent à jour les modèles de lecture avec un léger décalage. Cette consistance dite éventuelle a un impact fonctionnel, UX et technique qu’il faut comprendre et encadrer.
Cet article fait partie de la série CQRS : 4 sur 6.
- Part 1 - Introduction à CQRS (cqrs 1/6)
- Part 2 - CQRS et le scaling horizontal (cqrs 2/6)
- Part 3 - Frontières transactionnelles et agrégats en CQRS (cqrs 3/6)
- Part 4 - Cet article
- Part 5 - CQRS et Event Sourcing (cqrs 5/6)
- Part 6 - Évolution du schéma et versionning en CQRS (cqrs 6/6)
1. Définition opérationnelle
La consistance entre le modèle d’écriture et les modèles de lecture est souvent éventuelle : une commande est validée sur la base d’écriture, puis des événements sont publiés et consommés par des projections qui mettent à jour les modèles de lecture. Il existe donc un délai non nul entre le commit côté écriture et la disponibilité de cette mise à jour dans toutes les vues de lecture.
Formulé autrement :
Si plus aucune écriture ne se produit, alors, après un certain temps, tous les modèles de lecture convergent vers un état cohérent avec la base d’écriture.
Concrètement pour l’utilisateur, cela signifie par exemple qu’il peut créer ou modifier une ressource, obtenir un “OK”, mais ne pas la voir immédiatement dans certaines listes, tableaux de bord ou rapports.
2. Impacts fonctionnels et UX
La consistance éventuelle a des conséquences visibles :
- Une ressource créée n’apparaît pas instantanément dans la liste.
- Un champ modifié est à jour sur un écran, mais pas encore sur une vue agrégée.
- Certains chiffres agrégés (totaux, KPI) se mettent à jour avant ou après les listes détaillées.
Il est donc nécessaire de :
- éviter de promettre une synchronicité parfaite partout ;
- expliquer à l’utilisateur que certaines opérations sont “en cours de prise en compte” ;
- prévoir des mécanismes de rafraîchissement ou de mise à jour automatique des écrans.
3. Stratégies UX pour gérer la consistance éventuelle
Quelques patterns courants :
-
Optimistic UI
Mettre à jour immédiatement l’interface (liste, détail) après une commande réussie, en supposant que la projection suivra. Le modèle de lecture “officiel” rattrape ensuite la vue locale. -
Statuts intermédiaires
Introduire des états métier tels que “en cours de traitement”, “en attente de validation”, etc., visibles dans l’interface. L’utilisateur comprend que l’opération est acceptée mais pas encore entièrement propagée. -
Lectures ciblées après écriture
Après certaines commandes critiques, renvoyer une vue basée sur la source la plus fraîche (agrégat ou base d’écriture), plutôt qu’une projection potentiellement en retard. -
Rafraîchissement contrôlé
Offrir un moyen simple de recharger les données (bouton “Rafraîchir”, auto-refresh) pour les écrans sensibles au délai de projection.
4. Conception du pipeline de projections
Pour que la consistance éventuelle reste maîtrisée, le pipeline de projections doit être :
- Fiable
- événements persistés (Event Store, log durable) ;
- consumers avec mécanismes de retry ;
- handlers de projection idempotents (relecture d’un événement sans double traitement).
- Observé et mesuré
- métriques sur la latence entre émission d’un événement et mise à jour des vues ;
- suivi du débit de traitement ;
- journalisation et alertes en cas d’erreurs ou de dérive de latence.
- Rattrapable (replay)
Possibilité de supprimer/recréer une projection et de rejouer l’historique d’événements pour reconstruire un modèle de lecture après correction de bug ou évolution de schéma.
Selon le middleware (bus de messages, log distribué, etc.), les garanties de livraison peuvent varier (at-least-once, at-most-once, livraison hors ordre, etc.). En pratique, il faut supposer que des événements peuvent être dupliqués ou livrés dans le désordre, d’où la nécessité de handlers véritablement idempotents, souvent basés sur une version ou un numéro d’événement enregistré dans le modèle de lecture.
Exemple simplifié de handler idempotent basé sur un numéro d’événement croissant :
public async Task Handle(OrderPlaced evt)
{
var readModel = await _db.Orders.FindAsync(evt.OrderId);
if (readModel != null && readModel.LastEventNumber >= evt.EventNumber)
{
// Événement déjà appliqué (ou un événement plus récent l’a été), on ignore
return;
}
UpsertReadModelFrom(evt, readModel);
readModel.LastEventNumber = evt.EventNumber;
await _db.SaveChangesAsync();
}
Un cas important à surveiller est celui où les commandes continuent de réussir (base d’écriture OK) mais le pipeline de projection est en panne ou en fort retard : sans métriques ni alertes, les utilisateurs ont l’impression que le système est cassé alors que les écritures sont bien persistées.
5. Garanties de lecture : “read-your-writes”
Même dans un système globalement éventuellement consistant, il est parfois nécessaire d’offrir à un utilisateur une garantie de type read-your-writes (voir immédiatement ses propres changements).
Quelques approches :
- Lecture spéciale après écriture
Pour la réponse à une commande, lire directement :- l’agrégat ou la base d’écriture ;
- ou une projection très fortement cohérente (par exemple, mise à jour dans la même transaction ou dans un cache local).
- Corrélation par identifiant / version
Inclure dans la réponse :- l’
Idde la ressource, - éventuellement une
Versionou unEventNumber.
Une API de lecture peut alors : - attendre que la projection ait atteint au moins cette version avant de répondre (dans une certaine limite de temps),
- ou informer explicitement que la projection est en retard par rapport à la version écrite.
- l’
- Caches à courte durée
Pour certaines lectures immédiatement consécutives à une écriture, retourner la donnée depuis un cache en mémoire mis à jour par le handler de commande, en attendant la projection complète.
6. Anomalies visibles et alignement des vues
La consistance éventuelle peut créer des effets surprenants pour les utilisateurs si elle n’est pas anticipée :
- un dashboard agrégé se met à jour avant la liste détaillée sous-jacente ;
- un total change avant que toutes les lignes qui le composent soient visibles.
Pour limiter cela :
- aligner autant que possible les SLA de mise à jour des vues liées (liste + total) ;
- documenter clairement quelles vues sont “quasi temps réel” et lesquelles ont un rafraîchissement plus lent ou périodique.
7. Tests et validation
Tester la consistance éventuelle nécessite plus que des tests unitaires de handlers :
- Tests d’intégration / end-to-end
- envoyer une commande ;
- vérifier que l’écriture est bien persistée ;
- puis, avec un polling contrôlé (timeout, backoff), vérifier que la projection est correctement mise à jour.
- Tests de robustesse des projections
- simuler des duplications d’événements ;
- simuler des retards ou désordres de livraison (selon le middleware) ;
- valider l’idempotence des handlers.
- Tests de charge
Vérifier sous forte charge que la latence de projection reste dans des bornes acceptables pour le métier.
8. Où la consistance éventuelle est (ou n’est pas) acceptable
Il est essentiel d’identifier :
- les zones où un léger délai est acceptable :
- reporting, analytics, KPI globaux, certains dashboards ;
- les zones où la cohérence doit être stricte :
- paiements, mouvements de stock critiques, sécurité, conformité.
Dans ces zones critiques, on peut :
- soit ne pas appliquer CQRS/consistance éventuelle pour les lectures concernées ;
- soit réserver CQRS aux lectures secondaires et conserver des lectures directes sur des sources fortement cohérentes.
Souvent, lorsque la cohérence doit être stricte entre plusieurs données, cela révèle qu’elles devraient faire partie du même agrégat (voir la partie 3/6 sur les frontières transactionnelles) plutôt que d’être recollées a posteriori via des projections éventuellement consistantes.
En résumé, la consistance éventuelle est une conséquence naturelle du découplage lecture/écriture dans CQRS. Bien gérée (UX adaptée, pipeline de projections robuste, garanties ciblées comme read-your-writes), elle permet de bénéficier de la scalabilité et de la flexibilité de CQRS tout en maîtrisant l’expérience utilisateur et les risques métier.