Une présentation accessible des idées de Rich Hickey sur Clojure : simplicité, immutabilité et meilleurs choix par défaut — leçons pratiques pour construire des systèmes complexes plus calmes et plus sûrs.

Le logiciel ne devient rarement compliqué d’un seul coup. Il y arrive par une succession de décisions « raisonnables » : un cache rapide pour tenir un délai, un objet mutable partagé pour éviter des copies, une exception aux règles parce que « celui-ci est spécial ». Chaque choix paraît petit, mais ensemble ils créent un système où changer quelque chose semble risqué, où les bugs sont difficiles à reproduire, et où ajouter des fonctionnalités prend plus de temps que de les construire.
La complexité gagne parce qu’elle offre du confort à court terme. Il est souvent plus rapide d’ajouter une dépendance que de simplifier une dépendance existante. Il est plus simple de coller de l’état que de se demander pourquoi l’état est réparti sur cinq services. Et il est tentant de compter sur des conventions et du savoir tribal quand le système croît plus vite que la documentation.
Ce n’est pas un tutoriel sur Clojure, et vous n’avez pas besoin de connaître Clojure pour en retirer de la valeur. L’objectif est d’emprunter un ensemble d’idées pratiques souvent associées au travail de Rich Hickey—des idées que vous pouvez appliquer aux décisions d’ingénierie quotidiennes, quel que soit le langage.
La plupart de la complexité n’est pas créée par le code que vous écrivez volontairement ; elle est créée par ce que vos outils rendent facile par défaut. Si le défaut est « objets mutables partout », vous vous retrouvez avec un couplage caché. Si le défaut est « l’état vit en mémoire », vous peinerez avec le debug et la traçabilité. Les defaults façonnent des habitudes, et les habitudes façonnent les systèmes.
Nous nous concentrerons sur trois thèmes :
Ces idées ne suppriment pas la complexité de votre domaine, mais elles peuvent empêcher votre logiciel de la multiplier.
Rich Hickey est un développeur et concepteur de logiciels de longue date, surtout connu pour avoir créé Clojure et pour ses talks qui remettent en cause des habitudes courantes en programmation. Son focus n’est pas les tendances : c’est les raisons récurrentes pour lesquelles les systèmes deviennent difficiles à changer, à comprendre, et à faire confiance quand ils grossissent.
Clojure est un langage moderne qui tourne sur des plateformes bien connues comme la JVM (l’exécution Java) et JavaScript. Il est conçu pour travailler avec des écosystèmes existants tout en encourageant un style particulier : représenter l’information comme des données simples, préférer des valeurs qui ne changent pas, et séparer « ce qui s’est passé » de « ce que l’on affiche à l’écran ».
Vous pouvez le percevoir comme un langage qui vous pousse vers des blocs de construction plus clairs et éloigne des effets de bord cachés.
Clojure n’a pas été créé pour rendre les petits scripts plus courts. Il visait des douleurs récurrentes de projet :
Les choix par défaut de Clojure poussent vers moins d’éléments mobiles : structures de données stables, mises à jour explicites, et outils qui rendent la coordination plus sûre.
La valeur ne se limite pas au changement de langage. Les idées centrales de Hickey—simplifier en supprimant les interdépendances inutiles, traiter les données comme des faits durables, et minimiser l’état mutable—peuvent améliorer les systèmes en Java, Python, JavaScript, et au-delà.
Rich Hickey trace une ligne nette entre simple et facile—une ligne que la plupart des projets franchissent sans s’en apercevoir.
Facile concerne la sensation immédiate. Simple concerne le nombre de parties et leur enchevêtrement.
En logiciel, « facile » signifie souvent « rapide à taper aujourd’hui », tandis que « simple » signifie « plus difficile à casser le mois prochain ».
Les équipes choisissent souvent des raccourcis qui réduisent la friction immédiate mais ajoutent des structures invisibles qui doivent être maintenues :
Chaque choix peut sembler être de la vitesse, mais il augmente le nombre d’éléments mobiles, de cas particuliers et d’interdépendances. C’est ainsi que les systèmes deviennent fragiles sans erreur dramatique unique.
Livrer vite peut être excellent—mais la vitesse sans simplifier revient souvent à s’endetter sur le futur. Les intérêts apparaissent sous forme de bugs difficiles à reproduire, d’intégration longue et de changements nécessitant une « coordination soigneuse ».
Posez ces questions lors d’une revue de design ou d’un PR :
« L’état » est simplement ce qui peut changer dans votre système : le panier d’un utilisateur, le solde d’un compte, la configuration courante, l’étape d’un workflow. Le problème n’est pas que le changement existe—c’est que chaque changement crée une nouvelle opportunité de désaccord.
Quand on dit « l’état cause des bugs », on veut dire généralement ceci : si la même information peut être différente à des moments différents (ou à des endroits différents), alors votre code doit constamment répondre à « Quelle version est la vraie maintenant ? ». Se tromper produit des erreurs qui semblent aléatoires.
La mutabilité signifie qu’un objet est modifié en place : la « même » chose devient différente au fil du temps. Ça semble efficace, mais ça rend le raisonnement plus difficile parce qu’on ne peut pas se fier à ce qu’on a vu un instant plus tôt.
Un exemple parlant : une feuille de calcul partagée. Si plusieurs personnes peuvent éditer les mêmes cellules en même temps, votre compréhension peut être invalidée instantanément : les totaux changent, les formules cassent, une ligne disparaît parce que quelqu’un a réorganisé. Même sans malveillance, le caractère partagé et modifiable crée la confusion.
Le logiciel se comporte de la même façon. Si deux parties lisent la même valeur mutable, l’une peut la changer silencieusement pendant que l’autre continue avec une hypothèse obsolète.
L’état mutable transforme le debug en archéologie. Un rapport d’anomalie vous montre rarement « la donnée a été modifiée incorrectement à 10:14:03 ». Vous voyez juste le résultat final : un nombre erroné, un statut inattendu, une requête qui échoue seulement parfois.
Parce que l’état évolue dans le temps, la question la plus importante devient : quelle séquence de modifications a abouti ici ? Si vous ne pouvez pas reconstruire cette histoire, le comportement devient imprévisible :
C’est pourquoi Hickey considère l’état comme un multiplicateur de complexité : une fois que les données sont à la fois partagées et mutables, le nombre d’interactions possibles croît plus vite que votre capacité à les maîtriser.
L’immuabilité signifie simplement des données qui ne changent pas après leur création. Plutôt que de modifier un élément existant en place, vous créez un nouvel élément qui reflète la mise à jour.
Pensez à un reçu : une fois imprimé, on n’efface pas des lignes et on ne réécrit pas les totaux. Si quelque chose change, on émet un reçu corrigé. L’ancien existe toujours, et le nouveau est clairement « la dernière version ».
Quand les données ne peuvent pas être modifiées en douce, on arrête de se préoccuper des modifications invisibles. Cela facilite grandement le raisonnement au quotidien :
C’est une part importante de la vision de Hickey : moins d’effets de bord cachés signifie moins de branches mentales à suivre.
Créer de nouvelles versions peut sembler coûteux jusqu’à ce que vous compariez à l’alternative. Éditer en place vous laisse demander : « Qui a changé ça ? Quand ? Qu’est-ce que c’était avant ? » Avec des données immuables, les changements sont explicites : une nouvelle version existe et l’ancienne reste disponible pour le debug, l’audit ou le rollback.
Clojure encourage naturellement à considérer les mises à jour comme la production de nouvelles valeurs, pas comme la mutation d’anciennes.
L’immuabilité n’est pas gratuite. Vous pouvez allouer plus d’objets et les équipes habituées au « on modifie juste » doivent s’adapter. La bonne nouvelle : les implémentations modernes partagent souvent des structures sous le capot pour réduire le coût mémoire, et le gain est généralement des systèmes plus calmes avec moins d’incidents difficiles à expliquer.
La concurrence, c’est simplement « beaucoup de choses qui se passent en même temps ». Une app web gérant des milliers de requêtes, un système de paiement mettant à jour des soldes pendant qu’il génère des reçus, ou une appli mobile qui sync en arrière-plan—tous sont concurrents.
Le problème n’est pas que plusieurs choses se passent ; c’est qu’elles touchent souvent les mêmes données.
Quand deux travailleurs peuvent lire puis modifier la même valeur, le résultat final peut dépendre du timing. C’est une condition de course : un bug difficile à reproduire qui apparaît lorsque le système est chargé.
Exemple : deux requêtes essaient de mettre à jour le total d’une commande.
Rien n’a « crashé », mais vous avez perdu une mise à jour. Sous charge, ces fenêtres temporelles se multiplient.
Les solutions traditionnelles—verrous, blocs synchronisés, ordonnancements stricts—fonctionnent, mais elles obligent tout le monde à se coordonner. La coordination coûte : elle ralentit le débit et devient fragile à mesure que le codebase grossit.
Avec des données immuables, une valeur n’est pas éditée en place. À la place, vous créez une nouvelle valeur qui représente la modification.
Ce simple changement supprime toute une catégorie de problèmes :
L’immuabilité ne rend pas la concurrence gratuite—vous avez toujours besoin de règles pour déterminer quelle version est courante. Mais elle rend les programmes concurrents beaucoup plus prédictibles, car les données elles-mêmes ne sont plus une cible mouvante. Quand le trafic monte ou que les jobs d’arrière-plan s’accumulent, vous verrez moins de défaillances mystérieuses dépendant du timing.
« Meilleurs choix par défaut » veut dire que l’option la plus sûre se produit automatiquement, et que vous ne prenez de risque supplémentaire que lorsque vous le décidez explicitement.
Cela paraît anodin, mais les defaults enseignent silencieusement comment coder un lundi matin, ce que les reviewers acceptent un vendredi soir, et ce qu’un nouveau venu apprend du premier codebase qu’il touche.
Un « meilleur défaut » ne consiste pas à tout décider pour vous. Il vise à rendre le chemin commun moins sujet aux erreurs.
Par exemple :
Rien de tout cela n’élimine la complexité, mais cela l’empêche de se propager.
Les équipes ne suivent pas seulement la documentation—elles suivent ce que le code « veut » que vous fassiez.
Quand muter l’état partagé est facile, cela devient un raccourci normal, et les reviewers se retrouvent à débattre d’intentions : « Est-ce sûr ici ? » Quand l’immuabilité et les fonctions pures sont la norme, les reviewers peuvent se concentrer sur la logique et la correction, parce que les mouvements risqués ressortent.
Autrement dit, de meilleurs defaults créent un socle plus sain : la plupart des changements paraissent cohérents, et les patterns inhabituels sont suffisamment visibles pour être questionnés.
La maintenance long terme revient surtout à lire et modifier du code existant en toute sécurité.
Les meilleurs defaults aident les nouveaux collègues à monter en compétence parce qu’il y a moins de règles cachées (« attention, cette fonction modifie secrètement cette map globale »). Le système devient plus facile à raisonner, ce qui réduit le coût de chaque future feature, fix et refactor.
Un basculement mental utile dans les talks de Hickey est de séparer faits (ce qui s’est produit) et vues (ce que nous croyons actuellement vrai). Beaucoup de systèmes brouillent ces notions en ne stockant que la valeur la plus récente—en écrasant hier par aujourd’hui—et cela fait disparaître la dimension temporelle.
Un fait est un enregistrement immuable : « Commande #4821 passée à 10:14 », « Paiement réussi », « Adresse modifiée ». On n’édite pas ces faits ; on ajoute de nouveaux faits à mesure que la réalité change.
Une vue est ce dont votre app a besoin maintenant : « Quelle est l’adresse de livraison actuelle ? » ou « Quel est le solde client ? » Les vues peuvent être recalculées à partir des faits, mises en cache, indexées ou matérialisées pour la vitesse.
Quand vous conservez les faits, vous gagnez :
Écraser des enregistrements, c’est comme mettre à jour une cellule de feuille de calcul : vous ne voyez que le dernier nombre.
Un journal append-only, c’est comme un registre de chéquier : chaque écriture est un fait, et le « solde courant » est une vue calculée à partir des écritures.
Vous n’avez pas à adopter une architecture event-sourcée complète pour en bénéficier. Beaucoup d’équipes commencent plus modestement : conserver une table d’audit append-only pour les changements critiques, stocker des événements immuables pour quelques workflows à haut risque, ou garder des snapshots plus une courte fenêtre d’historique. L’essentiel est l’habitude : traiter les faits comme durables et l’état courant comme une projection pratique.
Une des idées les plus pratiques de Hickey est data first : traiter l’information du système comme des valeurs simples (faits), et traiter le comportement comme quelque chose qu’on exécute contre ces valeurs.
Les données sont durables. Si vous stockez une information claire et autonome, vous pouvez la réinterpréter plus tard, la déplacer entre services, la réindexer, l’auditer, ou l’exploiter pour de nouvelles fonctionnalités. Le comportement est moins durable—le code change, les hypothèses changent, les dépendances changent.
Quand vous mêlez tout, les systèmes deviennent collants : vous ne pouvez pas réutiliser des données sans traîner le comportement qui les a créées.
Séparer faits et actions réduit le couplage parce que les composants peuvent se mettre d’accord sur une forme de données sans s’accorder sur un chemin d’exécution commun.
Un job de reporting, un outil support et un service de facturation peuvent consommer les mêmes données de commande, chacun appliquant sa logique. Si vous embeddiez la logique dans la représentation stockée, chaque consommateur deviendrait dépendant de cette logique—et la changer deviendrait risqué.
Données propres (faciles à faire évoluer) :
{
"type": "discount",
"code": "WELCOME10",
"percent": 10,
"valid_until": "2026-01-31"
}
Mini-programmes dans le stockage (difficiles à faire évoluer) :
{
"type": "discount",
"rule": "if (customer.orders == 0) return total * 0.9; else return total;"
}
La seconde version paraît flexible, mais elle pousse la complexité dans la couche de données : vous avez besoin d’un évaluateur sécurisé, de règles de versioning, de frontières de sécurité, d’outils de debug et d’un plan de migration quand le langage de règles change.
Quand l’information stockée reste simple et explicite, vous pouvez changer le comportement au fil du temps sans réécrire l’historique. Les anciens enregistrements restent lisibles. De nouveaux services peuvent être ajoutés sans « comprendre » l’exécution héritée. Et vous pouvez introduire de nouvelles interprétations—nouvelles vues UI, nouvelles stratégies de tarification, nouvelles analyses—en écrivant du code nouveau, pas en mutant la signification des données.
La plupart des systèmes d’entreprise ne tombent pas en panne parce qu’un module est « mauvais ». Ils tombent parce que tout est connecté à tout.
Le couplage fort se manifeste par des changements « petits » qui déclenchent des semaines de retesting. Un champ ajouté à un service casse trois consommateurs en aval. Un schéma de base partagé devient un goulot de coordination. Un cache mutable unique ou un singleton "config" devient une dépendance cachée de la moitié du codebase.
Le changement en cascade est la conséquence naturelle : quand beaucoup de parties partagent la même chose changeante, le rayon d’explosion grandit. Les équipes réagissent en ajoutant plus de processus, plus de règles et plus de transferts—ralentissant souvent encore la livraison.
Vous pouvez appliquer les idées de Hickey sans changer de langage ni tout réécrire :
Quand les données ne changent pas sous vos pieds, vous passez moins de temps à diagnostiquer « comment en est-on arrivé là ? » et plus de temps à raisonner sur ce que fait le code.
Les defaults sont le lieu où l’incohérence s’insinue : chaque équipe invente son propre format de timestamp, forme d’erreur, politique de retry et approche de concurrence.
De meilleurs defaults ressemblent à : schémas d’événements versionnés, DTOs immuables standards, propriété claire des écritures, et un petit ensemble de bibliothèques approuvées pour la sérialisation, la validation et le tracing. Le résultat : moins d’intégrations surprises et moins de correctifs ad hoc.
Commencez là où le changement a déjà lieu :
Cette approche améliore la fiabilité et la coordination d’équipe tout en gardant le système en marche—et en limitant la portée pour pouvoir terminer.
Il est plus facile d’appliquer ces idées quand votre workflow supporte une itération rapide et à faible risque. Par exemple, si vous développez dans Koder.ai (une plateforme de vibe-coding chat pour web, backend et mobile), deux fonctionnalités correspondent directement à l’esprit des « meilleurs defaults » :
Même si votre stack est React + Go + PostgreSQL (ou Flutter pour mobile), le point central reste : les outils que vous utilisez au quotidien enseignent silencieusement une manière de travailler. Choisir des outils qui rendent routine la traçabilité, le rollback et la planification explicite peut réduire la tentation du « on patchera après ».
Simplicité et immuabilité sont des defaults puissants, pas des règles morales. Ils réduisent le nombre de choses qui peuvent changer de façon inattendue, ce qui aide quand les systèmes grossissent. Mais les projets réels ont des budgets, des délais et des contraintes—et parfois la mutabilité est l’outil approprié.
La mutabilité peut être un choix pragmatique dans des hotspots de performance (boucles serrées, parsing à haut débit, graphismes, calcul numérique) où les allocations dominent. Elle peut aussi convenir lorsque le périmètre est contrôlé : variables locales, cache privé derrière une interface, ou composant mono-thread avec frontières claires.
La clé est la contention : si la « chose mutable » ne fuit pas, elle ne peut pas propager la complexité.
Même dans un style majoritairement fonctionnel, les équipes ont besoin d’une propriété claire :
C’est là que le biais de Clojure pour les données et les frontières explicites aide, mais la discipline est architecturale, pas liée au langage.
Aucun langage ne corrige de mauvaises exigences, un modèle de domaine flou, ou une équipe qui n’est pas d’accord sur la définition du « fini ». L’immuabilité ne rendra pas un workflow confus compréhensible, et du code « fonctionnel » peut toujours encoder de mauvaises règles métier—juste de façon plus propre.
Si votre système est déjà en production, ne traitez pas ces idées comme un tout-ou-rien. Cherchez le plus petit mouvement qui diminue le risque :
Le but n’est pas la pureté—c’est moins de surprises à chaque changement.
Ceci est une checklist de l’ordre d’un sprint que vous pouvez appliquer sans changer de langage, framework, ou organisation.
Rendez vos formes de données immuables par défaut. Considérez requêtes/réponses, événements et messages comme des valeurs qu’on crée une fois et ne modifie pas. Si quelque chose change, créez une nouvelle version.
Privilégiez les fonctions pures au centre des workflows. Commencez par un workflow (par ex. pricing, permissions, checkout) et refactorez le cœur en fonctions qui prennent des données en entrée et renvoient des données en sortie—sans lectures/écritures cachées.
Déplacez l’état vers moins d’endroits, et plus clairs. Choisissez une source de vérité par concept (statut client, feature flags, inventaire). Si plusieurs modules maintiennent des copies, faites-en une décision explicite avec une stratégie de sync.
Ajoutez un log append-only pour les faits clés. Pour un domaine, enregistrez « ce qui s’est passé » comme événements durables (même si vous conservez aussi l’état courant). Cela améliore la traçabilité et réduit les conjectures.
Définissez des defaults plus sûrs dans les APIs. Les defaults doivent minimiser les comportements surprenants : timezones explicites, gestion explicite des nulls, retries clairs, garanties d’ordonnancement explicites.
Cherchez des ressources sur simplicité vs facilité, gestion d’état, conception orientée valeur, immutabilité, et comment l’« historique » (faits dans le temps) aide le debug et l’exploitation.
La simplicité n’est pas une fonctionnalité qu’on greffe—c’est une stratégie que l’on pratique par des choix petits et répétables.
La complexité s’accumule par de petites décisions localement « raisonnables » (flags supplémentaires, caches, exceptions, helpers partagés) qui ajoutent des modes et du couplage.
Un bon signal d’alerte : une « petite modification » nécessite des edits coordonnés dans plusieurs modules ou services, ou quand les relecteurs doivent s’appuyer sur du savoir tribal pour juger de la sécurité.
Les raccourcis optimisent la friction d’aujourd’hui (le temps de livraison) tout en repoussant des coûts vers l’avenir : temps de debug, frais de coordination et risque au changement.
Une bonne habitude en revue de design/PR : demander « Quels nouveaux éléments mobiles ou cas particuliers cela introduit, et qui va les maintenir ? »
Les choix par défaut guident ce que les ingénieurs font sous pression. Si la mutation est le comportement par défaut, l’état partagé se propage. Si « en mémoire c’est suffisant » est le défaut, la traçabilité disparaît.
Améliorez les défaults en rendant la voie sûre la plus facile : données immuables aux frontières, timezones/null explicites, retries clairs et propriété d’état définie.
L’état est tout ce qui change dans le temps. Le problème est que chaque changement crée une opportunité de désaccord : deux composants peuvent avoir deux valeurs « actuelles » différentes.
Les bugs apparaissent comme des comportements dépendant du timing ("marche chez moi", problèmes intermittents en prod) parce que la question devient : sur quelle version des données avons-nous agi ?
L’immuabilité signifie qu’on n’édite pas une valeur en place ; on crée une nouvelle valeur qui représente la mise à jour.
Concrètement, cela aide parce que :
Ce n’est pas toujours à proscrire. La mutabilité peut être utile si elle est contenue :
La règle clé : ne laissez pas des structures mutables fuir les frontières où beaucoup de parties peuvent lire/écrire.
Les conditions de course viennent généralement des données partagées et mutables lues puis réécrites par plusieurs acteurs.
L’immuabilité réduit la surface de coordination parce que les écrivains produisent de nouvelles versions plutôt que de modifier un objet partagé. Il reste nécessaire d’avoir une règle pour publier la version courante, mais les données cessent d’être une cible mouvante.
Considérez les faits comme des enregistrements append-only de ce qui s’est produit (événements), et l’état actuel comme une vue dérivée de ces faits.
Vous pouvez commencer petit sans event sourcing complet :
Stocker l’information comme des données simples et explicites (valeurs), et exécuter du comportement contre ces valeurs. Évitez d’imbriquer des règles exécutables dans des enregistrements stockés.
Cela rend les systèmes plus évolutifs car :
Choisissez un workflow qui change souvent et appliquez trois étapes :
Mesurez le succès par moins de bugs intermittents, un blast radius réduit par changement, et moins de coordination pointilleuse aux releases.