Article

CAP à toutes les échelles, et comment trancher

CAP vaut à toute frontière — DDD, CQRS, monolithe, micro-services. Et l'arbitrage cohérence/disponibilité se tranche par opération, pas en bloc.

On classe CAP au rayon des bases de données distribuées, comme un souci réservé aux spécialistes du stockage réparti. C'est le ranger trop loin : le théorème parle de toute frontière — et à chacune, il ne se tranche pas en bloc, mais opération par opération.
La même partition à cinq frontières : entre domaines DDD, entre write model et read model, entre modules, entre services, avec l'externe.
La même partition se rejoue à chaque frontière de communication, du monolithe modulaire aux micro-services.

« Pas de distribué chez nous, donc pas de ces problèmes » : la phrase rassure, et elle est fausse. Un monolithe, un seul déploiement, une seule base — puis un jour le module de facturation cherche un client que le module d'inscription vient de créer, et ne le trouve pas. Aucun réseau en cause : deux responsabilités, dans le même processus, n'ont pas encore la même vision de l'état. Au sens strict, ce n'est pas une partition réseau — ni nœuds distants, ni câble. Mais l'effet est celui d'une partition, et l'arbitrage imposé, identique : une partition en miniature, à l'intérieur de la machine.

Partout où il y a une frontière

C'est le déplacement qu'opère une lecture juste de CAP : le vocabulaire de la cohérence, de la disponibilité et de la partition ne vaut pas que pour les bases réparties. Il vaut partout où existe une frontière de communication — partout où une opération franchit une séparation de responsabilités, de transactions, de stockage ou d'exécution. Ces frontières sont plus nombreuses qu'on ne croit :

  • entre domaines DDD : les bounded contexts sont des frontières d'autonomie, où le désalignement temporaire doit être assumé ;
  • entre write model et read models en CQRS : la décision protège l'invariant d'un côté, la lecture est une projection de l'autre, propagée en différé donc parfois en retard ;
  • entre modules d'un monolithe modulaire : dès qu'un module dépend d'un autre via événements, jobs, files ou caches, on retrouve couplage temporel et pannes partielles ;
  • entre micro-services : les mêmes contraintes, plus visibles, donc mieux respectées ;
  • avec les systèmes externes : API tierces, serveur mail, passerelle de paiement, qu'on ne contrôle pas et qui répondent tard, mal, ou pas du tout.

D'où un renversement utile : passer en micro-services n'introduit pas les problèmes de partition, cela les rend impossibles à ignorer. À l'inverse, le déploiement unique ne les supprime pas — il les masque. Le monolithe ne paie pas la cohérence comptant parce que le code est colocalisé ; il cache seulement le moment où l'état source et l'état dérivé divergent, jusqu'à ce qu'un bug le rappelle.

Cette frontière dont parle CAP, on l'a déjà rencontrée : une dépendance orientée — le fait que A connaisse B — est une frontière de communication. La question « qui connaît qui ? » décide du sens du couplage ; la question CAP décide de ce qui arrive à ce lien quand il devient lent ou incertain.

Trancher par opération, pas par système

Si la partition guette à chaque frontière, « ce système est CP » ne dit pas grand-chose : la phrase répond à l'échelle du système à une question qui ne se pose qu'à l'échelle de l'opération. Une même application abrite un débit qui refuse de s'exécuter sur un solde incertain et un compteur de vues que personne ne vérifie au centime près. Les soumettre au même arbitrage, c'est se tromper d'échelle, et payer sur l'un la rigueur que seul l'autre exigeait.

La bonne unité de décision n'est pas le système, c'est l'opération, l'invariant qu'elle protège et son niveau de criticité. Pour chacune, et seulement sous partition, trois conduites :

Trois conduites sous partition : bloquer un débit, dégrader l'affichage d'un solde, tolérer un compteur de vues.
Sous partition, l'arbitrage se décide opération par opération : bloquer, dégrader ou tolérer — selon l'invariant en jeu.
  • bloquer — refuser l'opération tant qu'on ne peut garantir l'invariant. Le bon choix quand l'écart est inacceptable : un débit bancaire ne s'autorise pas « dans le doute ».
  • dégrader — répondre avec un état qu'on sait possiblement obsolète, et le signaler. Un solde daté, marqué comme tel, vaut mieux qu'une page d'erreur.
  • tolérer — accepter l'écart, qui se rattrapera sans dommage. Un compteur de vues converge ; personne n'en souffre entre-temps.

Aucune conduite n'est meilleure en soi : chacune est juste pour certaines opérations, fausse pour d'autres. Ce qui les départage n'est pas la mode technique — « eventual consistency partout », « tout verrouiller » — mais le besoin métier : quel écart est tolérable, sur cette opération, et pendant combien de temps ?

De là une exigence souvent négligée : rendre le mode dégradé explicite. Décider de dégrader ou de tolérer ne suffit pas ; il faut écrire ce que l'utilisateur peut encore faire quand la dépendance est lente ou absente — quelle lecture reste servie et marquée datée, quelle écriture est mise en file, quel geste est refusé et avec quel message. Un mode dégradé qu'on n'a pas pensé n'en est pas un : c'est un comportement non maîtrisé qui se découvre en production. Une architecture lucide ne colle pas une étiquette « CP » ou « AP » : elle tient une matrice, où chaque flux porte sa conduite et sa tolérance.

Mais bloquer, dégrader ou tolérer, c'est accepter qu'à un moment des états aient divergé. Reste à les ramener ensemble une fois la partition refermée, et à rattraper ce qui a déjà eu lieu : réconciliation et compensation, qu'aborde le prochain article.

Laisser un commentaire

Vous devez être connecté pour pouvoir laisser un commentaire.