Article
Le read model, dérivé réparable — et la porte vers le dehors
Le read model : projection dérivée, dénormalisée, jetable, reconstructible depuis le write model. Et c'est côté lecture que le système parle au monde extérieur.
Si le write model est un sanctuaire qui ne sort jamais de chez lui, quelqu'un doit bien parler au monde extérieur — servir les lectures, notifier, indexer, appeler les services tiers. C'est le rôle de tout ce qui vit en aval de la décision : le read model, cette vue dérivée dont la nature n'est pas d'avoir raison mais d'être commode — dénormalisée, jetable, reconstructible — et, à côté de lui, les réactions aux événements, qui, elles, sont autorisées à s'aventurer dehors.
Le volet précédent a enfermé la décision dans sa transaction. Ce huis clos serait invivable si rien, ailleurs, n'avait le droit d'ouvrir les fenêtres. C'est tout le rôle du côté lecture : prendre ce que le write model a décidé et le rendre utile — vite, sous la forme attendue, et jusqu'aux systèmes extérieurs. Le read model n'est pas le parent pauvre de l'architecture ; c'est là que le système rencontre ses utilisateurs et ses voisins.
Une vue par usage, aucune règle
Le read model est une projection : une représentation dérivée du write model, façonnée pour la lecture (le mot désigne à la fois la vue obtenue et le fait de la projeter — le contexte tranche). Là où le modèle de décision reste normalisé — chaque fait à un seul endroit — la projection est volontiers dénormalisée : données déjà assemblées, prêtes à afficher, dupliquées s'il le faut pour épargner des jointures. La page « mes commandes » ne rejoue pas la logique métier ; elle veut une ligne par commande, tout de suite.
Et puisqu'une projection ne porte aucune règle, rien n'oblige à n'en avoir qu'une. On peut en tailler autant que d'usages : une vue pour la liste, une autre pour la recherche plein texte, une autre encore pour un tableau de bord agrégé. Chacune est libre de sa forme, parce qu'aucune n'a à défendre un invariant. C'est un renversement complet par rapport au write model : là-bas, une seule forme, gardienne de la vérité ; ici, autant de formes que de besoins, toutes jetables.
Reprenons la commande e-commerce. Le même write model — l'agrégat Commande et ses règles — peut alimenter plusieurs projections sans rapport de forme : une table « résumé de commande » (numéro, client, total, statut) pour la page liste ; un index plein texte pour retrouver « où en est ma commande de chaussures ? » ; un cumul par jour pour le tableau de bord des ventes. Trois vues, trois formes, une seule vérité en amont. Modifier l'une n'engage pas les autres, et aucune ne peut contredire le write model, puisque aucune ne fait autorité. On ne se demande plus « comment faire tenir tous ces usages dans un seul modèle ? » — question insoluble qui mène aux schémas obèses — mais « quelle vue sert le mieux chaque usage ? ».
Jeter, et reconstruire
La propriété la plus libératrice du read model tient en une question qu'on devrait se poser en concevant tout système : que se passe-t-il quand ça casse ? Un bug corrompt une vue, une projection prend du retard, un déploiement laisse un index à moitié peuplé. Côté write model, un tel incident serait grave — c'est la source de vérité. Côté read model, il est presque anodin : on jette la vue et on la reconstruit.
Reconstruire une projection, c'est repartir de l'état courant du write model et la recalculer. Les auteurs de Cosmic Python le décrivent sans détour : on écrit un outil qui « interroge l'état courant du côté écriture » et rejoue la construction de la vue, entrée par entrée. Rien de sorcier, et surtout rien d'irréversible. Une projection n'est jamais une vérité à protéger ; c'est un dérivé réparable. Ce point mérite qu'on s'y arrête, car il tranche avec une idée reçue : reconstruire un read model ne suppose pas de rejouer un journal d'événements. On reprojette la dernière version depuis la source, tout simplement. Garder l'historique pour retrouver un état passé est un autre sujet — celui de l'event sourcing, qu'on traitera pour lui-même.
Imaginons qu'un déploiement bugué ait, une semaine durant, oublié de marquer les commandes « expédiées » dans le résumé. Le write model, lui, sait très bien lesquelles l'ont été — c'est sa vérité. La réparation ne consiste pas à retrouver péniblement les mises à jour manquées : on relance l'outil de reconstruction, qui relit l'état courant des commandes et réécrit chaque ligne de résumé. La vue redevient exacte en une passe, sans que la source ait jamais couru le moindre risque. C'est la tranquillité que procure un dérivé : on peut se tromper sur la vue, jamais sur ce dont elle dérive.
Là où l'on parle au monde extérieur
C'est le pendant exact de la règle du volet précédent. Puisque la commande ne sort pas de sa transaction, les intégrations avec le dehors se font en aval de la décision — dans les réactions aux événements, jamais dans la commande : c'est là que vivent les appels aux API tierces, l'envoi des mails, l'indexation dans un moteur de recherche, la synchronisation vers un autre système.
Le mécanisme est celui qu'on a esquissé : le write model émet un événement — un signal — une fois sa décision actée. En aval, deux familles distinctes s'en nourrissent, qu'il vaut la peine de ne pas confondre. Les projections mettent à jour les vues de lecture — c'est le read model proprement dit. Les réactions (qu'on appelle aussi consommateurs d'événements ou process managers) font, elles, le travail incertain vers le dehors, celui qui dépend d'un partenaire parfois lent, parfois absent, parfois contradictoire. Les deux vivent après la décision, mais ce ne sont pas la même responsabilité : l'une restitue, l'autre intègre. Ces réactions sont idempotentes et rejouables, parce que le dehors, lui, ne promet rien : un mail peut partir deux fois, une API tierce peut expirer. On assume cette incertitude là où elle ne met pas la cohérence du domaine en jeu — après la décision, jamais pendant.
C'est une répartition des rôles limpide une fois qu'on la voit : le write model garantit ce qui doit être vrai ; le read model, et tout ce qui pend à ses signaux, se charge de ce qui doit être fait savoir. L'un ne bouge pas de son enceinte ; l'autre vit dehors, exposé, et s'en accommode.
Un cas concret. Quand une commande passe à « expédiée », on veut prévenir le client par mail et pousser le suivi vers le transporteur. Ni l'un ni l'autre ne se fait dans la commande : celle-ci acte l'expédition et émet le signal correspondant, rien de plus. En aval, un worker capte ce signal, appelle le service d'e-mailing, interroge l'API du transporteur — et si l'un de ces appels échoue, il retente, sans que la commande, validée depuis longtemps, en soit affectée. Le client recevra son mail avec une seconde de retard plutôt que jamais, et surtout jamais pour une expédition qui n'aurait pas eu lieu. Le dehors est incertain ; on l'a rangé là où son incertitude ne contamine pas la décision.
Le contrat de fraîcheur
Reste une contrepartie qu'on aurait tort de passer sous silence. Comme la projection se propage souvent en différé, la vue peut accuser un léger décalage sur la source. Une lecture sert alors un état qui n'est plus tout à fait à jour : la commande vient de passer à « expédiée », mais le résumé affiche encore « en préparation », le temps que la projection rattrape.
Ce décalage n'est pas un défaut à cacher ; c'est un contrat à rendre explicite. Une interface honnête dit ce qu'elle sert : une donnée fortement cohérente (lue à la source), éventuellement cohérente (lue sur une projection qui peut retarder), ou en cours de convergence. Le danger n'est pas le retard lui-même — souvent imperceptible — mais son déni : un écran qui laisse croire à une fraîcheur qu'il ne garantit pas. Nommer le contrat, c'est déjà la moitié du travail.
Concrètement, cela peut tenir à un simple libellé — « mis à jour il y a quelques secondes » — ou au choix délibéré de lire à la source les données dont l'obsolescence serait inacceptable, tout en servant depuis une projection celles qui tolèrent le retard. Le compteur de messages non lus peut retarder d'une seconde sans que personne n'en pâtisse ; l'écran qui confirme un règlement, lui, doit refléter la décision à l'instant où elle est prise. Concevoir un read model, ce n'est donc pas supprimer le décalage partout — ce serait ruineux — mais décider, donnée par donnée, laquelle exige la fraîcheur et laquelle s'en passe. Ce discernement-là est un acte d'architecture, pas un réglage technique.
Point contesté. Faut-il propager de façon asynchrone, ou peut-on projeter dans la même transaction que l'écriture ? Les deux existent. Une projection synchrone supprime le retard, au prix d'un couplage plus fort et d'une transaction plus lourde ; une projection asynchrone découple et passe à l'échelle, au prix d'une cohérence éventuelle. Rien dans CQRS n'impose l'un ou l'autre — c'est un arbitrage, pas une obligation. Nous le reprendrons au volet suivant, celui de la frontière.
Deux natures, un même système
Le read model complète le write model sans lui ressembler. L'un est normalisé, l'autre dénormalisé ; l'un unique, l'autre multiple ; l'un gardien de la vérité, l'autre dérivé jetable ; l'un cloitré dans sa transaction, l'autre tourné vers le dehors. Ce n'est pas une redondance, c'est une division du travail : à chacun sa responsabilité, décider d'un côté, restituer et propager de l'autre.
Mais entre les deux, il y a un espace — le trajet de la projection, avec ses délais et ses ratés. Cet espace n'est pas un détail d'implémentation : c'est une véritable frontière, avec tout ce qu'une frontière suppose de retards, d'indisponibilités et d'écarts temporaires. C'est elle que le prochain volet regarde en face, avec le vocabulaire qui permet d'en raisonner proprement.
Laisser un commentaire
Vous devez être connecté pour pouvoir laisser un commentaire.