jguillaumesio
architecturewebdev

Pourquoi le code parfait est moins maintenable

J'ai passé 3 heures à relire une PR de 200 lignes gérant chaque cas limite. Une version de 40 lignes avec un TODO la bat. Voici pourquoi le code parfait vous ralentit.

Comparaison code parfait contre code maintenable

J’ai passé 3 heures à relire une pull request de 200 lignes le mois dernier.

Elle était propre. Chaque cas limite géré. Logique de retry, feature flags, types d’erreur complets, validation personnalisée. Le code était techniquement parfait.

Je l’ai rejetée. On a livré une version de 40 lignes avec un commentaire TODO à la place.

Voici la partie dont personne ne vous parle en ingénierie : le code parfait est moins maintenable que le code suffisant. Plus vous passez de temps à rendre quelque chose à toute épreuve, plus il devient difficile à changer plus tard. Et vous aurez besoin de le changer.

Des exemples concrets de code parfait qui nous a ralentis

Exemple 1 : le formulaire de contact avec un disjoncteur

Un collègue a soumis une PR pour un formulaire de contact. Notre site a 12 utilisateurs.

La PR incluait des types d’erreur personnalisés pour 6 modes de défaillance, une logique de retry avec backoff exponentiel, un pattern de disjoncteur (circuit breaker), une validation d’entrée avec 15 motifs regex, et des feature flags pour un déploiement progressif.

On ne fait pas de déploiements progressifs. On livre à 12 utilisateurs. Si ça casse, on corrige en 10 minutes.

Notre fournisseur d’e-mails a 99,99 % de disponibilité. Le disjoncteur protégeait contre un mode de défaillance qu’on n’a jamais vu en 2 ans. Il ajoutait 40 lignes de logique morte.

On a livré une version de 40 lignes : valider l’e-mail, appeler l’API, ajouter un TODO pour le retry si le taux d’échec dépasse 5 %. La relecture de la PR a pris 15 minutes. On a livré le jour même.

Exemple 2 : le système de permissions pour une équipe de 3 personnes

J’ai un jour construit un système RBAC complet pour un outil interne. Rôles, permissions, middleware, interface d’admin, journal d’audit. Ça a pris 2 semaines.

L’outil était utilisé par 3 personnes. Toutes avaient besoin d’accéder à tout. Le système RBAC gérait un scénario (restreindre l’accès par utilisateur) qui n’a jamais été demandé ni utilisé.

6 mois plus tard, on a eu besoin de changer le flux d’authentification. J’ai passé 3 jours à démêler les abstractions RBAC avant de pouvoir faire un simple changement. Une vérification en dur “cet utilisateur est-il dans l’équipe ?” aurait pris 30 secondes à modifier.

Le système de permissions parfait faisait 800 lignes. Le vrai besoin, c’était if (user.email.endsWith('@ourcompany.com')).

Exemple 3 : le pipeline de déploiement qui déployait 3 fois par mois

J’ai construit un pipeline CI/CD complet avec déploiements blue-green, analyse de canari automatisée, déclencheurs de rollback et health checks multi-régions.

On déployait 3 fois par mois sur un seul VPS avec un seul pod. Le blue-green voulait dire faire tourner deux environnements identiques pour 12 utilisateurs canaris. L’analyse de canari vérifiait des métriques qu’on ne collectait pas.

Le pipeline prenait 12 minutes à s’exécuter. Un git push + npm run build + kubectl rollout restart prend 45 secondes.

J’ai supprimé le pipeline après 4 mois. On est revenu à un script de déploiement d’une ligne. La fréquence de déploiement a augmenté parce que les gens ont arrêté d’attendre le pipeline.

Exemple 4 : le système de config qui configurait 5 choses

J’ai construit un système de config hiérarchique : variables d’environnement, fichiers de config, service de config distant, flags CLI, avec schémas de validation et coercition de types.

L’application avait 5 valeurs de config. URL de la base, port, niveau de log, clé d’API, mode debug.

Le système de config faisait 300 lignes. Lire une seule valeur demandait de comprendre l’ordre de précédence sur 4 couches. Les nouveaux développeurs passaient 20 minutes à trouver où changer le numéro de port.

Un simple config.json avec 5 clés aurait fait 8 lignes. Zéro courbe d’apprentissage.

Exemple 5 : le système d’événements qui dispatchait 2 événements

Pour un blog, j’ai construit un système d’événements avec bus d’événements, handlers typés, middleware, files de retry et gestion des lettres mortes.

Le blog dispatchait 2 événements : “article publié” et “commentaire soumis”. Chacun avait un seul écouteur.

Le système d’événements faisait 450 lignes. Ajouter un nouveau type d’événement voulait dire créer une classe d’événement, une interface de handler, enregistrer le handler et écrire un test pour le middleware. Ce qui était avant un appel de fonction demandait maintenant de comprendre 6 fichiers.

Je l’ai arraché. Maintenant, publier un article appelle sendNotification() directement. Le code fait 10 lignes. Toute l’équipe le comprend immédiatement.

Exemple 6 : le chiffrement fait maison

Une équipe que j’ai conseillée avait construit sa propre couche de chiffrement à partir de primitives cryptographiques brutes. Ils avaient une dérivation de clé personnalisée, leur propre schéma de padding et un protocole d’échange de clés bricolé à la main.

Ça a pris 3 mois à construire. Il y avait 2 vulnérabilités critiques trouvées dès le premier audit de sécurité.

Ils l’ont remplacé par une bibliothèque standard (des wrappers OpenSSL) en 2 jours. Le remplacement faisait 200 lignes de code bien testé et audité. Leur version faisait 4 000 lignes de cryptographie maison que personne ne pouvait vérifier.

C’est la classique erreur du “ne réinventez pas la roue”, mais c’est plus subtil qu’il n’y paraît. L’équipe n’était pas bête. Ils comprenaient les primitives. Ils ne comprenaient simplement pas que comprendre les primitives n’est pas la même chose que les implémenter correctement.

Exemple 7 : l’ORM maison parce qu‘“on pourrait changer de base”

Une startup avec laquelle j’ai travaillé avait construit une couche ORM maison. La raison : “On pourrait avoir besoin de passer de PostgreSQL à MySQL un jour.”

L’ORM faisait 2 000 lignes. Il gérait 60 % des requêtes dont ils avaient besoin. Les 40 % restants demandaient du SQL brut de toute façon, contournant complètement l’ORM.

Ils n’ont jamais changé de base. 3 ans plus tard, ils maintenaient toujours l’ORM, corrigeant des bugs dans son générateur de requêtes et contournant ses limites. Un ORM standard (Prisma, Drizzle, voire pg brut) aurait demandé zéro maintenance.

L’ORM maison n’a pas seulement coûté les 2 000 lignes à construire. Il a coûté 3 ans de maintenance continue pour un problème qui ne s’est jamais matérialisé.

Exemple 8 : l’architecture multi-locataire pour un seul locataire

Un projet SaaS que j’ai vu était architecturé pour le multi-locataire dès le premier jour. Chaque requête avait un filtre tenant_id. Chaque table avait une colonne tenant_id. Tout le système d’authentification était construit autour de l’isolation des locataires.

Ils avaient 1 client. Un locataire. L’architecture multi-locataire ajoutait 30 % de surcoût à chaque requête et rendait la base de code nettement plus difficile à comprendre.

Quand ils ont enfin eu un deuxième client, le multi-locataire fonctionnait. Mais il a fallu 18 mois pour en arriver là, et pendant ces 18 mois, chaque développeur devait comprendre et contourner un système qui n’apportait aucune valeur.

Une version mono-locataire aurait fait la moitié du code, été deux fois plus rapide, et aurait pu être migrée vers le multi-locataire quand ils ont vraiment eu un deuxième client.

Pourquoi le code parfait est un piège

Vous résolvez des problèmes qui n’existent pas encore

Chaque exemple ci-dessus partage le même schéma : le code gérait des scénarios qui ne se sont jamais matérialisés. Le disjoncteur ne s’est jamais déclenché. Les restrictions RBAC n’ont jamais servi. L’analyse de canari n’a jamais attrapé de régression. La base n’a jamais été changée. Le deuxième locataire n’est jamais venu.

Le coût n’était pas seulement d’écrire le code. C’était de le maintenir, de le relire et de le contourner pendant des mois ou des années jusqu’à ce que quelqu’un admette enfin qu’il était inutile.

Le code parfait est plus difficile à supprimer

Quand vous écrivez 200 lignes de système de config, vous créez des dépendances à travers la base de code. Douze fichiers importent votre module de config. Huit tests vérifient son comportement. Votre script de déploiement dépend de son schéma.

Maintenant, quand vous voulez simplifier, vous ne pouvez pas juste le supprimer. Vous devez migrer chaque consommateur, mettre à jour chaque test et espérer que rien ne casse.

Le config.json de 8 lignes ? Remplacez-le en 5 minutes. Zéro migration.

Le temps de relecture croît avec la complexité

J’ai suivi les temps de relecture des PR sur 6 mois sur notre projet. Le schéma était constant :

MétriquePR parfaites (150+ lignes)PR suffisantes (<50 lignes)
Temps de relecture moyen2,8 heures18 minutes
Taux de révision45 %12 %
Bugs trouvés en production3,2 par PR0,8 par PR
Délai de livraison4,2 jours0,8 jour
Temps pour supprimer/réécrire6 heures45 minutes

Les PR “parfaites” causaient plus de bugs, pas moins. Les relecteurs ne pouvaient pas garder leur concentration sur des changements de 200 lignes. Des problèmes passaient à travers. Et quand les besoins changeaient (ils changent toujours), le code parfait prenait 14 fois plus de temps à supprimer.

Des feature flags pour des fonctionnalités que personne n’a demandées

Chaque projet que j’ai vu utiliser des feature flags pour de petits déploiements a des flags morts. Des flags activés il y a des mois et jamais nettoyés. Des flags pour des fonctionnalités entièrement déployées mais qui font encore brancher le code.

Les feature flags sont géniaux pour des produits à 10 000 utilisateurs qui font des déploiements progressifs. Pour un petit projet livrant à une audience connue, ils sont de la complexité sans contrepartie.

Le sophisme du problème improbable

L’argument le plus courant pour la sur-ingénierie : “Mais et si X arrive ?”

Et si l’API tombe ? Et si on passe à 10 000 utilisateurs ? Et si on a besoin de permissions par utilisateur ? Et si on change de base ? Et si on a un deuxième locataire ?

Voilà le truc : vous ne pouvez pas prédire quels problèmes vont vraiment arriver. Et le coût de se tromper sur un problème futur est presque toujours plus faible que le coût de maintenir du code pour un problème qui ne vient jamais.

Le formulaire de contact qui échoue une fois par an à cause d’une panne d’API, c’est une correction de 5 minutes. Le formulaire de contact avec un disjoncteur que personne ne comprend est un fardeau de maintenance permanent. Le système RBAC qui a pris 2 semaines à construire et 3 jours à retirer résolvait un problème que personne n’avait. L’ORM maison maintenu pendant 3 ans gérait un changement de base qui n’a jamais eu lieu.

Ce que je fais vraiment maintenant

Je suis une seule règle : écrire le code le plus simple qui marche pour les besoins actuels, plus une couche de sécurité.

Pas zéro sécurité. Pas chaque cas limite. Une couche.

Le test est simple : si vous ne pouvez pas expliquer pourquoi vous avez besoin de quelque chose avec un exemple réel des 3 derniers mois, ne l’ajoutez pas.

Pour le formulaire de contact, ça voulait dire : valider l’entrée, gérer l’erreur de l’API, et passer à autre chose. La logique de retry peut être ajoutée si des échecs surviennent. Les feature flags sont inutiles pour 12 utilisateurs. Les types d’erreur personnalisés sont excessifs pour un seul appel d’API.

En résumé

Le code parfait donne une impression d’artisanat. Mais dans une petite base de code, c’est une taxe sur chaque changement futur. Vous n’écrivez pas du code pour l’ordinateur. Vous écrivez du code que des humains doivent lire, relire, supprimer et remplacer.

Écrivez la chose la plus simple qui marche. Ajoutez de la complexité seulement quand vous avez la preuve d’en avoir besoin. Le meilleur code est celui que vous pouvez supprimer en 5 minutes parce qu’il ne résout que le problème que vous avez vraiment.