Nguyen Le Phong

Les fondamentaux de l'architecture logiciellePartie 5 sur 6

Monolithe, Monolithe Modulaire, Microservices : Un Guide de Décision Honnête

Les microservices sont une taxe que vous payez pour la mise à l'échelle organisationnelle — pas un point de départ. Un parcours sans fioriture du monolithe au monolithe modulaire puis aux microservices, avec les signaux qui vous disent quand (et si) diviser.

Voici une scène qui se joue plus souvent qu'elle ne le devrait : une startup de trois personnes décide, dès le premier jour, de construire des microservices. Six mois plus tard, ils ont quinze services, un cluster Kafka que personne ne comprend vraiment, et un bug qui nécessite de tracer une requête à travers huit services pour le reproduire. L'équipe est épuisée. Le produit est à moitié fini. Et quelque part dans un thread Slack, quelqu'un écrit les mots on a essentiellement construit un monolithe distribué.

Les microservices sont réellement puissants — à la bonne échelle, avec la bonne équipe, pour les bonnes raisons. Mais ce sont une taxe que vous payez pour la mise à l'échelle organisationnelle, pas un point de départ. Ce guide parcourt honnêtement les trois formes architecturales — monolithe, monolithe modulaire et microservices — et vous donne les signaux pour savoir quand (et si) passer de l'une à l'autre.

Le monolithe : un seul déployable pour les gouverner tous

Un monolithe est une unité déployable unique. Un seul build, un seul binaire (ou un seul ensemble d'assets compilés), une seule étape de déploiement. Chaque fonctionnalité, chaque module, chaque requête de base de données vit dans le même processus.

Ça semble limité, et le mot lui-même a acquis une connotation légèrement embarrassante — comme si admettre que vous avez un monolithe revenait à admettre que vous n'êtes pas à jour avec l'ingénierie moderne. Ce cadrage est erroné, et il a causé beaucoup de douleur inutile.

Un monolithe est le bon défaut. Voici pourquoi :

  • Les appels locaux sont gratuits. Un appel de fonction à l'intérieur d'un processus prend des nanosecondes. Un appel réseau entre services prend des millisecondes — et il peut échouer, expirer, renvoyer un résultat partiel ou se perdre entièrement. Vous ne portez pas cette complexité jusqu'à ce que vous en ayez besoin.
  • Les transactions sont faciles. Une base de données relationnelle vous donne des transactions ACID sur tout votre modèle de données. Dans un monolithe, transférer de l'argent du compte A au compte B est une seule transaction. Dans les microservices, ça devient une transaction distribuée ou un saga — de vrais problèmes difficiles avec de vraies modalités d'échec.
  • Le débogage est direct. Un seul processus, un seul flux de logs, une seule trace de pile. Vous trouvez le problème. Dans un système distribué, une seule action utilisateur peut générer des dizaines d'opérations asynchrones à travers plusieurs services, et les corréler nécessite des outils d'observabilité dédiés.
  • Le déploiement est simple. Un seul artefact, un seul pipeline, une seule stratégie de rollback. Vous pouvez livrer en toute confiance dès le premier jour.
  • Les grandes entreprises livrent de grands monolithes. Shopify a fait tourner un monolithe Rails pendant des années à une énorme échelle. Stack Overflow sert des millions de requêtes par jour depuis une poignée de serveurs exécutant un monolithe. Un monolithe ne vous empêche pas de passer à l'échelle — il se met simplement à l'échelle différemment.

Les vraies douleurs d'un monolithe sont réelles, mais elles arrivent plus tard que les gens ne le pensent. Les temps de build augmentent avec le codebase — un rebuild complet qui prend deux minutes à 50k lignes en prend douze à 500k. La friction lors des fusions augmente quand vingt ingénieurs modifient tous le même codebase ; les branches longues et les conflits deviennent un rituel quotidien. Le couplage de déploiement signifie que chaque équipe déploie ensemble — un changement risqué d'une équipe peut bloquer la release de tout le monde.

Ces douleurs sont réelles. Mais ce sont des douleurs du succès, et elles arrivent bien plus tard que le battage médiatique ne le suggère. La plupart des équipes seraient chanceux d'y arriver.

Le monolithe modulaire : le point idéal que la plupart des équipes passent

Un monolithe modulaire est toujours une unité déployable unique — mais en interne, il est organisé en modules avec des frontières imposées. Les modules communiquent via des interfaces claires. Un module n'est pas autorisé à accéder directement aux internes d'un autre module : pas d'import de classes privées d'un voisin, pas de partage de tables de base de données, pas d'appel de fonctions d'aide internes à travers les lignes de modules.

Ça ressemble à une petite discipline, mais ça change considérablement le jeu.

Pensez-y comme au plan d'une maison. Un monolithe sans modules est un appartement studio : une grande pièce ouverte où tout est partout. Un monolithe modulaire vous donne des pièces avec des murs — vous vivez toujours dans une seule maison, vous partagez toujours la plomberie et la porte d'entrée, mais la cuisine et la chambre ont leurs propres destinations distinctes et vous ne cuisinez pas sur le lit.

Les modules correspondent naturellement aux contextes délimités du Domain-Driven Design : un module Billing, un module Inventory, un module Notifications. Chacun possède sa propre logique de domaine et sa propre tranche du schéma de base de données. Ils exposent une interface publique (une façade de service ou un ensemble de fonctions publiques) et gardent tout le reste privé.

Les bénéfices pratiques sont substantiels :

  • Raisonnement indépendant. Un développeur travaillant sur Billing peut le comprendre et le modifier sans avoir tout le codebase en tête.
  • Refactoring sûr. Vous pouvez réécrire les internes d'un module tant que l'interface publique est préservée — le reste de l'app ne le remarque pas.
  • Propriété claire. Les équipes revendiquent les modules. La revue de code reste focalisée. Intégrer un nouveau développeur signifie lui confier un module, pas un codebase de 200k lignes.
  • Coutures prêtes pour la migration. Si vous avez jamais besoin d'extraire un service, la frontière est déjà tracée. Le module devient un service. L'interface devient une API. Le travail est significatif, mais c'est un travail connu, pas de la chirurgie exploratoire.

Le monolithe modulaire est l'architecture que la plupart des équipes sautent directement, passant d'un monolithe enchevêtré aux microservices sans s'arrêter ici. C'est une erreur. Pour la majorité des équipes produit — moins de 100 ingénieurs, services comptés sur les doigts d'une main, un ou deux pipelines de déploiement — un monolithe modulaire bien maintenu est l'endroit le plus productif pour vivre.

Les microservices : ce que vous achetez réellement

Les microservices décomposent le système en services déployables indépendamment. Chaque service possède son propre processus, son propre pipeline de déploiement et — crucialement — son propre magasin de données. Les services se parlent via un réseau : HTTP, gRPC, files de messages, flux d'événements.

Quand les microservices fonctionnent bien, ils livrent quatre choses qu'aucune autre architecture ne peut égaler à grande échelle :

  • Déploiement indépendant. L'équipe paiements peut livrer un changement au service de paiements mardi après-midi sans coordonner avec l'équipe recommandations, l'équipe recherche ou l'équipe notifications. Dans une entreprise avec des centaines d'ingénieurs, c'est transformationnel — c'est la différence entre déployer une fois par semaine et déployer cent fois par jour.
  • Mise à l'échelle indépendante. Si votre service ImageProcessing a besoin de quarante cœurs CPU aux heures de pointe et que votre service UserProfile n'en a besoin que de deux, vous pouvez les mettre à l'échelle séparément. Dans un monolithe, vous mettez l'ensemble à l'échelle — vous achetez quarante cœurs pour chaque fonctionnalité que vous en ayez besoin ou non.
  • Autonomie des équipes. Chaque service est un petit produit autonome. Une équipe de cinq ingénieurs peut posséder un service complètement : choisir son langage, sa base de données, son cadence de déploiement. C'est le superpouvoir organisationnel des microservices — la Loi de Conway qui travaille pour vous plutôt que contre vous.
  • Isolation des pannes. Une fuite de mémoire dans le RecommendationEngine fait tomber le RecommendationEngine, pas la caisse. Vous pouvez dégrader gracieusement — le magasin fonctionne toujours, les produits n'ont juste pas de suggestions personnalisées. Dans un monolithe, une exception non catchée dans n'importe quel module peut faire tomber tout le processus.

Remarquez le pattern : tout dans cette liste concerne la mise à l'échelle organisationnelle et opérationnelle. Ce ne sont pas des préoccupations qui s'appliquent à une équipe de huit personnes construisant un produit qui n'a pas encore trouvé son product-market fit.

Ce que vous payez réellement : la taxe des systèmes distribués

Les microservices ne sont pas gratuits. Chaque bénéfice ci-dessus a un coût correspondant, et les coûts sont non négligeables. Les sous-estimer, c'est comment des équipes finissent épuisées et sous-livrées.

L'avertissement du monolithe distribué

Le pire résultat n'est ni les microservices ni un monolithe — c'est un monolithe distribué : des microservices tellement couplés qu'ils doivent être déployés ensemble, partagent la même base de données ou tombent ensemble. Vous payez chaque coût des microservices sans recevoir aucun des bénéfices. Ça arrive quand les équipes divisent par couche technique plutôt que par domaine métier, ou quand les services s'appellent mutuellement en synchrone dans de longues chaînes. Si le ServiceA ne peut se déployer qu'après le ServiceB, vous avez construit un monolithe distribué.

Voici la taxe en détail :

  • Les appels réseau échouent. Un appel de fonction ne peut pas échouer (sauf bug). Un appel réseau peut échouer, expirer, renvoyer une valeur mise en cache périmée, ou réussir lors d'une nouvelle tentative alors que la première avait déjà eu des effets de bord. Vous devez gérer les échecs partiels partout, en permanence.
  • Cohérence éventuelle. Les services possèdent leurs propres données. Maintenir cette cohérence à travers les frontières de services nécessite une conception soignée — patterns event-driven, sagas, opérations idempotentes, transactions compensatoires. Expliquer à un chef de produit pourquoi un utilisateur peut passer une commande mais ne la voit pas immédiatement dans son historique est délicat.
  • Les transactions distribuées sont difficiles. Le protocole à deux phases de validation existe et est pénible. La plupart des équipes utilisent le pattern Saga à la place, ce qui déplace la complexité de la base de données vers votre code applicatif. Les deux sont nettement plus complexes que BEGIN; UPDATE; COMMIT;.
  • Le débogage est une compétence différente. Une trace de pile ne vous dit plus ce qui s'est passé. Vous avez besoin de traçage distribué (Jaeger, Zipkin, Honeycomb), d'IDs de corrélation dans chaque ligne de log, de service meshes et de tableaux de bord qui corrèlent les événements entre services. Construire et maintenir cette infrastructure d'observabilité est un vrai travail d'ingénierie.
  • La charge opérationnelle se multiplie. Un service a besoin d'un Dockerfile, d'un déploiement Kubernetes, d'une règle d'ingress, d'un endpoint de health check, de limites CPU et mémoire, d'une configuration d'agrégation de logs et de règles d'alerte. Multipliez par trente services. Cette charge évolue linéairement avec le nombre de services ; vos effectifs d'ingénierie n'en font pas autant.
  • Les tests sont plus difficiles. Les tests unitaires sont toujours faciles. Mais les tests d'intégration qui traversent les frontières de services nécessitent d'exécuter plusieurs services simultanément, de gérer leurs versions et configurations, et de traiter les données de test à travers plusieurs bases de données. Les tests de contrat (Pact, etc.) aident mais ajoutent leur propre discipline à apprendre.

Aucun de ces coûts n'est insoluble — l'industrie dispose d'outils matures pour tous. Le point est qu'il s'agit de vrais coûts qui doivent être payés chaque jour, et ils ne valent pas la peine d'être payés tant que les bénéfices qu'ils débloquent ne sont pas réellement nécessaires.

Signaux : quand diviser (et quand ne pas le faire)

Alors comment savoir quand vous avez franchi le seuil où les microservices valent leur coût ? Voici les signaux qui indiquent réellement qu'il est temps :

  • Les besoins de mise à l'échelle indépendante sont réels et coûteux. Vous pouvez pointer un composant spécifique qui a besoin de 10 fois plus de ressources que le reste du système, et vous payez pour ces ressources à travers tout le monolithe. La division permettrait d'économiser un argent significatif ou améliorerait significativement les performances.
  • La contention de déploiement est chronique. Des équipes sont régulièrement bloquées dans leurs livraisons par les changements en cours d'autres équipes. La charge de coordination d'un déploiement partagé vous ralentit de façon mesurable — pas occasionnellement, mais comme une frustration hebdomadaire ou quotidienne.
  • Le nombre d'équipes l'exige. Vous avez plusieurs équipes de cinq ingénieurs ou plus travaillant sur des domaines métier distincts avec des cadences de déploiement différentes, des préférences technologiques différentes et des roadmaps vraiment indépendantes. La Loi de Conway prédit l'architecture sur laquelle vous allez aboutir — autant le planifier.
  • Un composant a des exigences fondamentalement différentes de fiabilité ou de sécurité. Les paiements, l'authentification et le stockage de PII justifient souvent une isolation non pour des raisons de performance mais pour la conformité, la réduction du rayon d'impact et la capacité à les auditer indépendamment.
  • Un module est déjà effectivement séparé. Il n'a aucun état partagé avec le reste du système, ne communique que via des événements ou APIs bien définis, et une équipe différente le possède. La couture organisationnelle existe déjà — en faire une frontière de service, c'est formaliser ce qui est déjà vrai.

Et voici les signaux que vous divisez trop tôt :

  • Votre organisation d'ingénierie entière compte moins de vingt personnes.
  • Vous divisez parce que ça semble plus évolutif plutôt que parce que vous avez atteint une limite concrète.
  • Les services que vous planifiez devraient être déployés ensemble pour fonctionner.
  • Vous ne pouvez pas encore tracer une frontière nette où chaque service possède ses propres données sans tables partagées.
  • Votre équipe ne dispose pas encore de l'infrastructure d'observabilité (traçage distribué, journalisation centralisée, alertes) pour déboguer un système distribué.
  • La principale raison est que les microservices figurent dans les offres d'emploi ou dans le marketing de l'entreprise.

Comment bien diviser : coutures, strangler figs et propriété des données

Si les signaux ci-dessus pointent vers une division, le comment importe énormément. Diviser mal produit le monolithe distribué décrit plus haut. Diviser bien produit des services réellement indépendants qui se justifient d'eux-mêmes.

Divisez selon les contextes délimités, pas les couches techniques. Ne créez pas un DatabaseService ou un ValidationService — ce sont des préoccupations techniques, pas métier. Créez un OrderService, un BillingService, un InventoryService — des unités de capacité métier avec des significations claires et stables dans votre domaine. Un contexte délimité est une partie du domaine où un modèle spécifique s'applique et où le langage est cohérent. C'est là qu'appartiennent les frontières de service.

Utilisez le pattern strangler fig. Nommé d'après une liane qui enveloppe progressivement un arbre hôte, le pattern strangler fig vous permet d'extraire des services de façon incrémentale plutôt que tout d'un coup. Vous démarrez le nouveau service aux côtés du monolithe, routez une tranche spécifique du trafic vers lui, vérifiez qu'il fonctionne, puis supprimez le code correspondant du monolithe. Le monolithe rétrécit avec le temps ; il n'est jamais réécrit. C'est plus sûr, plus réversible et bien moins susceptible d'aboutir à une migration big-bang de six mois qui ne se termine jamais vraiment.

Extrayez un service à la fois. Chaque extraction vous apprend quelque chose. La deuxième extraction se passera mieux que la première. Essayer d'extraire cinq services simultanément distribue l'apprentissage et multiplie le risque.

La propriété des données est non négociable. Un service qui partage une table de base de données avec un autre service n'est pas un service — c'est un module avec une surcharge réseau supplémentaire. Chaque service doit posséder ses propres données. Si deux services ont besoin des mêmes données, l'un est autoritaire et l'autre les récupère via une API ou les synchronise via des événements. Cette contrainte est douloureuse à établir et douloureuse à maintenir, mais c'est ce qui vous donne le déploiement indépendant et l'isolation des pannes pour lesquels vous êtes venu.

Utilisez des ports pour définir les coutures avant de les extraire. Si vous avez suivi une approche Ports & Adapters à l'intérieur de votre monolithe modulaire, l'extraction devient presque mécanique : le port définit l'API, l'adapter derrière lui devient le client du service. La logique de domaine ne change pas — seul le mécanisme de livraison change. C'est l'un des arguments pratiques les plus forts pour construire avec des ports dès le départ : ils vous donnent des coutures prêtes à l'extraction gratuitement.

Comparaison côte à côte

Dimension Monolithe Monolithe Modulaire Microservices
Unité de déploiement Un processus, un pipeline Un processus, un pipeline Plusieurs processus, plusieurs pipelines indépendants
Propriété des données Base de données partagée ; tout le code peut toucher toutes les tables Base de données partagée ; les modules possèdent leurs zones de schéma par convention Chaque service possède sa propre base de données ; pas de tables partagées
Mode de panne Un crash du processus fait tomber toute l'app Un crash du processus fait tomber toute l'app Les pannes de service sont isolées ; les autres dégradent gracieusement
Adapté aux équipes Idéal pour 1–3 équipes ; la friction augmente avec le nombre d'équipes Idéal pour 2–8 équipes ; les modules s'alignent sur la propriété des équipes Nécessaire pour 5+ équipes livrant à des cadences indépendantes
Coût opérationnel Faible : un déploiement, un flux de logs, un ensemble d'alertes Faible : même empreinte opérationnelle qu'un monolithe simple Élevé : observabilité, orchestration de conteneurs, service mesh, CI/CD par service
Modèle de transaction Transactions ACID complètes trivialement Transactions ACID complètes trivialement Sagas ou cohérence éventuelle ; les transactions distribuées sont difficiles
Quand utiliser Nouveaux produits, petites équipes, domaine inconnu Produit en croissance avec des frontières de domaine claires, 10–100 ingénieurs Plusieurs équipes autonomes, modèle de domaine éprouvé, vrais besoins de mise à l'échelle indépendante

La vue honnête par taille d'entreprise

Les conseils d'architecture ont tendance à venir d'entreprises qui ont déjà franchi le seuil où les microservices font sens — parce que ce sont celles qui ont des blogs d'ingénierie, des conférences et les ressources pour écrire des post-mortems détaillés. Ça crée un biais de survie. Voici une carte plus honnête :

Évolution en trois étapes : Monolithe, Monolithe Modulaire et Microservices présentés de gauche à droite avec des flèches entre eux. STAGE 1 STAGE 2 STAGE 3 Monolith One deployable All Code one process shared DB enforce boundaries Modular Monolith One deployable · internal modules Billing Orders Notif. shared DB · owned schemas split at seams Billing Orders Notif. own DBs · network calls
Les trois étapes architecturales. Étape 1 : une seule boîte, tout ensemble. Étape 2 : une seule boîte avec des frontières de modules internes imposées (lignes en pointillé). Étape 3 : des boîtes séparées connectées par des appels réseau (flèches en pointillé), chacune avec son propre cylindre de base de données. Vous pouvez vous arrêter à n'importe quelle étape — la bonne forme dépend de votre équipe, pas de ce qui est à la mode.

Startup (1–15 ingénieurs). Construisez un monolithe. Vous ne savez pas quelles parties de votre système auront besoin d'être mises à l'échelle, quelles fonctionnalités survivront, ni quelles frontières de domaine sont réelles. La décomposition prématurée fixe des décisions avant que vous ayez suffisamment d'informations pour les prendre. Shopify, GitHub et Basecamp ont tous démarré comme des monolithes Rails. Twitter aussi, et ils l'ont fait tourner pendant des années à une échelle significative avant de le décomposer sous une vraie charge.

Scale-up (15–80 ingénieurs, en croissance). C'est là que le monolithe modulaire justifie son existence. Vous avez suffisamment d'ingénieurs pour que la croissance non contrôlée dans un seul codebase cause une friction réelle, mais vous déployez encore en équipe et la surcharge des microservices complets tuerait votre vélocité. Investissez dans les frontières de modules, la propriété des équipes et des interfaces propres. Gardez l'option d'extraire des services, mais ne l'exercez pas encore sauf si un besoin de mise à l'échelle concret vous y force.

Enterprise / grande organisation (80+ ingénieurs, plusieurs équipes autonomes). Les microservices sélectifs font sens ici — mais sélectifs est le mot-clé. Les architectures les plus efficaces à grande échelle ne sont ni des monolithes purs ni une mer de services de taille identique : ce sont un mélange réfléchi. Une poignée de services core pour les capacités les plus critiques, un monolithe modulaire pour le milieu opérationnel, et quelques services spécialisés où la mise à l'échelle indépendante ou la conformité l'exige réellement. Amazon n'a pas tout décomposé d'un coup ; ils ont identifié les coutures sous charge et les ont extraites une par une.

La vraie leçon de Netflix et Amazon

Quand Netflix et Amazon décrivent leurs parcours avec les microservices, la partie qui est citée est le diagramme d'architecture avec des centaines de services. La partie qui est omise est qu'ils ont démarré comme des monolithes, les ont fait tourner jusqu'à ce que la douleur soit indéniable, puis ont investi des années et d'énormes efforts d'ingénierie dans la transition — y compris la construction d'une grande partie des outils qui rendent aujourd'hui les microservices viables. Ils vous disent où ils sont arrivés, pas où vous devriez commencer.

Points clés à retenir

  • Le monolithe est le bon défaut. Simple à construire, facile à déboguer, trivial pour les transactions. Les douleurs arrivent plus tard que le battage médiatique ne le suggère et seulement à une vraie échelle.
  • Le monolithe modulaire est l'option la plus sous-utilisée. Les frontières imposées entre les modules internes vous donnent la clarté d'équipe, le refactoring sûr et des coutures prêtes à l'extraction — sans aucune complexité de systèmes distribués.
  • Les microservices sont une taxe, pas une fonctionnalité. Vous payez en pannes réseau, cohérence éventuelle, traçage distribué et charge opérationnelle. La contrepartie — déploiement indépendant, mise à l'échelle indépendante, autonomie des équipes — est réelle mais seulement à l'échelle qui l'exige.
  • Le monolithe distribué est le pire résultat. Des services étroitement couplés qui doivent être déployés ensemble vous donnent tous les coûts des microservices et aucun des bénéfices. Divisez selon les contextes délimités, pas les couches techniques.
  • La propriété des données est la contrainte dure. Un service qui partage une base de données avec un autre service n'est pas un service. Chaque service doit posséder ses propres données ; la cohérence à travers les frontières nécessite une conception explicite.
  • Utilisez le pattern strangler fig pour diviser de façon incrémentale. Extrayez un contexte délimité à la fois. Chaque extraction vous apprend quelque chose et est réversible. Ne faites jamais une décomposition big-bang.
  • Adaptez l'architecture à la taille de l'équipe, pas à l'aspiration. Startup : monolithe. Scale-up : monolithe modulaire. Grandes équipes autonomes : microservices sélectifs là où c'est vraiment nécessaire.

Les décisions d'architecture se propagent dans le temps et s'accumulent. La prochaine question que beaucoup d'équipes affrontent après avoir décidé de diviser est ce qu'il faut faire avec le frontend — s'il faut servir une interface unifiée unique ou la décomposer aussi. Ce terrier de lapin est exploré dans Micro-Frontends : Quand, Pourquoi et Ce Qu'ils Coûtent Réellement.