CQRS: en Regular, Premium ou Deluxe ?

J’ai l’habitude de rédiger des articles techniques que l’on pourrait classer dans la catégorie des « how-to? » Celui-ci sera la première exception à cette règle.

J’ai eu la chance de pouvoir assister à une présentation de Dino Esposito lors de la conférence SDD 2015 à Londres. Celle-ci avait pour titre Applying CQRS and Event Sourcing in .NET applications. Vous pouvez d’ailleurs trouver une vidéo de la même présentation pour TechEd 2014. Son contenu n’a pas été tout à fait celui auquel je m’attendais, et je pense que ce pourrait être l’impression d’autres développeurs qui se seraient déjà intéressés au pattern CQRS.

CQRS est le nom récent d’une technique remise au goût du jour. Il y a assez peu de ressources comparé à d’autres approches plus conventionnelles. Greg Young et Martin Fowler, parmi d’autres, en ont beaucoup parlé, mais il reste difficile, de mon point de vue, d’appréhender CQRS en termes de retours d’expérience « real world » et d’exemples pratiques concrets autres que des projets « demo ». MSDN contient une documentation à ce sujet que je trouve à la fois relativement approfondie et synthétique.

Le fait est qu’il est difficile de présenter des exemples à la fois génériques et concrets de cette approche. La présentation de Dino Esposito m’en a fait prendre conscience. Cela est dû à plusieurs choses:

  • la mise en oeuvre demande plus de travail que l’approche classique;
  • le corrolaire du précédent point est qu’il y a aussi plusieurs façons de faire; des alternatives très différentes et des variantes plus subtiles qui dépendent des contraintes de chaque application.

CQRS: Command & Query Responsibility Segregation

Je ne ferai pas une présentation de ce pattern, je me concentre seulement sur ce qui fait sa différence principale en supposant que ses grands principes vous soient déjà familiers.

Bien que lourd, le nom est explicite: le principe est de séparer le traitement des actions du traitement des requêtes. Les commandes représentent les actions susceptibles de modifier les données, tandis que les requêtes sont les lectures de ces données. Autrement dit, du point de vue du composant qui exécute les requêtes, les données ne sont pas modifiables directement.

En fonction de l’implémentation adoptée, cela apporte tout un tas de bénéfices, essentiellement l’assurance de performances inégalées notamment côté requêtes. Cela vient au prix d’une mise en oeuvre plus difficile. Le fait de séparer ces deux flux, écritures et lectures, permet tout un tas d’optimisations très intéressantes. Par exemple, si un seul thread écrit, il devient assez facile de précharger les données en mémoires pour les modifier, plutôt que de modifier directement les données en bases (ce qui est typiquement nécessaire pour gérer les accès concurrents de plusieurs threads). Cela élimine les conflits et donc la nécessité de mécanismes de synchronisation. Côté lectures, comme il n’y a pas de synchronisation avec les écritures, cela garantie quasiment le temps d’accès aux données quelle que soit la charge de l’application.

Pour avoir mis en oeuvre CQRS dans le projet relativement conséquent d’un grand groupe, je pense que mon retour d’expérience peut être intéressant à partager, au regard de la vision présentée à la conférence SDD. Je ne pourrai malheureusement pas donner d’exemples de code, cet article est très modestement l’expression d’idées sur le sujet et j’espère que cela puisse servir à quiconque souhaiterait appréhender le sujet.

CQRS: la caractéristique clé (d’après moi)

La description de ce pattern va changer selon la source, plus ou moins stricte ou souple. Par exemple, on associe souvent CQRS au pré-requis d’utiliser l’Event Sourcing. La version stricte ne se limite pas à cela mais je pense que ce point est souvent considéré comme une caractéristique clé.

Pour moi, la véritable distinction de CQRS est simplement le fait de séparer les données dans deux sources. C’est cela qui permet d’obtenir les avantages décrits plus hauts. Ce que j’ai appelé source peut être une base de données SQL classique, une base nosql, un fichier, ou même une forme de repository en mémoire…

Je pense avoir compris que Dino Esposito est encore plus souple en la matière et considère CQRS le simple fait d’avoir deux API séparées: une API pour les lectures, une autre pour les commandes (dans sa solution d’exemple, Merp, ces API sont nommées stacks). C’est la mise en oeuvre la plus simple, la « regular ».

Dino, qui a une faculté certaine à rendre son discours sympa à écouter, voit en CQRS trois parfums. Je pense que son idée n’est pas de dire qu’il n’y en a que trois, mais que d’après son expérience, trois ressortent finalement. Il les a nommés ainsi, par ordre de perfectionnement :

  • Regular
  • Premium
  • Deluxe

CQRS Regular

C’est la mise en oeuvre la plus simple, celle du « pauvre » pourrait-on dire. Elle consiste simplement à respecter la séparation des deux API: commandes et requêtes.

Pour moi, le fait de séparer les API, tout en ne conservant qu’une source de données, n’apporte rien des avantages communément permis par le pattern CQRS. Avec cette définition, il est assez facile d’appliquer ce pattern à un projet. Et je ne dis pas que ce serait faux. Mais dans les faits, c’est toujours une approche classique, basée sur un modèle et une source unique des données (j’entends source physique), simplement avec une API différente. C’est un peu comme choisir entre utiliser un ORM (Entity, NHibernate…) et avoir une couche ADO.NET légère, selon l’usage que l’on souhaite faire de sa source de données. Limité à cela, je trouve que c’est plus une affaire de préférence qu’un véritable attribut d’architecture. Ce peut être cependant une phase transitoire: en effet, bien réalisée, cette séparation en deux API distinctes peut ouvrir la voie à une séparation physique des données.

Le parfum « premium » est intéressant en cela qu’il représente un compromis entre cette version simpliste et la troisième version que je considère être l’approche conventionnelle.

CQRS Premium

Cette version est à mon sens le premier niveau à partir duquel on bénéficie d’avantages: il nécessite la séparation des données en deux sources: une modifiable (par des commandes), une consommable (par des requêtes). La séparation physique du stockage des données est ce qui apporte les avantages, à savoir éliminer la concurrence entre les lectures et les écritures, au moins du côté des lectures, et possiblement du côté des écritures si on se contraint à un seul thread.

Comme la source utilisée pour les requêtes est indépendante de la source originale (celle que l’on peut modifier), une optimisation simple et intéressante est de matérialiser les vues: les deux sources n’ont pas forcément le même modèle, c’est autant de travail en moins au moment de consommer les données. Ajouté à la non concurrence des écritures et des lectures, on comprend sans difficulté que ce pattern rend possibles des performances hors normes.

La réelle difficulté n’est pas d’avoir deux sources de données, c’est de les synchroniser. Le plus souvent, on souhaite refléter dans un délai raisonnable les modifications apportées à la source de travail. De plus, l’état finalement reflété doit être cohérent. Il ne s’agit pas de permettre à une requête de retourner un état transitoire incohérent.

Il existe certainement des applications où un court délai de synchronisation n’est pas important. Par exemple, un traitement quotidien pourrait utiliser une base de données conventionnelle, et une tâche de synchronisation aurait lieu la nuit vers une base de données en lecture seule. Dans ce cas, l’implémentation n’a pas besoin d’appliquer la technique qui va suivre.

Il n’y a pas énormément de façons de réaliser la synchronisation pseudo-« temps réel » de manière performante. Il y en a sûrement plus d’une, mais la plus courante, et celle généralement mise en avant lorsqu’on décrit CQRS est l’Event Sourcing. C’est la version « deluxe ».

CQRS Deluxe

C’est le « parfum » tel qu’on l’on retrouve le plus souvent appliqué par les évangélistes de cette approche: chaque modification de données a pour origine un événement. Chaque événement est écrit dans un Event Store, une forme de journal d’audit. L’Event Store est le flux d’événements qui, une fois reconstitué dans un « snapshot », représente l’état du monde à un instant donné. On pourrait penser qu’il n’y a qu’une seule source de données mais il y en a bien deux: l’Event Store qui permet de retrouver n’importe quel état du « big bang » jusqu’à « maintenant », et l’état résultant de l’événement le plus rėcent (le dernier « snapshot », parfois appelé « cache »). Celui-ci est typiquement une structure en mémoire (un objet ou un tableau d’objets), mais pas forcément. L’idée est que l’on n’exécute pas les requêtes sur l’Event Store, en tout cas pas sur son intégralité. Ce serait inefficient dans la majorité des cas.

L’Event Sourcing fournit un moyen, circonvolu certes mais pratique, pour assurer une synchronisation quasi temps réel entre l’événement initial, qui représente la modification, et le snapshot de l’état consommé par les requêtes. En effet, le fait de représenter toute modification d’état par une intention (par exemple, création d’un panier d’achat vide, ajout d’un article au panier, demande de confirmation d’achat, etc.) nous permet de garantir que chaque transition d’état est cohérente. Si une transition consistait à modifier différentes valeurs sans notion d’intentionnalité, il serait plus difficile de garantir la cohérence continue de l’état, celui-ci pouvant être consommé à tout moment. Ensuite, chaque événement publié dans l’Event Store est aussi un excellent déclencheur pour la consommation de cet événement afin de reconstituer le snapshot de l’état courant. En effet, l’Event Store peut (et devrait généralement) être utilisé comme une file d’attente. Cela résout donc la problématique de la synchronisation entre l’état modifiable et l’état consommable, dans un délai raisonnable.

Conclusion

Cette présentation à la conférence SDD 2015 m’a fait m’interroger sur mon choix inconscient du parfum le plus « avancé » de CQRS. Inconscient car je ne voyais pas différentes formes du pattern, en dehors de petites variantes sur des détails d’implémentation (et il y en a beaucoup). C’est ce que je trouve intéressant dans ce type d’occasion: écouter des retours d’expérience et avoir de temps en temps cette pensée « ah! Je n’avais pas vu ça comme ça ». On identifie une approche comme solution possible à notre problème. Ce problème a toujours un contexte bien particulier et on élimine/adopte des choix selon les contraintes propres à ce contexte. Notre condition nous pousse naturellement à penser/vouloir que la solution adoptée est plus générique qu’elle ne l’est en réalité. C’est aussi pour cela que je doute souvent des solutions ou des outils de productivité qui vous promettent de vous faire économiser des heures de travail. Je digresse. Si CQRS est une solution à l’un de vos projets, je ne pense pas que vous ayez un réel choix entre ces trois « parfums ». En effet, les contraintes de votre projet vous guideront dans l’application de ce pattern.

Tout cela pour dire que si CQRS est utilisé dans une application typique, c’est-à-dire composée d’une ou plusieurs UI et d’un repository (des données), et éventuellement de tâches de fond (mais pas nécessairement), alors je pense que l’on finira par avoir besoin des concepts suivants :

  • un Service Bus pour l’aspect Messaging
  • un Event Store et un design Event Driven

Ensuite, il y aura évidemment des variantes, d’une application à l’autre, sur chacun de ces concepts: événements asynchrones, garantie de l’ordre des événements basée un numéro d’ordre ou sur une marque horaire, stratégie pour les snapshots de l’Event Store, au moins deux sources physiques de données mais peut-être davantage, etc.

Le pattern CQRS est donc un choix majeur dans la plupart des applications. De mon point de vue, c’est un excellent choix dans beaucoup de situations car les techniques que l’on est amené à embarquer (service bus, event-driven application) apportent un design plus robuste qu’une approche plus conventionnelle. Un tel design sera typiquement plus « scalable ». Cela a évidemment un coût en amont qui n’est pas forcément justifié pour une application très simple, avec peu d’accès concurrents. Et il est effectivement possible d’utiliser ces techniques sans appliquer le pattern CQRS. En cela, on ne peut réduire CQRS à la combinaison de ces dernières.

Quelques références de qualité

CQRS (Martin Fowler, bliki, July 2011)
CQRS and Event Sourcing (Greg Young, video, Code on the Beach 2014)
Command and Query Responsibility Segregation (CQRS) Pattern (MSDN)
Event Sourcing (Martin Fowler, bliki, December 2005)
[mise à jour du 18/07/2015]:
3 Types of CQRS, Vladimir Khorikov (20/04/2015), article de Vladimir Khorikov, qui est proche à mon sens du point de vue de Dino Esposito