Le code est un système, pas une prose élégante
Le code est un système, pas une prose élégante
On parle encore trop souvent du code comme d’un style. Pourtant, le code tourne dans notre monde.
Le code n’est pas une prose élégante. C’est un système, qu’il faut juger à sa robustesse.
On parle trop souvent du code comme on parlerait d’un style.
On dit d’un morceau de code qu’il est élégant, beau, fin, propre, parfois même inspiré. On lui attribue des qualités presque littéraires, presque artistiques, comme si écrire du logiciel relevait d’abord d’un art de la forme. Comme si la qualité du code se jouait avant tout dans l’allure de ses phrases.
C’est une erreur de catégorie.
Le problème n’est pas le mot élégant en lui-même. Le problème, c’est tout ce qu’il charrie lorsqu’il est employé dans un sens romantique, littéraire, esthétique : l’idée qu’un bon code serait d’abord un code que l’on admire pour sa tenue formelle, pour sa grâce, pour une sorte de beauté interne presque autonome.
Or le code n’est pas un texte que l’on contemple. Le code tourne dans le monde.
Il s’exécute dans des machines, des réseaux, des navigateurs, des téléphones, des bases de données, des files de messages, des systèmes distribués, des organisations humaines. Il rencontre des latences, des coupures, des réponses incomplètes, des effets de bord, des utilisateurs incohérents, des environnements imparfaits, des états intermédiaires, des dépendances instables.
Le monde réel n’applaudit pas la beauté d’une abstraction. Il sanctionne l’absence de garde-fous.
C’est pourquoi le bon vocabulaire n’est pas d’abord celui de l’élégance. C’est celui de la robustesse.
Il faut d’ailleurs préciser une chose : je ne parle pas ici contre l’élégance au sens où certains ingénieurs l’entendent — la clarté, la bonne décomposition, la simplicité juste. Cette tradition-là est précieuse. Ce que je vise, c’est une autre manière de parler du code : une manière plus romantique, plus esthétique, qui le juge d’abord pour son allure, au lieu de le juger pour sa tenue dans le réel.
Ce qui est souvent célébré comme “élégant” n’est pas toujours une qualité de structure. C’est parfois une qualité de confort local : moins de code, plus de fluidité, plus de colocation, plus de convention, plus d’implicite. Or ce qui est pratique à écrire ou agréable à lire localement n’est pas toujours ce qui rend le système plus explicite, plus stable ou plus transmissible.
La robustesse comme vrai critère
J’emploie ici le mot robustesse dans un sens large.
Un code robuste n’est pas simplement un code qui “ne casse pas”. Un code robuste est un code :
- intelligible ;
- résilient ;
- observable.
Cette définition change la façon même de juger le logiciel.
La question n’est plus : est-ce que ce code est beau ?
La question devient :
- Est-ce qu’un humain peut le comprendre sans faire un acte de foi ?
- Est-ce qu’une machine peut raisonner dessus sans deviner ?
- Est-ce qu’il encaisse proprement les défaillances et les écarts du réel ?
- Est-ce qu’on peut savoir ce qu’il a fait, dans quel état, et pourquoi ?
- Est-ce qu’on peut le reprendre, l’étendre, l’auditer, sans fragiliser l’ensemble ?
Là se trouve le véritable déplacement.
On ne devrait pas juger le code d’abord comme un objet esthétique. On devrait le juger comme un système soumis au réel.
Cela ne veut pas dire que tout code exige partout le même niveau de formalisation, mais que la bonne boussole n’est pas l’allure du code : c’est le niveau de robustesse dont la situation a réellement besoin.
Ces outils ont eux aussi un coût. Un prototype qui cherche encore son problème n’a pas besoin de la même formalisation qu’un système critique. La discipline n’est pas de tout structurer partout, mais de savoir ce qui mérite de l’être, à quel moment, et pour quel risque.
Intelligible, parce qu’il faut pouvoir le lire sans deviner
La robustesse commence par l’intelligibilité. Et, dans cet article, ce que j’appelle intelligible tient d’abord à une propriété : l’explicite. Des frontières visibles, des responsabilités nettes, des états nommés, des contrats formulés, des transitions déclarées, mais aussi des noms clairs, des flux compréhensibles, des transformations localisées et des conventions stables. Ce n’est pas parce qu’un système est explicite qu’il devient automatiquement simple. Mais sans explicitation, il oblige à deviner — et un système qui oblige à deviner devient vite coûteux.
Mais aujourd’hui, cela ne suffit plus.
Le code doit aussi être intelligible pour les machines.
Nous entrons dans une époque où le code n’est plus seulement lu par des développeurs. Il est aussi lu, audité, transformé, complété, expliqué, parfois généré par des intelligences. Un code ambigu, implicite, plein de conventions non dites, ne devient pas seulement pénible pour un collègue humain : il devient également fragile pour un agent. Il force à deviner.
Or dès qu’un système repose sur de la devinette, il devient coûteux à maintenir et dangereux à modifier.
L’intelligibilité n’est donc pas un luxe de style. C’est une propriété opérationnelle. Et, ici, elle dépend largement de l’explicitation du système.
Résilient, parce que le monde dévie
Le bon code n’est pas celui qui suppose que tout ira bien. C’est celui qui a été conçu en sachant que tout ne se passera pas comme prévu.
J’emploie ici “résilient” dans un sens large. Un système résilient ne se contente pas de survivre à une panne. Il encaisse les écarts du réel : latences, réponses incomplètes, dépendances instables, rejouements, indisponibilités temporaires, effets de bord, interruptions partielles, variations de contexte.
Dès qu’un système touche au monde extérieur, il rencontre cette incertitude : une réponse réseau peut arriver en retard, une API peut changer de forme, un utilisateur peut envoyer des données absurdes, une base peut être temporairement indisponible, un événement peut être traité deux fois, un état peut être partiellement mis à jour.
Un système robuste ne nie pas cette réalité. Il la cadre.
Il distingue ce qui est sûr de ce qui ne l’est pas.
Il limite les zones où les effets de bord se produisent.
Il rend explicites les cas d’erreur et les cas dégradés.
Il accepte que l’échec fasse partie du comportement normal du système.
La robustesse n’est donc pas un supplément que l’on ajoute après coup avec quelques try/catch. C’est une manière de concevoir le logiciel dès le départ.
Observable, parce qu’en production on ne voit jamais tout à l’avance
Même un système bien conçu rencontrera en production des cas que personne n’avait complètement anticipés.
C’est inévitable.
La question n’est donc pas de savoir si des erreurs surviendront. La question est de savoir si, lorsqu’elles surviendront, le système nous donnera les moyens de comprendre ce qui s’est passé.
Un code observable produit des traces utiles. Il rend visibles les transitions importantes, les échecs significatifs, les contextes utiles, les interactions externes, les délais anormaux, les états métier pertinents.
Il ne se contente pas d’échouer. Il échoue en laissant des indices exploitables.
L’observabilité n’est pas un sujet secondaire réservé à l’exploitation ou à l’infrastructure. C’est un sujet de conception logicielle. Un système mal structuré est difficile à observer parce qu’il ne sait déjà pas très bien décrire ce qu’il fait. Un système bien structuré permet au contraire d’attacher naturellement des traces aux frontières, aux contrats, aux validations, aux transitions d’état, aux erreurs métier.
Observer un système, ce n’est pas ajouter des lampes sur une machine obscure. C’est construire une machine dont les mécanismes peuvent être inspectés.
Ce qui rend un code robuste
Si l’on accepte que le vrai critère soit la robustesse, alors une autre famille de concepts devient centrale.
Non plus les concepts de style, mais les concepts de système.
Et ces concepts ne sont pas simplement côte à côte. Ils s’enchaînent.
Cette séquence est un ordre d’exposition, pas un ordre strict de fabrication. Dans la pratique, ces dimensions se répondent, se corrigent et se co-construisent. Mais pour les penser clairement, il est utile de les dérouler ainsi.
1. Les frontières
Tout commence par les frontières.
Un système robuste doit savoir où il commence, où il finit, et où se situent ses zones d’incertitude.
Les frontières séparent par exemple :
- l’interne et l’externe ;
- le domaine et l’infrastructure ;
- la logique pure et les effets de bord ;
- les données fiables et les données encore suspectes.
Sans frontières, tout communique avec tout. Les responsabilités se mélangent, les causes se diffusent, les erreurs deviennent difficiles à localiser.
Les frontières ne servent donc pas seulement à “bien organiser le code”. Elles servent à contenir l’incertitude.
Exemple contemporain : dans les applications web, certains frameworks rendent les interactions client/serveur très fluides. Les Server Actions de Next.js, par exemple, n’obligent pas à brouiller les frontières : on peut parfaitement les définir séparément et marquer clairement la coupure. Mais le framework rend possible, via une simple directive, une forme de colocation très pratique entre le code frontend et l’action serveur. Ce confort local peut donner une impression d’élégance. Pourtant, la frontière n’a pas disparu : elle s’est seulement faite plus discrète. Derrière cette simplicité apparente, il existe toute une mécanique réelle : fonctions serveur appelables via le réseau, contrôles d’authentification et d’autorisation à refaire dans l’action elle-même, variables capturées envoyées puis renvoyées, chiffrées, et liées à un build donné. Le framework en prend une partie en charge, mais cette abstraction ne supprime pas la complexité : elle la déplace et la masque partiellement. Le problème n’est pas l’outil. Le problème, c’est la facilité avec laquelle un confort local peut faire oublier la frontière, ses exigences, et la charge opérationnelle qui continue d’exister derrière elle.
2. Les contrats
Dès qu’une frontière existe, il faut définir ce qui a le droit de la traverser.
C’est le rôle des contrats.
Un contrat dit ce qui est attendu, ce qui est garanti, et dans quelles conditions. Il peut prendre plusieurs formes : type, interface, schéma, protocole, convention explicite. Peu importe la forme exacte. L’important, c’est qu’il réduise l’ambiguïté.
La logique est simple :
frontière → contrat
On ne laisse pas n’importe quoi circuler entre deux zones du système.
Un système sans contrats repose sur des suppositions. Et les suppositions cassent silencieusement.
3. La validation
Un contrat qui n’est jamais vérifié reste un souhait.
C’est pourquoi les contrats appellent la validation.
Dès qu’une donnée traverse une frontière, elle doit être validée. Cela vaut pour une saisie utilisateur, une variable d’environnement, une réponse d’API, un fichier importé, un message consommé depuis une file, un payload réseau.
Valider, c’est refuser de faire comme si toute entrée était légitime.
Le système robuste ne fait pas confiance trop tôt. Il valide aux frontières. Il transforme l’inconnu en connu.
C’est particulièrement vrai lorsque l’on consomme un système externe. Le simple fait qu’il annonce un contrat ne dispense jamais de vérifier ce qui entre réellement. Une API peut dériver, être mal implémentée, ou violer ponctuellement sa propre promesse. Sans validation à l’entrée, on laisse alors entrer dans le système des données traitées comme vraies alors qu’elles ne le sont pas.
La chaîne devient alors :
frontières → contrats → validation
Et cette chaîne est essentielle, parce qu’elle empêche l’incertitude du monde extérieur de contaminer silencieusement le cœur du système.
4. Les invariants
Une fois les entrées cadrées et validées, le système peut commencer à protéger ses vérités internes.
C’est le rôle des invariants.
Un invariant est une chose qui doit rester vraie.
Par exemple : une commande payée ne redevient pas en attente ; un utilisateur authentifié possède toujours un identifiant valide ; un objet dans tel état ne peut pas déclencher telle transition ; une donnée marquée comme validée respecte effectivement le schéma attendu.
Les invariants donnent au système des points d’ancrage. Ils réduisent l’espace du possible. Et réduire l’espace du possible, c’est rendre le système plus intelligible et plus sûr.
Un invariant n’est pas seulement une vérité abstraite ; c’est aussi quelque chose qu’il faut protéger face au comportement réel du système. Si un webhook de paiement est rejoué, par exemple, le système ne devrait pas faire comme si un événement inédit venait d’arriver. Sans garde-fou d’idempotence, un même signal peut produire plusieurs effets et faire dériver l’état au lieu de le stabiliser.
On peut le formuler ainsi :
la validation protège les invariants
5. Les états explicites
Dès qu’un système dépend du temps, d’étapes, d’autorisations, de séquences, d’événements, de transitions ou de temporalité, il faut rendre son état explicite.
Dès qu’une ressource existe dans le temps, elle a aussi un cycle de vie. Et penser ce cycle de vie explicitement, c’est justement refuser qu’il soit dispersé dans une multitude de conditions locales, implicites ou contradictoires.
Et rendre l’état explicite, en pratique, cela veut dire le modéliser.
On mobilise alors des outils conceptuels comme :
- les machines à états ;
- les statecharts, lorsqu’on a besoin d’ajouter de la hiérarchie, du parallélisme ou de la communication.
Pourquoi sont-ils utiles ? Parce qu’ils forcent à nommer les états possibles, à définir les transitions autorisées, à expliciter les événements déclencheurs, à limiter les passages illégaux.
Ils remplacent une logique diffuse par une structure déclarée.
La progression devient alors :
invariants → états explicites → transitions autorisées
Et c’est souvent là qu’un système commence réellement à tenir. Non pas parce qu’il devient plus “intelligent”, mais parce qu’il devient moins flou.
Prenons un système d’enregistrement audio ou vidéo. Avant même de démarrer, l’utilisateur peut choisir une caméra, un micro, un fond, vérifier un aperçu. Puis il lance l’enregistrement, peut mettre en pause, reprendre, couper la caméra, changer certains réglages, pendant qu’en arrière-plan le système crée une trace côté serveur, écoute des périphériques et envoie des segments. À la fin, il peut supprimer ou sauvegarder. Sans états explicites, tout cela se disperse vite en conditions locales et en drapeaux contradictoires. Avec une machine à états, on nomme les phases, on borne les transitions, et on rend enfin cohérent ce que l’utilisateur voit, ce que le système fait, et ce que le serveur reçoit.
6. La responsabilité et l’autorité
Une fois les états rendus explicites, une autre question devient inévitable : qui a le droit d’agir sur quoi, à quel moment, et avec quelles responsabilités ?
Ici, la question devient celle de la responsabilité et de l’autorité.
Un système devient flou lorsque plusieurs parties peuvent créer, modifier, faire transiter ou détruire une même ressource sans règle claire. À l’inverse, un système robuste sait qui possède quoi au sens opérationnel : qui est responsable d’une donnée, d’un processus, d’une ressource, d’une transition, d’un nettoyage, d’une clôture.
Le cycle de vie dit dans quels états une chose peut exister au fil du temps. La responsabilité et l’autorité disent qui peut agir sur cette chose, à quel moment, et sous quelles conditions.
Ce point est essentiel, car un état explicite sans autorité explicite laisse encore place à la dérive. On sait alors ce qui peut arriver, mais pas qui est légitime pour le provoquer.
La progression s’enrichit donc ainsi :
états explicites → responsabilité et autorité → transitions maîtrisées
7. Les transformations déterministes
Une fois les frontières posées, les contrats définis, les validations en place, les invariants protégés et les états rendus explicites, une autre question devient centrale : comment rendre le comportement local du système aussi lisible et prévisible que possible ?
C’est à ce moment que la question des transformations déterministes se pose.
Chaque fois que c’est possible, on veut des transformations dont le comportement est stable, compréhensible et testable. On veut, autant que possible, des fonctions pures, de l’immutabilité, de la composition, et des effets de bord isolés.
Non par goût doctrinal, ni pour afficher une appartenance à une école, mais parce que cela rend le cœur du système plus robuste.
Les idées issues de la programmation fonctionnelle sont ici précieuses. Elles ne remplacent ni les frontières, ni les contrats, ni les invariants, ni les états explicites. Elles rendent simplement le comportement interne plus prévisible.
Une fonction pure ne constitue pas, à elle seule, un invariant du système. En revanche, elle fournit une unité locale de prévisibilité. Même entrée, même sortie, pas d’effet de bord caché. Et ces unités locales de prévisibilité sont extrêmement précieuses lorsqu’on veut construire un ensemble lisible, testable et fiable.
En pratique : aux frontières, on valide ; dans le cœur du système, on cherche la pureté autant que possible ; dès que le temps, les permissions ou les transitions comptent, on rend l’état explicite.
Cette recherche de déterminisme local ne remplace pas la pensée systémique. Elle la complète.
Et elle prépare naturellement l’étape suivante : la preuve.
8. La preuve
Une fois les frontières posées, les contrats définis, les validations en place, les invariants protégés et les états explicités, reste une question : qu’est-ce qui dépend encore uniquement de la discipline humaine ?
La question de la preuve apparaît alors.
Dans le logiciel, la preuve n’est pas toujours formelle au sens mathématique. Mais il existe plusieurs degrés de preuve :
- les types ;
- les schémas ;
- les tests ;
- l’exhaustivité ;
- les contraintes de compilation ;
- l’impossibilité structurelle de certains états ou de certaines transitions.
L’idée centrale est simple : tout ce qui peut être garanti structurellement ne devrait pas reposer seulement sur l’habitude, la prudence ou la mémoire d’une équipe.
On peut le résumer ainsi :
contrats + invariants + états explicites → formes de preuve
Plus un système remplace la discipline implicite par des garanties explicites, plus il devient robuste.
Il faut d’ailleurs ajouter une nuance importante : la preuve, ici, ne se limite pas aux formes les plus courantes et les plus accessibles de garantie structurelle. Dans certains contextes, elle peut aller jusqu’à la vérification formelle, à l’aide de langages, d’assistants de preuve et de méthodes capables de démontrer qu’un programme respecte certaines spécifications. Cette voie reste exigeante et peu répandue dans le développement quotidien, mais son existence rappelle que la “preuve” n’est pas seulement une métaphore commode. Dans certains cas, elle peut devenir un objectif technique réel.
On le voit aussi dans des formes plus ordinaires de preuve. Entre un état stocké comme simple chaîne de caractères et un type exhaustif qui oblige le compilateur à traiter tous les cas, la différence est nette : dans un cas, on espère ; dans l’autre, on rend certains oublis structurellement impossibles.
9. L’observabilité, comme prolongement du système dans le réel
Et enfin, tout ce qui a été structuré conceptuellement doit pouvoir être vu lorsqu’il fonctionne réellement.
L’observabilité revient alors, non plus comme un simple critère général, mais comme le prolongement concret de toute l’architecture du système.
Un système bien structuré permet d’observer :
- ce qui entre par ses frontières ;
- ce qui est accepté ou rejeté par validation ;
- quels contrats ont été violés ;
- quels invariants ont été protégés ou menacés ;
- dans quel état il se trouve ;
- quelle transition a eu lieu ;
- quelle interaction externe a échoué ;
- quels délais ou comportements anormaux ont été rencontrés.
On n’ajoute pas l’observabilité à la fin comme un décor technique. On l’attache à la structure même du système.
La logique complète devient alors :
frontières → contrats → validation → invariants → états explicites → responsabilité et autorité → transformations déterministes → preuve → observabilité
Là, on ne parle plus seulement de code organisé. On parle d’un système que l’on peut comprendre, contraindre, faire évoluer et inspecter.
La différence se voit très vite en production. Un log qui dit seulement « erreur 500 » signale un problème, mais aide rarement à le comprendre. Un log ou une trace qui porte un identifiant de corrélation, l’état métier courant, la transition tentée et l’entrée rejetée raconte déjà une partie du système. L’observabilité utile ne consiste pas à produire plus de bruit, mais à rendre le comportement du système intelligible lorsqu’il dévie.
À l’ère des intelligences, le flou coûte plus cher
À l’ère des intelligences, la pensée systémique devient encore plus importante. Le sujet n’est pas seulement que des agents produisent du code. Le sujet est que ce code doit pouvoir circuler entre plusieurs formes d’intelligence : humaine et artificielle, aujourd’hui ; souvent entremêlées, demain. On ne veut pas seulement qu’une machine écrive du code. On veut qu’elle écrive un système qu’un humain puisse relire, comprendre, critiquer et valider. Et réciproquement, on veut que les humains produisent des structures que les machines puissent suivre sans deviner.
Quand plusieurs humains et plusieurs agents interviennent sur un même code, l’absence de frontières, de contrats, d’invariants, d’états explicites et d’autorités claires ouvre la voie à toutes les dérives : dérive du sens, dérive de l’architecture, dérive du comportement. Le code continue parfois de fonctionner, mais il cesse progressivement de faire système. À l’inverse, un système bien structuré ne se contente pas de faciliter la maintenance : il contient les déformations, rend les écarts visibles, et permet à la transformation de rester contrôlée.
Pendant longtemps, une petite équipe pouvait compenser un code flou par de la familiarité. Quelqu’un savait “comment ça marche vraiment”. Une compréhension implicite, collective, plus ou moins artisanale, permettait de colmater les trous.
Cette époque s’érode.
Aujourd’hui, le code circule entre plus d’acteurs : développeurs, reviewers, nouveaux arrivants, outils d’analyse, pipelines, assistants, agents. Chaque zone d’ambiguïté devient un coût multiplié. Chaque convention implicite devient une dette plus lourde. Chaque frontière poreuse, chaque contrat absent, chaque état mal modélisé réduit la capacité d’un humain ou d’un agent à intervenir de façon sûre.
Il y a d’ailleurs un autre déplacement important. Pendant longtemps, certaines modélisations paraissaient trop coûteuses à écrire, à maintenir ou simplement à justifier. On se contentait alors de formes plus implicites, plus légères, parfois plus floues. Avec les gains de productivité apportés par les intelligences, cet arbitrage change. Le coût d’implémentation baisse dans de nombreux cas. Ce qui devient plus coûteux, en revanche, c’est la compréhension, la validation, la testabilité et la maîtrise de la dérive. Dès lors, certaines structures plus explicites — qui pouvaient sembler trop lourdes hier — deviennent au contraire de bons investissements, parce qu’elles rendent le système plus lisible, plus contrôlable et plus transmissible.
Un tel code est plus simple à reprendre, à auditer, à instrumenter et à transformer, non parce qu’il serait “plus joli”, mais parce qu’il expose sa logique et réduit la part de devinette.
Et c’est sans doute l’un des grands déplacements de notre époque : un code flou n’est plus seulement difficile à maintenir. Il est aussi beaucoup plus difficile à faire évoluer proprement avec des intelligences.
Conclusion
Le code n’est pas une prose élégante. C’est un système, qu’il faut juger à sa robustesse.
Et la robustesse, ici, n’est pas un mot vague. Elle désigne quelque chose de précis : un code intelligible, résilient et observable.
Pour y parvenir, il faut moins penser en termes de style qu’en termes de structure : rendre le système plus explicite, poser des frontières, définir des contrats, valider aux points de contact, protéger des invariants, rendre les états explicites, clarifier les responsabilités et les autorités, construire des formes de preuve, attacher l’observabilité à l’architecture elle-même.
S’il faut encore parler d’élégance, alors il faut déplacer le regard. Ce qui mérite d’être admiré, ce n’est pas le code qui impressionne. C’est le code qui fait système.
L’élégance est un vocabulaire de surface. La robustesse est un vocabulaire de système.
Pour aller plus loin
- David Parnas — sur la modularité et les frontières
- Bertrand Meyer — Design by Contract
- David Harel — sur les statecharts
- Eric Evans — Domain-Driven Design
- John Hughes et Philip Wadler — sur la tradition fonctionnelle
- Charity Majors, Liz Fong-Jones, George Miranda — sur l’observabilité moderne
Une remarque après lecture ?
Si vous souhaitez envoyer un mot au sujet de cet article, vous pouvez écrire ici. Je partage ici parce que le sujet m’intéresse et que je veux apprendre des autres. Merci pour vos retours, surtout lorsqu’ils sont formulés avec soin.