jguillaumesio
architecturedevops

L'architecture compte plus que le code propre

J'ai passé 2 semaines à rendre mes fonctions serverless propres. L'architecture était mauvaise. J'ai tout réécrit en un seul serveur Express et c'était plus simple.

Comparaison architecture contre code propre

J’ai passé 2 semaines à refactoriser une configuration serverless. Chaque fonction était propre. Gestion d’erreurs correcte, entrées typées, logs structurés, tests unitaires.

C’était quand même un bazar.

Les fonctions communiquaient entre elles via 4 canaux différents : files SQS, événements S3, API Gateway et invocation Lambda directe. Une seule requête utilisateur touchait 6 fonctions. Tracer un bug voulait dire fouiller 6 groupes de logs différents. Déployer un changement voulait dire coordonner 3 services.

J’ai tout réécrit en un seul serveur Express. Un processus. Un flux de logs. Un déploiement. Le code était moins “propre” (pas de fonctions séparées, pas de patterns événementiels) mais le système était radicalement plus simple.

L’architecture compte plus que le code propre. Vous pouvez écrire des fonctions parfaites dans une mauvaise structure et obtenir quand même un système que personne ne veut toucher.

Des exemples concrets où l’architecture comptait plus que la qualité du code

Exemple 1 : le monolithe serverless qui n’en était pas un

On avait 14 fonctions Lambda pour une appli CRUD. Chaque fonction était superbement écrite. Interfaces typées, bonnes frontières d’erreur, 90 % de couverture de tests.

Le problème : chaque fonction partageait un pool de connexions à la base via une variable globale. Les démarrages à froid donnaient des connexions périmées. Une seule fonction lente bloquait tout le pool. Déboguer voulait dire corréler 14 groupes de logs CloudWatch avec des politiques de rétention différentes.

On les a fusionnées en un seul serveur Express. Le code est passé de 14 fonctions propres à un fichier de 600 lignes avec des handlers en ligne. C’était “plus moche” selon toutes les métriques de qualité de code. Mais les démarrages à froid ont disparu, le pooling de connexions a fonctionné normalement, et déboguer voulait dire lire un seul flux de logs.

La version “propre” prenait 45 minutes pour déboguer un incident en production. La version “moche” en prenait 5.

Exemple 2 : le microservice qui servait 12 utilisateurs

Un projet sur lequel j’ai consulté avait 3 microservices : auth, API et notifications. Chacun avait son propre dépôt, son propre pipeline CI, sa propre base de données.

Le produit avait 12 utilisateurs actifs. Un seul développeur maintenait les 3 services.

Déployer une fonctionnalité qui touchait les 3 services prenait 2 jours : mettre à jour auth, déployer, mettre à jour l’API, déployer, mettre à jour les notifications, déployer. Chaque déploiement devait être séquencé correctement sinon le système cassait. Les rollbacks voulaient dire revenir en arrière sur 3 services dans l’ordre inverse.

On les a fusionnés en un monolithe. Un dépôt, un déploiement, une base de données. Le code était moins modulaire. Les frontières entre “services” étaient juste des dossiers au lieu d’appels réseau. Mais déployer une fonctionnalité est passé de 2 jours à 10 minutes.

Exemple 3 : le système événementiel avec 2 événements

Un projet perso d’e-commerce utilisait une architecture événementielle. Chaque action publiait des événements. Commande créée, paiement reçu, e-mail envoyé, stock mis à jour.

Le problème : la partie “événementielle”, c’était 300 lignes de code de bus d’événements pour 4 événements qui se produisaient toujours dans l’ordre. Commande créée → paiement reçu → e-mail envoyé. Ils n’étaient jamais indépendants. Ils n’étaient jamais consommés par plus d’un handler.

Le bus d’événements ajoutait de la latence (chaque saut faisait 50 à 200 ms), de la complexité (schémas d’événements, logique de retry, files de lettres mortes) et de l’opacité (tracer un paiement voulait dire suivre des événements à travers 3 services).

On l’a remplacé par des appels de fonction synchrones. Le tunnel de paiement est passé de 4 événements à travers 3 services à une fonction qui appelait 3 helpers. La latence a chuté de 600 ms. Le code était plus facile à lire de haut en bas.

Exemple 4 : la base par service qui partageait quand même les données

Une équipe avec laquelle j’ai travaillé suivait strictement le pattern “une base de données par service”. Le service utilisateur avait sa propre base. Le service commande avait sa propre base. Le service notification avait sa propre base.

Mais les commandes avaient besoin des données utilisateur. Les notifications avaient besoin des données de commande. Alors ils ont construit des appels d’API entre services pour aller chercher les données nécessaires. Un seul chargement de page déclenchait 3 appels d’API internes pour assembler des données qui auraient tenu en une seule jointure.

L’architecture “correcte” voulait dire 3 bases de données, 3 pools de connexions, 3 stratégies de sauvegarde et des appels réseau qui étaient avant des jointures. Une simple requête “montre-moi mes commandes avec les détails utilisateur” était devenue un problème de systèmes distribués.

On a consolidé vers une seule base avec une bonne séparation de schémas. Le code était “moins propre” (une base au lieu de 3) mais les requêtes étaient plus simples, les sauvegardes plus simples, et les chargements de page 3 fois plus rapides.

Exemple 5 : le cluster k3s qui faisait tourner un seul blog

Je fais tourner mon blog sur k3s. Un pod, un conteneur, un service NodePort. Le blog reçoit peut-être 50 visites par jour.

J’aurais pu le faire “comme il faut” : déploiement multi-réplicas, contrôleur d’ingress, cert-manager pour le HTTPS, autoscaler horizontal de pods, service mesh pour l’observabilité.

À la place, c’est un seul conteneur Caddy qui sert des fichiers statiques depuis un PVC. Le déploiement, c’est npm run build + rollout restart. Si ça tombe, je redémarre le pod. Infrastructure totale : 3 fichiers YAML.

La configuration “comme il faut” aurait pris une journée à mettre en place et demanderait une maintenance continue. La configuration simple a pris 20 minutes et tourne depuis des mois sans intervention.

Le schéma récurrent

Chaque exemple ci-dessus suit le même arc :

  1. Choisir une architecture parce que c’est “la bonne façon” (serverless, microservices, événementiel, une base par service, Kubernetes)
  2. Écrire du code propre à l’intérieur de cette architecture
  3. Passer tout son temps à se battre contre l’architecture au lieu de livrer des fonctionnalités
  4. Simplifier l’architecture
  5. Le code devient “moins propre” mais le système devient radicalement plus facile à manipuler

Quand le “code propre” ne peut pas vous sauver

Les pratiques de code propre (petites fonctions, interfaces typées, couverture de tests, gestion d’erreurs) opèrent au niveau de la fonction. Elles rendent les pièces individuelles plus faciles à comprendre.

Mais elles n’aident pas quand :

  • Une requête touche 6 services et vous ne pouvez pas la tracer
  • Déployer un changement demande de coordonner 3 dépôts dans l’ordre
  • Une simple requête demande 4 appels d’API parce que chaque service possède ses données
  • Les démarrages à froid ajoutent 3 secondes à chaque requête parce que vos fonctions partagent un pool de connexions
  • Vous passez plus de temps sur l’infrastructure que sur les fonctionnalités

Ce sont des problèmes d’architecture. Aucune quantité de fonctions propres ne les règle.

Ce que je fais vraiment maintenant

Je commence par l’architecture la plus simple qui pourrait fonctionner :

  • Un processus jusqu’à ce que vous ayez une raison de découper
  • Une base de données jusqu’à ce que vous ayez une raison de séparer
  • Des appels synchrones jusqu’à ce que vous ayez une raison d’ajouter des événements
  • Un dépôt jusqu’à ce que vous ayez une raison de découper
  • Un serveur jusqu’à ce que vous ayez une raison d’orchestrer

Puis j’ajoute de la complexité seulement quand je rencontre une vraie contrainte :

  • “Cette fonction doit passer à l’échelle indépendamment” → on la découpe
  • “Ces données sont accédées par plusieurs services” → on sépare la base
  • “Cette opération prend trop de temps et bloque la réponse” → on la rend asynchrone
  • “Cette équipe ne peut pas travailler indépendamment avec un seul dépôt” → on le découpe

Le mot-clé, c’est “vraie”. Pas “on pourrait en avoir besoin un jour”. Pas “la bonne pratique dit”. Une contrainte réelle, actuelle, mesurée.

En résumé

Le code propre est une optimisation locale. L’architecture est une optimisation globale. Vous pouvez avoir des fonctions parfaites dans une mauvaise structure et obtenir quand même un système pénible à déployer, déboguer et faire évoluer.

Commencez simple. Ajoutez de l’architecture quand vous avez la preuve d’en avoir besoin. La meilleure architecture est celle qui rend le système entier facile à comprendre, pas celle qui rend chaque pièce belle isolément.