CQRS, Docteur Read et M. Write

Inside GroupActualité
CQRS, pour Command Query Responsibility Segregation est une architecture logiciel qui repose sur un principe simple : la séparation, au sein d’une application, des composants de modification et de restitution de l’information sous les termes Command (pour l’écriture) et Query (pour la lecture).

Ces termes sont repris du pattern CQS, pour Command and Query Separation, selon lequel les méthodes d’une classe doivent être divisées en 2 catégories bien distinctes :

  • Les « Queries » : renvoient un résultat et ne modifient pas l'état observable du système (sont donc exemptes d'effet de bord).
  • Les « Commands » : modifient l'état d'un système mais sans renvoyer de valeur

CQRS applique donc CQS au niveau applicatif.

Diviser pour mieux régner

Les développeurs à l’origine de CQRS partent du principe suivant : les besoins, fonctionnels ou techniques, d’une application peuvent être très différents que l’on soit dans un scénario de lecture ou d’écriture :

  • En lecture : des besoins de scalabilité, de dénormalisation, d’agrégation de différentes sources, de recherche
  • En écriture : des besoins transactionnels, de garantir la cohérence des données, de normalisation

 

Scinder ces différentes visions en 2 modèles bien distincts induit donc une séparation au niveau du code, rendant également possible une séparation au niveau hardware si nécessaire (sources de données, voir microservices).

[Source : Martin Fowler]

Le modèle d’écriture

C’est le modèle qui porte les règles et le comportement métier de notre application. Avec CQRS, chaque action utilisateur est contextualisée : on ne se contente pas de modifier une liste de données, la commande porte l’intention et borne le champ d’intervention sur une ressource.

On remplace ainsi des action PATCH, au sens « CRUDien », pauvres en terme de sens, par des classes Command reflétant les cas d’utilisation fonctionnels, avec un nom d’action simple, à l’impératif : PayOrder, RenewPassword, CreateUser …

Dans une application web, la requête est généralement validée de manière superficielle, transformée en « Command », puis validée et traitée par un « Handler » qui va avoir pour responsabilité principale de coordonner les actions de la couche métier.

Comme un Handler traite unitairement une Command, il est d’autant plus facile de mettre en place des écritures transactionnelles avec une gestion simplifiée des erreurs et des retours en arrière, en profitant pleinement des fonctionnalités avancées d’un ORM (Object Relational Mapping) par exemple.

Cerise sur le gâteau, en permettant de se focaliser sur les modifications de l’état d’un système, la couche d’écriture est donc un partenaire de choix pour des modélisations métier basées sur Domain Driven Design, permettant de profiter pleinement par exemple du pattern Aggregate pour garantir la cohérence des données tout au long de son cycle de vie.

Le modèle de lecture

Ici plus besoin de modèle riche, nous sommes dans la couche de restitution de l’information. Les données sont donc renvoyées via des DTOs, taillés sur mesure pour les cas d’utilisation auxquels ils correspondent.

Il est donc possible de retourner plusieurs visions d’un même agrégat, basées sur le besoin fonctionnel, facilitant l’émergence d’IHM orientées tâches.

Le principal bénéfice de cette approche : la performance ! On oublie l’ORM et on tire partie au maximum de requêtes dédiées et optimisées, avec jointures et directives SQL spécifiques à votre système de base de données.

Besoin d’encore plus de performance : isolez les bases de données de lecture et d’écriture et passez au pattern Database per service et bénéficiez des avantages de scalabilité d’une architecture en microservices. Vous aurez cependant à veiller à la synchronisation entre la base de données principale, gérée par la couche d’écriture, source de vérité, et les différentes projections crées pour le ou les modèles de lecture, chose qui peut être résolue de manière asynchrone avec de la gestion d’événements.

Bilan

Les avantages

  • Supprimez le risque d’effets de bord : les méthodes des services de lecture ne modifient pas l’état du système et peuvent donc être utilisés sans crainte de provoquer des comportements anormaux sur le reste du système.
  • Isolez vous parcours applicatifs et évitez les couplages inutiles en séparant les services de lecture et d’écriture en base (mise en place du pattern Repository simplifiée)
  • Facilitez l’exposition de vos ressources : l’API est rendue réellement indépendante de son implémentation grâce à l’utilisation de DTOs et de types “simples”. Les routes sont plus explicites en termes techniques et sémantiques
  • Bénéficiez d’une architecture logicielle performante et évolutive répondant à des besoins de scalabilité (évolution d’une couche monolithique vers une architecture en microservices avec un coût moins fort que pour une architecture plus traditionnelle).

Les inconvénients

  • N’utilisez pas CQRS partout: réservez cette architecture aux contextes métiers complexes (ou Bounded Context au sens DDD), la multiplication des classes apportant une complexité supplémentaire en termes de développement pour des cas d’utilisations simple ou un CRUD serait plus efficace.
  • L’utilisation de DTOs peut apporter une couche de mapping supplémentaire pouvant conduire, en cas de modifications fréquentes des schémas de persistance, à des temps supplémentaires d’adaptation du code.
  • Mettre en place CQRS pour la première fois nécessitera un temps d’apprentissage non négligeable afin de se familiariser avec les différents concepts

Pour aller plus loin