Le contexte : des microservices avant que le produit les ait merites
Quand je suis arrive sur le projet, l'architecture etait deja en place : sept microservices, un broker RabbitMQ, des pipelines de deploiement separes, et une equipe de quatre developpeurs qui essayait de garder le systeme en mouvement.
La decision initiale de partir sur du distribue avait une forme de logique en 2020. Les microservices etaient partout : conferences, articles, fiches de poste, plans de recrutement. L'equipe precedente avait suivi le reflexe architectural du moment et concu le produit avec une logique distribuee avant que le produit lui-meme ait ete valide par suffisamment d'utilisateurs.
C'est plus frequent qu'on ne l'admet.
Les microservices sont souvent adoptes comme une ambition architecturale plutot que comme une reponse a une contrainte precise et prouvee. Le pattern resout de vrais problemes : deploiement independant, scalabilite isolee, autonomie des equipes a l'echelle, frontieres reglementaires, isolation des pannes. Mais ces benefices n'apparaissent que lorsque l'organisation, la maturite produit et le profil de trafic justifient le cout.
Ici, ce n'etait pas le cas.
Les symptomes : la complexite distribuee etait devenue le goulot d'etranglement
Le systeme n'etait pas en echec spectaculaire. Pas d'incident majeur, pas de catastrophe unique, pas de signal evident qui imposait une reecriture.
Il fonctionnait. Mais fonctionner et bien fonctionner sont deux etats differents.
La latence s'etait installee dans des parcours utilisateurs ordinaires. Chaque operation significative cote utilisateur traversait au moins deux frontieres de service en HTTP. Envoyer un email de confirmation, par exemple, impliquait un appel API vers un service mailer, une queue, de la logique de retry, et un cycle de deploiement separe. Rien de tout cela n'etait absurde pris isolement. Ensemble, cela rendait le systeme plus lent que necessaire.
La complexite d'infrastructure etait disproportionnee. Quatre developpeurs maintenaient sept configurations de deploiement, sept jeux de variables d'environnement, sept dispositifs de health-check, et une topologie RabbitMQ qui demandait de l'attention des qu'une fonctionnalite touchait plus d'un service.
L'experience de developpement local s'etait degradee. Onboarder un nouveau developpeur signifiait expliquer non seulement le domaine, mais aussi la topologie. Lancer toute la stack localement signifiait orchestrer sept processus. Debugger un bug transverse signifiait passer d'un codebase a l'autre, entre logs, queues et etats de deploiement.
Le cout etait difficile a defendre. Chaque application avait besoin d'au moins trois pods pour assurer la resilience en production. Sept applications representaient environ vingt et un pods par environnement. Avec staging, UAT et production, la plateforme portait ce cout trois fois. Pour le trafic que nous avions a ce moment-la, ce n'etait pas de la scalabilite. C'etait du cout architectural.
La decision : choisir la proportion plutot que la purete
Proposer de passer de microservices a un monolithe en 2023 avait presque quelque chose de contrariant. L'industrie avait passe des annees a evangeliser le distribue, et suggerer le mouvement inverse demandait un argument defensible.
L'argument etait simple : l'architecture ne correspondait plus a l'equipe, au produit, ni a la realite operationnelle.
Taille de l'equipe. La loi de Conway fonctionne dans les deux sens. Avec quatre developpeurs qui partageaient le contexte au quotidien, il n'y avait aucune raison organisationnelle de decouper le systeme en sept unites deployables. L'equipe n'etait pas divisee en groupes autonomes proprietaires de services. L'architecture simulait une organisation qui n'existait pas.
Time to market. Certaines fonctionnalites exigeaient des changements dans plusieurs microservices. Cela signifiait plusieurs pull requests, des releases coordonnees, du sequencing d'API, et plus d'endroits ou le flux de delivery pouvait se bloquer. Ramener la logique dans une seule application permettait de livrer les memes changements avec une pull request et un deploiement.
Simplicite operationnelle. Une unite deployable signifiait un pipeline CI, un chemin de rollback, une surface de health-check, et un seul jeu de configuration runtime. Meme l'application front-end pouvait etre servie comme assets statiques depuis le backend, ce qui supprimait une ligne de deploiement supplementaire.
Nous n'abandonnions pas les systemes distribues parce qu'ils seraient mauvais. Nous reconnaissions que leur cout doit etre proportionne a leur benefice. Le notre ne l'etait pas.
La strategie de migration : un groupe de routes a la fois
La migration avait une contrainte dure : pas de big bang.
Le produit etait en production. L'equipe ne pouvait pas se permettre un gel, une reecriture parallele ou une bascule risquee.
L'architecture existante nous donnait une frontiere de migration utile. L'application principale exposait deja des routes HTTP avec des prefixes specifiques aux services : /mailer, /auth, /notifications, etc. Chaque prefixe correspondait a un microservice aval. Cela nous donnait une unite de migration propre : un groupe de routes a la fois.
Pour chaque service, le processus restait le meme.
D'abord, nous reimplementions la logique du service dans l'application principale derriere le prefixe de route existant. L'interface externe ne changeait pas. Les appelants ne pouvaient pas savoir si le handler etait local ou distant.
Ensuite, nous introduisions un feature flag. Flag desactive, le trafic continuait vers le service externe. Flag active, le trafic passait par le nouveau handler local. Cela donnait un rollout controle et un chemin de rollback immediat.
Enfin, une fois le handler local execute en production sans probleme, nous activions le flag de maniere permanente, supprimions l'appel HTTP sortant, drainions la queue RabbitMQ et decommissionnions le service aval.
Nous avons repete ce pattern sur les sept services pendant plusieurs semaines. A aucun moment nous n'avons deploye de changement cassant. A aucun moment nous n'avons interrompu le systeme.
Les resultats : moins de pieces mobiles, plus de vitesse
La premiere fusion de service a rendu l'impact visible. La migration complete l'a rendu structurel.
| Zone | Avant | Apres |
|---|---|---|
| Unites deployables | 7 services | 1 application |
| Lignes CI/deploiement | 7 chemins separes | 1 pipeline CI |
| Empreinte runtime | Environ 21 pods par environnement | 3 pods par environnement |
| Environnements | Staging, UAT et production payaient le cout du distribue | Memes environnements, surface operationnelle reduite |
| Setup local | 7 processus a coordonner | 1 processus |
| Livraison feature | Plusieurs pull requests pour le travail transverse | 1 pull request pour la plupart des fonctionnalites |
| Deploiement front-end | Ligne de delivery separee | Assets statiques servis par le backend |
Le time to market s'est ameliore parce que le cout de coordination s'est effondre. Des fonctionnalites qui demandaient auparavant des changements dans deux ou trois services pouvaient maintenant etre livrees avec une seule pull request et un seul deploiement.
La latence a baisse parce que les appels HTTP inter-services sont devenus des appels de fonction in-process. Les aller-retours reseau, la serialisation et les hops de retry ont disparu des parcours utilisateurs ordinaires.
L'equipe a travaille avec moins de friction. Lancer toute la stack localement est devenu ennuyeux, dans le bon sens du terme. Debugger un bug ne demandait plus de correler des logs entre plusieurs deployables. L'onboarding est revenu vers la comprehension du produit plutot que la memorisation de la topologie.
Le cout d'infrastructure est aussi devenu plus facile a justifier. Trois pods par environnement correspondaient beaucoup mieux au trafic reel et aux besoins de resilience que sept applications avec trois pods chacune sur staging, UAT et production.
Un seul deploiement semble trivial tant qu'une equipe n'a pas vecu sans. Une seule unite deployable signifie un rollback, un chemin de health-check, et une seule reponse a la question : qu'est-ce qui tourne actuellement en production ?
Ce que je ferais differemment
Deux lecons ressortent.
D'abord, j'aurais redessine l'architecture interne pendant la migration. Nous avons deplace le comportement depuis des services externes vers le monolithe, mais nous avons conserve trop de structure heritee. La migration etait une occasion de definir des frontieres internes plus fortes : logique domaine isolee du code framework, adaptateurs d'infrastructure gardes aux bords, et use cases exprimes de maniere testable sans demarrer toute l'application.
Autrement dit, j'aurais utilise la migration pour rendre le monolithe modulaire par conception, pas seulement plus petit par topologie de deploiement.
Ensuite, j'aurais ecrit la suite de tests d'integration avant de commencer. Nous avons migre avec trop de validation manuelle et d'observation en production. Cela a fonctionne, mais c'etait inconfortable. Une suite d'integration solide aurait rendu la preservation du comportement objective. La definition de done pour chaque groupe de routes aurait du etre : l'ancien comportement est couvert, le nouveau handler local passe les memes checks, et le feature flag peut basculer le trafic sans changer le contrat.
Les deux lecons pointent dans la meme direction : les migrations sont des moments architecturaux. Il faut les utiliser pleinement.
Quand les microservices sont la bonne reponse
Ce n'est pas un argument contre les microservices. C'est un argument contre le distribue premature.
Les microservices sont la bonne reponse lorsqu'ils resolvent un probleme que le produit a reellement.
Ils ont du sens lorsque les equipes sont assez grandes pour que la coordination coute plus cher que la distribution. Ils ont du sens lorsque des composants ont des profils de scalabilite vraiment differents, par exemple le traitement video par rapport a l'authentification. Ils ont du sens lorsque des contraintes reglementaires, de securite ou d'isolation des donnees exigent des frontieres runtime fortes. Ils ont du sens lorsque le deploiement independant n'est pas seulement confortable, mais necessaire a l'autonomie des equipes.
Une equipe de quatre developpeurs qui livre un produit SaaS B2B n'a pas les memes besoins architecturaux qu'une organisation de mille ingenieurs qui opere une plateforme mondiale.
Copier les patterns de la seconde quand on est la premiere, ce n'est pas de l'ambition. C'est du surcout.
La checklist de decision
Avant de choisir des microservices, prouvez au moins une de ces contraintes :
- Des equipes independantes ont besoin d'un ownership et de cycles de release independants.
- Une partie du systeme a un profil de scalabilite que le reste ne partage pas.
- La regulation, la securite ou l'isolation des donnees exige une frontiere runtime dure.
- L'isolation des pannes compte suffisamment pour payer le cout operationnel.
- L'organisation peut operer plusieurs services sans ralentir le delivery.
Si rien de cela n'est vrai, commencez par un monolithe modulaire.
Ce n'est pas un retour en arriere. C'est une decision de ne depenser la complexite architecturale que lorsqu'elle achete quelque chose de reel.