Découvrez en plus sur CQRS à travers des bonnes pratiques et des 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.
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).
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 :
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 :
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 :
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.
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é.
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.
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.
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 :
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.
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.
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 :
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.