Feature flags : un guide pratique
Un feature flag est une condition placée autour d'un morceau de code qui permet d'activer ou de désactiver ce code (pour tout le monde, pour un sous-ensemble d'utilisateurs, ou progressivement) sans livrer un nouveau déploiement. L'idée est si simple qu'il est tentant de semer des if dans toute la base de code, et c'est précisément comme cela que les feature flags dérapent à grande échelle. Bien faits, ils découplent le déploiement du code de sa mise en production ; faits à la légère, ils deviennent un labyrinthe permanent de branches mortes que personne n'ose supprimer.
La version la plus simple possible
Au fond, un feature flag n'est qu'un test booléen :
if (flags.isEnabled('new-checkout-flow', { userId: user.id })) {
return renderNewCheckout();
}
return renderLegacyCheckout();
Le point intéressant n'est pas le if, mais flags.isEnabled. D'où vient cette réponse, et à quelle vitesse peut-elle changer ?
Trois façons d'évaluer un flag
Configuration statique. Un fichier JSON ou YAML, ou des variables d'environnement, vérifiés au moment du déploiement. C'est le plus simple à implémenter, mais changer un flag exige un redéploiement, ce qui annule une grande partie de l'intérêt (un basculement rapide et peu risqué).
const flags = { 'new-checkout-flow': process.env.FF_NEW_CHECKOUT === 'true' };
Un service de flags distant, interrogé (et mis en cache) à l'exécution, de sorte qu'un flag peut être basculé depuis un tableau de bord sans toucher du tout au pipeline de déploiement :
async function isEnabled(flagKey, context) {
const cached = cache.get(flagKey);
if (cached) return evaluate(cached, context);
const flag = await flagService.fetch(flagKey);
cache.set(flagKey, flag, { ttl: 30_000 });
return evaluate(flag, context);
}
Un déploiement progressif par pourcentage ou par attribut, où le flag n'est pas seulement activé ou désactivé mais cible une tranche du trafic : 5 % des utilisateurs, ou tous ceux dont plan === 'enterprise' :
function evaluate(flag, context) {
if (!flag.enabled) return false;
if (flag.rolloutPercent != null) {
const bucket = hashToPercent(context.userId, flag.key);
return bucket < flag.rolloutPercent;
}
if (flag.rules) {
return flag.rules.every((rule) => matchesRule(rule, context));
}
return true;
}
function hashToPercent(userId, flagKey) {
const hash = simpleHash(`${userId}:${flagKey}`);
return hash % 100;
}
La partie hashToPercent compte plus qu'il n'y paraît : hacher sur userId (plutôt que de tirer au hasard à chaque requête) garantit qu'un même utilisateur tombe toujours du même côté du flag d'un chargement de page à l'autre ; sinon, un utilisateur pourrait voir le nouveau tunnel de paiement sur une requête et l'ancien sur la suivante, ce qui déroute et rend les bugs presque impossibles à reproduire.
À quoi servent vraiment les flags
Les flags de release découplent le déploiement de la mise en production : on fusionne et on livre le code désactivé, puis on l'active quand on est prêt, indépendamment de la prochaine fenêtre de déploiement. Les flags d'expérience lancent un test A/B : un pourcentage d'utilisateurs reçoit la variante B, et l'on mesure la différence. Les flags d'exploitation sont des interrupteurs d'urgence pour dégrader proprement : désactiver une fonctionnalité lente et non critique (recommandations, widget tiers) sous charge sans déploiement. Les flags de permission réservent une fonctionnalité à certains utilisateurs ou formules, comme une fonction réservée aux clients entreprise ; pour un contrôle d'accès durable, un vrai système de droits (entitlements) est cependant généralement préférable à un flag censé être temporaire.
Cette distinction compte, car chaque type a un cycle de vie différent. Les flags d'exploitation peuvent vivre éternellement. Les flags de release et d'expérience, non.
La partie que tout le monde saute : les supprimer
Un flag qui est à 100 % depuis six mois et dont plus aucun chemin de code ne vérifie l'ancien comportement n'apporte plus de sécurité : c'est du poids mort qui rend la base de code plus difficile à lire et chaque changement futur plus risqué, car il y a désormais deux chemins à raisonner au lieu d'un.
// Six months later, this still exists:
if (flags.isEnabled('new-checkout-flow')) {
return renderNewCheckout();
}
return renderLegacyCheckout(); // dead code nobody has run in months, still shipped, still maintained
Considérez le nettoyage des flags comme faisant partie de la définition de « terminé » d'une fonctionnalité, et non comme un suivi facultatif : une fois un flag entièrement déployé et stable depuis une période raisonnable, supprimez le test du flag et la branche morte dans la foulée. Certaines équipes l'imposent avec un linter qui signale tout test de flag plus ancien que N jours, ou avec un tableau de bord qui met en avant les flags que personne n'a basculés récemment.
Un piège : les flags qui interagissent
Deux flags d'apparence indépendante peuvent se combiner en un état que personne n'a testé :
if (flags.isEnabled('new-checkout-flow')) {
if (flags.isEnabled('new-payment-provider')) {
return renderNewCheckoutWithNewPayments(); // did anyone actually test this combination?
}
return renderNewCheckoutWithOldPayments();
}
Chaque flag supplémentaire double le nombre de chemins de code dans les parties de l'application qu'il touche. Deux flags font quatre chemins ; trois en font huit. Gardez des flags à portée étroite et de courte durée, et décidez délibérément quelles combinaisons vous devez réellement prendre en charge : tester toutes les combinaisons n'est pas réaliste dès que plus de deux flags sont actifs dans la même zone de code.
À retenir
Les feature flags vous donnent la possibilité de livrer du code sans le mettre en production, de déployer progressivement et de couper une fonctionnalité défaillante sans redéployer : des propriétés réellement précieuses. Le mode d'échec n'est pas le flag lui-même, c'est le fait de le traiter comme permanent au lieu de temporaire : chaque flag ajouté est une dette qu'il faut rembourser en le supprimant un jour, et une base de code qui ne rembourse jamais cette dette finit plus difficile à comprendre qu'une base sans aucun flag.