Évolution des données en microservices : éviter les changements cassants grâce à la compatibilité

Évolution des données en microservices : éviter les changements cassants grâce à la compatibilité

Table des matières

Passer d’un monolithe à une architecture microservices offre une réelle agilité. Mais ce changement ouvre aussi la porte à un défi majeur : la compatibilité des données.

Quand plusieurs services communiquent, une question critique se pose : comment échanger des données sans provoquer de breaking changes à chaque mise à jour ?

Cette problématique dépasse la simple performance. Elle repose d’abord sur trois piliers essentiels :

  • Le Protocole de communication (par ex. HTTP pour les requêtes, AMQP pour la messagerie)
  • Le Mécanisme de sérialisation des données (la manière d’encoder et décoder les données)
  • Les Frameworks/bibliothèques utilisés (comme gRPC, Kafka ou tout simplement REST)
Pile de communication des microservices
Pile de communication des microservices

Le rôle crucial de la sérialisation

Entre JSON, Protobuf, Avro et d’autres formats, chaque choix implique un compromis sur :

  1. La vitesse d’encodage/décodage
  2. La taille des données transmises
  3. La lisibilité humaine
  4. La disponibilité cross-langage
  5. L’évolutivité du format

Les quatre premiers relèvent de la performance et de la logistique.

Mais l’évolutivité conditionne la résilience : si vos données ne peuvent pas évoluer sans casser d’anciens services, votre architecture devient fragile.

pourquoi les déploiements cassent les systèmes

L’évolutivité est la capacité à modifier la structure des données échangées sans provoquer de défaillance.

On pourrait penser qu’il suffit de mettre à jour tous les services simultanément. Dans un monde idéal, ce serait vrai. En pratique, c’est rarement possible :

  • Les déploiements sont progressifs. La propagation complète prend du temps.
  • Les retours arrière existent. Un service peut revenir à une version précédente si un bug survient.
  • Certains clients prennent du retard. Les partenaires externes ou les applications utilisateurs peuvent mettre des jours ou semaines à adopter votre nouveau format.

Cela signifie que vous allez forcément gérer des versions coexistantes : des services avec nouvelle structure, et d’autres avec l’ancienne.

Les deux règles d’or : compatibilité ascendante et descendante

Pour garantir une évolution continue, vous devez assurer :

  • Compatibilité descendante (backward compatibility) : un service mis à jour doit pouvoir lire des données produites par un service plus ancien.

  • Compatibilité ascendante (forward compatibility) : un service plus ancien doit pouvoir lire (ou ignorer sans erreur) les données produites par un service plus récent Sans ces deux principes de compatibilité, vos déploiements risquent d’introduire des pertes de données ou des défaillances en cascade.

CONSOMMATEUR (ANCIEN V1) CONSOMMATEUR (NOUVEAU V2)
PRODUCTEUR (V1) Fonctionne (cas de base) Compatibilité descendante (gère les données anciennes)
PRODUCTEUR (V2) Compatibilité ascendante (tolère les données nouvelles) Fonctionne (cas nouveau)

Stratégie 1 : évolution de schéma compatible

Cette stratégie fonctionne quand les services partagent un schéma commun, un plan formel de la donnée. Des outils comme Protobuf, Avro ou Thrift imposent des règles pour éviter les changements cassants.

Info

Même si JSON est lisible, il est faiblement typé et ne gère pas automatiquement les champs inconnus. Cela rend l’évolution compatible difficile. C’est pourquoi des formats fortement typés comme Protobuf et Avro sont privilégiés en communication interne.

Fonctionnement

Vous faites évoluer votre schéma existant, sans changer l’API visible :

  • Champs inconnus : La bibliothèque de sérialisation garantit que les services consomment sans erreur les champs qu’ils ne connaissent pas. Cela assure la compatibilité ascendante et descendante.

  • Ajouter un champ : Le champ doit être optionnel. Les services plus anciens l’ignorent automatiquement.
    Les nouveaux services doivent accepter qu’il soit absent dans les anciens messages.

  • Supprimer un champ : Les nouveaux services cessent de l’envoyer. Les anciens utilisent la valeur par défaut ou un mécanisme de secours.
    Les nouveaux doivent aussi gérer sa disparition dans les anciens messages.

Warning

Modifier un champ existant est extrêmement risqué. Pour préserver la compatibilité descendante, ne changez jamais :

  • le type de données (ex. integerstring),
  • la sémantique (réutiliser un numéro de champ pour autre chose),
  • les dépendances logiques (rendre un champ indispensable alors qu’un service ancien ne l’envoie pas).

Ces changements rendent les déploiements impossibles sans rupture.

Évolution de schéma compatible

Exemple (Protobuf)

 1// v1
 2message User {
 3  string username = 1;
 4  string email = 2;
 5  int32 age = 3;
 6}
 7
 8// v2 (Évolution compatible)
 9message User {
10  string username = 1;
11  string email = 2;
12  int32 age = 3 [deprecated = true]; // Les anciens services peuvent encore lire 'age'
13  string birth_date = 4; // Les nouveaux services utilisent 'birth_date'
14}
Avantages Inconvénients
Un seul schéma à maintenir. Peu adapté aux gros changements fonctionnels.
Compatibilité gérée automatiquement par la bibliothèque. Accumulation possible de nombreux champs dépréciés.

Stratégie 2 : versionnement d’API

Quand le changement est trop important ou incompatible, vous publiez une nouvelle version d’API. Les versions coexistent jusqu’à la migration complète.

Fonctionnement

  • Vous gardez l’ancienne API active (ex. /v1/userdata).
  • Vous publiez une nouvelle version (ex. /v2/userdata).
  • Les clients migrent progressivement.
  • Vous supprimez l’ancienne version une fois tout le monde transféré.

Info

Le versionnement peut être fait via le chemin d’URL (/v2/...) ou via un en-tête (Accept, X-API-Version).

Version Endpoint Changement
v1 /v1/userdata détails initiaux
v2 /v2/userdata modification d’un champ majeur
Versionnement d’API
Avantages Inconvénients
Flexibilité totale : tout peut changer. Plusieurs API à maintenir en parallèle.
Les anciens clients sont totalement isolés. Charge accrue en test, déploiement et maintenance.

Choisir la bonne approche

Facteur Évolution de schéma Versionnement d’API
Effort de maintenance Faible Élevé (multiples API)
Flexibilité Limitée aux changements compatibles Changements structurels illimités
Idéal pour Microservices internes API publiques accessibles par des clients externes

En résumé :

  • Si vos changements sont additifs, privilégiez l’évolution de schéma (Protobuf/Avro). C’est le plus simple.
  • Si vous avez des clients externes ou un refactoring majeur, utilisez le versionnement d’API.
  • Beaucoup d’équipes combinent les deux : — évolution de schéma en interne, — versionnement d’API pour l’externe.

Conclusion

L’évolution des structures de données est inévitable dans un système de microservices vivant.

En concevant dès le départ avec la compatibilité ascendante et descendante, vous permettez des déploiements sereins, des mises à jour progressives et une évolution sans chaos.

Concevoir pour le changement : la stratégie de compatibilité

Articles Connexes

Explication Illustrative de la Faute, de l'Erreur, de la Défaillance, du Bug et du Défaut Logiciel

Explication Illustrative de la Faute, de l’Erreur, de la Défaillance, du Bug et du Défaut Logiciel

Les logiciels ne se comportent pas toujours comme prévu. Des erreurs dans l’implémentation ou dans la spécification des exigences peuvent …

Lire la suite