Schema registry : versionner un message Kafka sans casser les consommateurs existants

Sur MissionMatch, plusieurs consommateurs indépendants lisent les mêmes événements Kafka, comme décrit dans l'article sur le découplage event-driven. Ce que cet article-là laisse de côté, c'est une question qui finit toujours par se poser : que se passe-t-il le jour où il faut changer la forme du message lui-même ?
Le contrat qu'on oublie parce qu'il est implicite
Un topic Kafka ne dit rien sur la structure de ses messages. Producteur et consommateurs s'accordent tacitement sur un format, souvent documenté nulle part ailleurs que dans le code du producteur. Renommer un champ, changer son type, ou en supprimer un qu'un consommateur lisait encore : rien n'empêche techniquement le producteur de le faire, et rien ne prévient les consommateurs avant que ça casse en production, généralement au pire moment.
Un schema registry (Confluent Schema Registry, ou l'équivalent AWS Glue
Schema Registry) rend ce contrat explicite : chaque schéma (souvent en Avro,
parfois Protobuf ou JSON Schema) est enregistré, versionné, et associé à un
subject (typiquement <topic>-value). Le producteur ne peut plus publier
un message dont le schéma n'a pas été validé contre les règles de
compatibilité définies pour ce subject.
Sur MissionMatch, les événements sont aujourd'hui sérialisés en JSON brut
(JsonSerializer/JsonDeserializer de Spring Kafka), sans schema registry
devant le topic : exactement le point aveugle que cet article explore. Le
projet fonctionne, parce qu'un seul dépôt de code gère producteurs et
consommateurs à la fois, donc un changement de format est visible immédiatement
à la compilation. Ce filet de sécurité disparaît dès que des équipes ou des
dépôts séparés possèdent chacun un bout de la chaîne : c'est précisément le
moment où un schema registry cesse d'être une option.
Producteur Schema Registry Topic Kafka
┌──────────┐ 1. valider ┌────────────────┐ ┌──────────┐
│ nouveau │──────────────▶│ schéma v3 │ │ message │
│ message │◀──────────────│ compatible avec │───────────▶│ + id │
└──────────┘ 2. schema id │ v1 et v2 ? │ 3. publier│ schéma │
└────────────────┘ └──────────┘
▲
│ 4. récupérer le schéma par id
│
┌──────────────┐
│ Consommateur │
└──────────────┘
Le consommateur ne reçoit jamais le schéma complet dans le message : juste un identifiant compact, qu'il utilise pour aller chercher le schéma exact auprès du registry. C'est ce qui permet de faire évoluer des messages sans alourdir chaque message d'un schéma redondant.
Les quatre modes de compatibilité, et quand choisir lequel
Le registry n'empêche pas les changements : il les autorise ou les refuse selon une règle de compatibilité configurée par subject, vérifiée automatiquement à chaque tentative d'enregistrement d'un nouveau schéma.
| Mode | Ce qu'il garantit | Cas d'usage typique |
|---|---|---|
BACKWARD | Les consommateurs qui utilisent le nouveau schéma peuvent lire les messages écrits avec l'ancien | Ajouter un champ optionnel, le cas le plus fréquent |
FORWARD | Les consommateurs qui utilisent l'ancien schéma peuvent lire les messages écrits avec le nouveau | Supprimer un champ optionnel que personne ne lisait plus |
FULL | Les deux garanties à la fois (BACKWARD et FORWARD) | Topics critiques, avec beaucoup de consommateurs qu'on ne contrôle pas tous |
NONE | Aucune vérification | À éviter en dehors d'un prototype jetable |
BACKWARD est le mode par défaut le plus courant, parce que le cas le plus
fréquent en pratique est justement celui-là : un consommateur redéployé
récemment doit continuer à lire les messages plus anciens encore présents
dans le topic (Kafka conserve l'historique, contrairement à une file de
messages classique).
Un exemple concret : ce qui casse, ce qui ne casse pas
Prenons un schéma inspiré du vrai événement MissionPublishedIntegrationEvent
de MissionMatch, qui transporte aujourd'hui un identifiant, les compétences
requises, un taux journalier et une date de début :
// Schéma v1
{
"type": "record",
"name": "MissionPublished",
"fields": [
{ "name": "missionId", "type": "string" },
{ "name": "requiredSkills", "type": { "type": "array", "items": "string" } },
{ "name": "dailyRateAmount", "type": "string" },
{ "name": "startDate", "type": "string" }
]
}
Ajouter un champ optionnel avec une valeur par défaut est compatible
BACKWARD : un consommateur qui lit un vieux message avec le nouveau
schéma utilisera simplement la valeur par défaut pour le champ absent.
// Schéma v2 : compatible BACKWARD
{
"type": "record",
"name": "MissionPublished",
"fields": [
{ "name": "missionId", "type": "string" },
{ "name": "requiredSkills", "type": { "type": "array", "items": "string" } },
{ "name": "dailyRateAmount", "type": "string" },
{ "name": "startDate", "type": "string" },
{ "name": "durationInDays", "type": ["null", "int"], "default": null }
]
}
En revanche, renommer missionId en id, ou changer son type de string
à long, casse la compatibilité : ce ne sont pas des évolutions, ce sont
des schémas différents du point de vue du registry, qui refusera
l'enregistrement si le mode configuré l'exige.
Ce que le schema registry ne fait pas
Il vérifie la compatibilité structurelle du schéma, pas la compatibilité
sémantique. Un champ status qui passait de "active" à "draft" reste
structurellement valide (c'est toujours une chaîne de caractères), mais un
consommateur qui ne connaît pas encore la nouvelle valeur possible peut
malgré tout se comporter de façon incorrecte. Le registry protège contre
les changements de forme, pas contre les changements de sens : les tests de
contrat entre producteur et consommateurs restent nécessaires en
complément, pas en remplacement.
Pourquoi ça vaut le coût avant même d'en avoir besoin
La tentation, en solo ou en petite équipe, est de repousser le schema registry à « quand on aura plusieurs équipes ». Le problème, c'est que le coût de son absence ne se voit pas avant le premier changement de schéma en production avec des consommateurs déjà déployés séparément : à ce moment-là, il est trop tard pour l'ajouter sans risque, puisque le passé du topic contient déjà des messages dans l'ancien format. La bonne discipline est de l'introduire dès que deux dépôts de code distincts touchent le même topic, pas après.