Technologies
Méthodologie
April 30, 2024

CQRS : bonnes pratiques et points de vigilance

Découvrez en plus sur CQRS à travers des bonnes pratiques et des points de vigilance !

Nous suivre

CQRS : bonnes pratiques et points de vigilance.

Le patron de conception CQRS (Command and Query Responsibility Segregation) a émergé comme une approche novatrice dans le domaine de l'architecture logicielle. Il a pris plus d’attraction récemment surtout avec l’augmentation de la complexité et les attentes des solutions informatiques et l’apparition des outils facilitant sa mise en place.

Le but de cet article est de mettre ce style d’architecture sous inspection et de formuler les avantages, les précautions à prendre et les impacts potentiel si vous décidez de l’adopter dans l’espoir de mieux vous informez pour réussir votre projet.

Avant de commencer à le définir, expliquons d’abord le problème qu’il essaye de résoudre.

Le problème

Historiquement, les applications utilisaient un même modèle de données pour effectuer les lectures et écritures en bases de données. Ce qu’on appelle communément opérations CRUD (Create Read Update Delete). 

Une image contenant texte, capture d’écran, conceptionDescription générée automatiquement

Utiliser la même chaîne de traitement des écritures et lectures était le choix naturel à entreprendre à l’égard de simplicité, productivité et limitations techniques parfois des librairies et outils utilisés. Toutefois, dans le cas des applications plus complexes, elle peut devenir plus difficile à gérer à cause de la simple constatation suivante : la charge de travail de lecture et d’écriture sont asymétriques de nature et elles sont soumises à des contraintes de performance et de mise à échelle différentes.

Une autre constatation, traditionnellement, il y a toujours une disparité de la fréquence des opérations de lecture et d’écriture. Celle de lecture tend à être beaucoup plus importante généralement surtout si le système expose une interface utilisateur.

Utiliser la même représentation de données pour effectuer ces deux types d’opérations aura les inconvénients suivants dans une application suffisamment complexe :

  • Chargement en trop ou en moins de données : souvent des colonnes et des propriétés sont obligatoires pour effectuer les contrôles des opérations d’écritures ne le sont pas pour les lectures et vice versa. Dans ce cas Elles seront chargées sans être utilisées ce qui impacte les performances et la sécurité. 
  • La gestion des droits peut se compliquer comme chaque entité du modèle de donnée est soumise à la fois à des lectures et écritures. Des propriétés de l’entité risque de s’exposer hors contexte d’habilitation.
  • Augmentation de la contention et la concurrence sur les données comme à chaque requête on touche à un sous-ensemble plus large du schéma global de données pour couvrir à la fois les besoins de lecture et écriture. Ce qui impacte négativement les performances et met en difficulté les possibilités d’adapter la granularité des données en fonction du contexte.

CQRS

CQRS sépare les deux modèles d’écriture et de lecture et prévoit deux mécanismes pour les gérer commandes et requêtes

  • Les commandes : se sont, exclusivement, les tâches de création/mutation des données.  Ils doivent se baser sur une logique d’accomplissement de tâches et non un chargement/visualisation de données.
  • Les requêtes : se sont, exclusivement les tâches de lecture de données. Elles ne devraient en aucun cas modifier le statut des données en base.

Ces deux pipelines opèrent sur deux modèles de données distincts en base de données.

Avec cette architecture le modèle de l’application deviendra le suivant :

Une image contenant texte, capture d’écran, conceptionDescription générée automatiquement

La présence de deux modèles de données distincts permet d'adapter leur conception aux besoins spécifiques de chaque domaine.

Concrètement le modèle de lecture peut se restreindre à des vues SQL au-dessus du modèle d’écriture dans l’implémentation la plus basique. Néanmoins, un inconvénient de cette approche réside dans le coût des jointures complexes, qui devront être recalculées à la volée. 

Pour une isolation plus stricte, les données de lecture et d’écriture peuvent être séparées physiquement dans des tables différentes et mêmes des bases de données différentes. Une base de lecture et une d’écriture.

Une image contenant texte, capture d’écran, Police, conceptionDescription générée automatiquement

La base de lecture peut être optimisée pour la latence, mise à échelle et peut avoir une forme physique des données en mémoire différente de la base d’écriture. Par exemple la base d’écriture peut être sous forme de base relationnelle traditionnelle alors que celle de lecture peut être une base NoSQL (Redis, MangoDB…) avec plusieurs niveaux de cache des entrées fréquemment sollicitées.

Il est possible dans ce cas aussi de prévoir des politiques de balancement de charge et de réplique dissociés pour les deux bases pour adapter et varier les ressources d’infrastructure en fonction de la charge perçue par chaque base.

Cependant une contrainte potentielle à cette séparation est le devoir de garder les deux bases en synchronisation permanente pour garantir la cohérence des données l’application. Il a y une latence supplémentaire et inévitable introduite pour synchroniser les deux bases, ce qui rend le modèle de cohérence de données un modèle de cohérence éventuelle (plus de détails dans la suite). Il faut que l’application soit tolérante de ce modèle et que ça n’impacte pas négativement l’expérience utilisateur en termes de réactivité.

En résumé les avantages de CQRS sont les suivants :
  • Pouvoir mettre à échelle d’une façon indépendante les ressources matérielles pour la base de lecture et d’écriture. Surtout dans un environnement de cloud. Ce qui facilitera l’adaptation des ressources aux vraies charges de production et minimiser les coûts.
  • Sécurité : pour la partie écriture et vu que ce ne sont que des commandes qui sont censées muter les données on peut prévoir des politiques de droits à granularité fine et robuste.
  • Requêtes simplifiées et performantes : pas de jointures complexes, ni de surcharge de données, les commandes opéreront que sur les données à modifier et requêtes vont charger les données à partir d’un format déjà adapté et plus plat.
  • Séparation de responsabilité : la partie écriture peut s’occuper de la complexité de la logique métier, tandis que celle de lecture sera plus simple et facile à gérer.

Cohérence de données

Les applications modernes utilisent de plus en plus des données dispersées dans des bases de données différentes. La gestion et le maintien de la cohérence des données dans cet environnement peuvent devenir un aspect critique du système, notamment en termes de problèmes de concurrence et de disponibilité qui peuvent survenir. Vous devez souvent sacrifier une forte cohérence contre de la disponibilité. Cela signifie que vous devrez peut-être concevoir certains aspects de vos solutions autour de la notion de cohérence éventuelle et accepter que les données utilisées par vos applications ne soient pas toujours totalement cohérentes.

Dans le monde des bases de données relationnelles, la cohérence est souvent assurée par des modèles transactionnels qui utilisent des verrous pour empêcher des instances d'applications concurrentes de modifier simultanément les mêmes données.

Dans un système fortement cohérent, les verrous bloquent également les demandes simultanées d'interrogation de données, mais de nombreuses bases de données relationnelles permettent à une application d'assouplir cette règle et de donner accès à une copie des données qui reflète l'état dans lequel elles se trouvaient avant le début de la mise à jour.

De nombreuses applications qui stockent des données dans des bases de données non relationnelles, des fichiers plats ou d'autres structures suivent une stratégie similaire, appelée verrouillage pessimiste. Une instance d'application verrouille les données pendant leur modification, puis libère le verrou une fois la mise à jour terminée.

Dans une application cloud moderne, les données sont susceptibles d'être réparties entre des bases de données hébergées sur différents sites, dont certains pourraient être dispersés sur une vaste zone géographique. Cela peut se produire pour diverses raisons : améliorer l'évolutivité en équilibrant la charge sur plusieurs serveurs, améliorer le temps de réponse en localisant les données à proximité des utilisateurs et les services qui y accèdent, ou améliorer la disponibilité en répliquant les données sur différents sites.

Avec CQRS si on décide de séparer physiquement les deux bases de lecture et d’écriture, le maintien de la cohérence des données dans les bases distribuées peut constituer un défi de taille. Le problème est que les stratégies telles que la sérialisation et le verrouillage ne fonctionnent correctement que si toutes les instances d'application partagent la même base de données, et que l'application est conçue pour garantir que les verrous sont de très courte durée. Cependant, si les données sont partitionnées ou répliquées dans différentes bases de données, le verrouillage et la sérialisation de l'accès aux données pour maintenir la cohérence peuvent devenir une surcharge coûteuse qui a un impact sur le débit, le temps de réponse et l'évolutivité d'un système. Par conséquent, la plupart des applications distribuées modernes ne verrouillent pas les données qu’elles modifient et adoptent une approche plus détendue en matière de cohérence, connue sous le nom de cohérence éventuelle.

Forte cohérence

Dans le modèle à cohérence forte, tous les changements sont atomiques. Si une transaction met à jour plusieurs éléments de données, la transaction n'est pas autorisée à se terminer tant que toutes les modifications n'ont pas été effectuées avec succès ou (en cas d'échec) qu'elles n'ont toutes été annulées.

Entre le début et la fin d'une transaction, d'autres transactions simultanées peuvent ne pas pouvoir accéder aux données qui ont été modifiées, ils seront bloqués. Si les données sont répliquées, une transaction qui implémente une cohérence forte peut ne pas être autorisée à se terminer tant que chaque copie de chaque élément modifié n'a pas été mise à jour avec succès.

L'objectif du modèle de cohérence forte est de minimiser le risque qu'une instance d'application se voit présenter une vue incohérente des données.

Cohérence éventuelle

La cohérence éventuelle est une approche plutôt pragmatique de la cohérence des données. Dans de nombreux cas, une forte cohérence n’est pas réellement requise tant que tout le travail effectué par une transaction est terminé ou annulé à un moment donné et qu’aucune mise à jour n’est perdue.

Dans le modèle de cohérence éventuelle, les opérations de mise à jour des données qui s'étendent sur plusieurs sites peuvent se répercuter sur les différentes bases de données à leur propre rythme, sans bloquer les instances d'applications simultanées qui accèdent aux mêmes données.

Dance cas vous aurez à choisir entre cohérence, disponibilité et tolérance de partition. Concrètement :

  • Sois-vous fournissez une vue cohérente des données distribuées au prix du blocage de l'accès à ces données pendant que les incohérences sont résolues. Cela peut prendre un temps indéterminé, en particulier sur les systèmes présentant un degré de latence élevé ou si une panne de réseau entraîne une perte de connectivité sur une ou plusieurs partitions.
  • Ou vous fournissez un accès immédiat aux données au risque qu’elles soient incohérentes entre les sites. Les systèmes de gestion de bases de données traditionnels visent à assurer une forte cohérence, tandis que les solutions basées sur le cloud qui utilisent des bases de données partitionnées sont généralement motivées par la garantie d'une plus grande disponibilité et sont donc davantage orientées vers une cohérence éventuelle.

Il convient de garder à l’esprit qu’une application peut ne pas exiger que les données soient cohérentes à tout moment. Par exemple, dans une application Web de commerce électronique typique qui permet à un utilisateur de parcourir et d'acheter des produits, tous les niveaux de stock présentés à un utilisateur sont susceptibles d'être des valeurs statiques déterminées lorsque les détails d'un article en stock sont interrogés. Si un autre utilisateur simultané achète le même article, le niveau de stock dans le système diminuera mais ce changement n'aura probablement pas besoin d'être reflété dans les données affichées au premier utilisateur. Si le niveau de stock tombe à zéro et que le premier utilisateur tente d'acheter l'article, le système peut soit alerter l'utilisateur que l'article est désormais en rupture de stock, soit placer l'article en rupture de stock et informer l'utilisateur que le délai de livraison peut être étendu.

‘Event sourcing’

CQRS est souvent utilisé en conjonction avec ‘event sourcing’, une approche où tous les changements d'état de l'application sont enregistrés sous forme d'événements. 

Chaque action ou changement dans le système est représenté en tant qu'événement. Ces événements sont stockés dans un journal (ou une séquence) appelé "journal d'événements". Plutôt que de stocker l'état actuel d'une entité, le système reconstruit cet état en jouant les événements dans l'ordre chronologique.

Cette combinaison renforce la traçabilité des modifications et permet de reconstruire l'état actuel de l'application à tout moment en rejouant les événements dans l'ordre chronologique.

Dans un contexte CQRS, ces événements forment une représentation immédiate et logique des commandes. Ainsi chaque commande peut être enregistrée comme un événement dans le journal au lieu de modifier le dernier statut des données dans une base relationnelle classique. Ce qui évite les problèmes de concurrence sur les données.

Dans ce cas ce journal servira comme base d’écriture.

La base de lecture peut être calculée, en asynchrone, à tout moment en rejouant les événements passés pour créer la représentation actuelle en suivant un modèle de cohérence éventuelle.

Cependant, adopter ce schéma peut introduire une complexité car du code doit être écrit pour lancer et gérer des événements, et assembler ou mettre à jour les vues ou objets appropriés requis par le modèle de lecture. La complexité du modèle CQRS lorsqu'il est utilisé avec le modèle ‘event sourcing’ peut rendre une mise en œuvre réussie plus difficile et nécessite une approche différente. Mais il pourra faciliter la modélisation des objets du domaine et faciliter la reconstruction des vues ou la création de nouvelles, car l'intention des modifications apportées aux données par les commandes est préservée.

Un autre aspect à considérer dans ce cas, est que faire ces reconstructions de vues à partir des journaux d’événements peut s’avérer coûteuse en termes de ressources matérielles et temps processeur/mémoire. Surtout quand il y a des calculs de sommaires, agrégations et analyses à faires sur les événements sur des périodes longues.

Une technique pour mitiger ça sera de sauvegarder des images de l’état des vues calculées à des intervalles réguliers, de manière à ne pas reprendre les calculs depuis le premier évènement mais depuis l’image la plus proche de votre date de reconstruction. 

Quand utiliser CQRS

Le modèle CQRS comme tout patron de conception est adapté à des situations et pas d’autres. Considérez son utilisation dans les cas suivants : 

  • Les règles métier et du domaine sont suffisamment complexes. Quand il y a une interface utilisateur à base de transitions conditionnelles avec plusieurs étapes. Gérer cette complexité exclusivement avec des commandes et une chaine de traitement qui ordonne ces transitions avec un modèle d’objets adapté et dédié seulement à altérer les données d’une façon cohérente. Le nombre de lignes de code à écrire juste pour mettre en place CQRS doit être proportionné par rapport au nombre de lignes de code totales et ne doit se justifier qu’à partir d’un certain seuil de complexité.
  • Votre application est basée sur le cloud avec des bases de données partitionnées, et des interactions avec d’autres systèmes. Des exigences de disponibilité fortes. Vérifier l’exigence en termes de cohérence de données. Une cohérence éventuelle suffira-t ’elle ou il faut une cohérence forte.
  • Vous avez une politique d’adaptation automatique des ressources matérielles, mise à échelle des nœuds de traitements ou des groupes de balancement de charge. (Docker, Kubernetes, Microservices etc…). Quand vous voulez adapter cette mise à échelle d’une manière dissociée pour les consultations de données (lecture) et les écritures.
  • Les scénarios quand il y a une forte probabilité que le système évolue progressivement et que les règles métier changent fréquemment. L’isolation des lectures de l’écriture permettra de minimiser le périmètre des changements et de faire évoluer plusieurs versions de l’application en parallèle.
  • Pour mieux distribuer la charge d’un grand projet sur différentes équipes. Une équipe peut se focaliser sur la complexité du domaine et gérer les écritures et les commandes et une autre équipe peut s’occuper des lectures.
  • Système avec une forte probabilité de concurrence sur les données. Avec plusieurs utilisateurs qui essayent de modifier les mêmes données. CQRS vous permet de créer des commandes d’une granularité fine qui minimisent la probabilité des conflits et les gérer au cas où ils se présentent.
  • Dans les architectures microservices, lorsqu’on veut récupérer des informations (côté serveur) provenant de plusieurs microservices : on a le choix entre l’API Composition et le CQRS. Dans ce cas, le choix de CQRS est souvent fait pour des raisons de performance.

Ne pas utiliser CQRS quand

  • Votre système est simple et les règles métiers sont suffisamment faciles et peuvent être gérées avec un modèle CRUD classique.
  • Quand vous avez une exigence d’une cohérence de données forte et instantanée

Gardez également à l'esprit qu'une stratégie mixte, où vous appliquez CQRS dans des sous-modules de votre application lorsque cela apporte le plus d'avantages, peut être adoptée.

Découvrez aussi

Inscrivez-vous à notre newsletter