Monolithe, modulaire, microservices
C’est la première question qui dépasse le périmètre d’une application, et celle qui est le plus systématiquement mal posée. « Faut-il faire des microservices ? » n’a pas de réponse, parce que ce n’est pas une question d’architecture : c’est une question de topologie de déploiement.
La décision de modélisation — où passent les frontières du métier — est un sujet distinct, traité par les contextes délimités. Cette page traite de ce qui vient après : quelles frontières déjà identifiées méritent de devenir des unités de déploiement séparées, et à quel prix.
Les quatre formes
| Monolithe | Monolithe modulaire | Microservices | Monolithe distribué | |
|---|---|---|---|---|
| Unités de déploiement | 1 | 1 | N | N |
| Frontières internes | Floues ou absentes | Explicites et vérifiées | Le réseau | Floues |
| Base de données | Une, partagée | Une instance, schémas séparés | Une par service | Partagée |
| Appel entre modules | Appel de fonction | Appel de fonction via une API de module | Appel réseau | Appel réseau |
| Déplacer une frontière | Facile | Facile | Très coûteux | Très coûteux |
| Défaillance partielle | Impossible | Impossible | Permanente | Permanente |
| Verdict | Défaut acceptable | Le bon défaut | Sur besoin nommé | À éviter absolument |
La quatrième colonne est le résultat le plus fréquent d’une migration mal conduite : on a payé le prix du réseau — latence, pannes partielles, complexité d’exploitation — sans obtenir le bénéfice, puisque les services restent couplés par un modèle ou une base commune. On déploie séparément des choses qui doivent changer ensemble.
Le monolithe n’est pas le problème
Le mot est devenu une insulte, et c’est une confusion qu’il faut défaire d’emblée : ce qui est mauvais, ce n’est pas le monolithe, c’est le monolithe sans structure interne — la grande boule de boue.
Ce sont deux choses différentes, et les avoir confondues est ce qui a vendu les microservices à des équipes qui n’en avaient pas besoin. Le raisonnement implicite était : notre code est un plat de spaghettis, donc découpons-le en services. Or distribuer des spaghettis produit des spaghettis distribués — avec de la latence en prime. La discipline qui manquait manquera toujours après.
Un monolithe avec des frontières internes réelles est une excellente architecture. C’est même la seule qui permette de se tromper sur l’emplacement d’une frontière sans que cela coûte un trimestre.
Le monolithe modulaire
C’est la thèse de cette page : c’est le bon défaut, et la grande majorité des systèmes n’ont jamais besoin d’en sortir.
Une seule unité de déploiement, N modules dont les frontières sont aussi réelles que celles de services — mais franchies par un appel de fonction plutôt que par le réseau. On obtient l’essentiel du bénéfice de la séparation, sans en payer le prix.
Quatre conditions le rendent réel. Aucune n’est optionnelle : il suffit qu’une seule manque pour retomber sur un monolithe ordinaire avec des dossiers bien nommés.
Une API de module explicite. Chaque module expose une surface publique minuscule ; tout le reste est inaccessible depuis l’extérieur. C’est le point le plus important, et le seul qui demande un mécanisme de langage.
Des schémas de base séparés. Une seule instance PostgreSQL, mais un schéma par module, et aucune jointure inter-schémas. C’est la condition qu’on sacrifie le plus souvent, et sa violation est irréversible en pratique : une fois que trois rapports font des jointures entre modules, la frontière n’existe plus.
Aucune référence directe aux entités d’un autre module. On échange des identifiants et des structures de données plates, jamais des objets du domaine voisin.
Une frontière vérifiée par la CI, pas par la discipline — voir les tests d’architecture.
L’API de module
La frontière est portée par les exports du package.json du module, dans un
workspace npm ou pnpm, et vérifiée par dependency-cruiser.
Python n’offre aucune visibilité inter-paquets : la frontière tient entièrement à
import-linter, dont les contrats déclarent que modules.expedition ne peut
atteindre que modules.facturation — et non ses sous-paquets.
C’est la version la plus solide des trois : la frontière est garantie par le
compilateur, sans configuration ni linter. Un mod privé n’est pas atteignable,
point final.
Tip
Le détail qui rend l’extraction future presque gratuite : donner à l’API de module une signature de type distant dès le départ. Données plates en entrée, résultat explicite en sortie — pas d’objet du domaine, pas d’exception qui traverse, pas de transaction partagée. Le jour où ce module devient un service, l’appelant ne change pas : seule l’implémentation de la façade devient un client HTTP.
C’est aussi un coût — on écrit une traduction pour un appel en mémoire — et il ne se justifie que sur les modules dont on pense sérieusement qu’ils partiront un jour.
L’arborescence
Chaque module est un hexagone
complet, avec en plus une façade publique. Le dossier partage/ est à surveiller de
près : c’est un noyau partagé, légitime pour des identifiants, désastreux dès qu’il
contient des règles.
Ce que le réseau change
Voici le prix réel, et c’est la section qui devrait décider à elle seule. Chaque ligne est une propriété qu’un appel de fonction offre gratuitement et qu’un appel réseau facture.
| Appel de fonction | Appel réseau | |
|---|---|---|
| Résultat | Il retourne ou il lève | Il peut expirer alors que le travail a été fait |
| Atomicité | Une transaction couvre l’ensemble | Aucune. Compensation ou cohérence différée |
| Latence | Nanosecondes | Millisecondes, qui s’additionnent |
| Débogage | Une pile d’appels | Une trace distribuée à corréler |
| Déploiement | Atomique | Compatibilité ascendante obligatoire |
| Types | Vérifiés à la compilation | Un contrat à versionner et à tester |
| Données du voisin | Une lecture | Une copie locale, donc périmée |
| Refactoring inter-module | Un renommage | Une migration en deux temps |
Trois conséquences méritent d’être détaillées, parce qu’elles surprennent toujours.
L’expiration ambiguë. Un appel réseau qui échoue ne dit pas si le travail a été effectué. Toute opération devient donc obligatoirement idempotente, ou bien on accepte des doubles facturations. Ce n’est pas une optimisation, c’est un prérequis, et cela change la signature de chaque opération.
La disponibilité se multiplie. Une chaîne de cinq appels synchrones, chacun à 99,9 % de disponibilité, donne 99,5 % au bout de la chaîne — soit 3 h 36 par mois d’indisponibilité au lieu de 43 minutes. C’est le calcul qui condamne les chaînes d’appels synchrones, et il n’a rien d’un cas pathologique : on l’atteint avec une passerelle, deux services métier et un service d’authentification.
Le déploiement cesse d’être atomique. On ne peut plus renommer un champ : il faut l’ajouter, migrer les producteurs, migrer les consommateurs, puis retirer l’ancien — quatre déploiements ordonnés au lieu d’un commit. Tout changement de contrat devient un projet en deux temps.
Les trois seules raisons légitimes
L’échelle indépendante. Une partie du système a besoin de quarante machines et une autre de deux. Raison réelle, mais bien plus rare qu’annoncée : un monolithe derrière un répartiteur de charge se réplique horizontalement très loin, et la mémoire gaspillée par les modules inactifs coûte moins cher que l’exploitation de douze services.
La cadence de déploiement indépendante. Une équipe doit livrer dix fois par jour, une autre une fois par trimestre après revue réglementaire. C’est la raison la plus souvent authentique, et celle qu’on invoque le moins.
L’autonomie d’équipe. Au-delà d’un certain nombre de personnes, la coordination sur une unité de déploiement unique devient le goulet d’étranglement : conflits de fusion, tests bloqués, livraisons qui s’attendent. C’est la loi de Conway prise par le bon bout — on change les frontières du logiciel pour refléter celles des équipes.
Warning
Et les raisons qui n’en sont pas, énoncées franchement.
« C’est l’état de l’art. » Un argument d’autorité, pas une analyse.
« Pour découpler le code. » Un module fait cela, mieux et pour beaucoup moins cher. Le réseau n’est pas un outil de découplage : c’est un outil de découplage du déploiement.
« Pour utiliser plusieurs langages. » Bénéfice quasi nul, coût réel : deux chaînes de construction, deux écosystèmes de bibliothèques, deux profils de recrutement. Se justifie sur un besoin très spécifique, jamais par principe.
« Parce que notre monolithe est illisible. » Le découpage n’ajoute pas la discipline qui manquait. Modularisez d’abord ; si vous n’y arrivez pas dans un seul processus, vous n’y arriverez pas à travers un réseau.
« Pour recruter. » Cela existe, ce n’est pas une décision d’architecture.
À partir de quelle taille
Le facteur déterminant n’est ni le nombre de lignes, ni le trafic : c’est le nombre d’équipes.
| Équipes | Verdict |
|---|---|
| 1 – 3 | Monolithe modulaire, sans discussion. Distribuer ici n’apporte rien |
| 4 – 8 | Monolithe modulaire, plus un ou deux services extraits sur un besoin nommé |
| Plus de 8 | La distribution répond à un problème d’organisation réel |
Le nombre de lignes n’entre pas dans le tableau, et c’est volontaire : un monolithe modulaire de plusieurs millions de lignes fonctionne parfaitement s’il est correctement cloisonné, tandis que trois services sur deux développeurs sont une punition quotidienne.
Les quatre prérequis
Avant d’extraire le premier service, ces quatre éléments doivent déjà exister. S’il en manque un seul, n’extrayez rien : le service coûtera plus qu’il ne rapporte.
- Un pipeline CI/CD entièrement automatisé, capable de livrer un service sans intervention manuelle.
- De l’observabilité distribuée : traces corrélées, journaux agrégés, métriques par service. Sans elle, le premier incident inter-services est inexplicable.
- Une politique de contrat écrite : versionnement, compatibilité ascendante, tests de contrat.
- Quelqu’un dont c’est le travail d’exploiter tout cela.
Il faut compter plusieurs mois de travail de plateforme avant que le premier service soit rentable. C’est ce délai, jamais budgété, qui fait échouer la plupart des migrations.
Le chemin : extraire, jamais découper d’emblée
L’ordre suivant est celui qui échoue le moins souvent, et il commence loin du réseau.
- Identifier les contextes — travail de modélisation, pas de découpage. Voir Contextes délimités.
- Modulariser dans le monolithe : un module par contexte, une façade publique, frontière vérifiée en CI.
- Séparer les schémas de base et supprimer les jointures inter-modules. C’est l’étape la plus longue et la plus utile — elle donne à elle seule l’essentiel du bénéfice.
- Faire passer tous les appels inter-modules par les façades, avec des signatures de type distant.
- Nommer le besoin : échelle, cadence ou autonomie. Sans besoin nommé, on s’arrête ici — et c’est un très bon endroit pour s’arrêter.
- Extraire un seul module, celui qui porte ce besoin. La façade devient un client HTTP ; le code appelant ne change pas.
- Mesurer, puis s’arrêter si le besoin est satisfait.
L’étape 6 est presque gratuite si les étapes 2 à 4 ont été faites, et c’est tout l’intérêt de l’ordre : le travail de modularisation n’est pas une préparation à la distribution, c’est le gros du bénéfice, et la distribution n’en est qu’un prolongement optionnel.
Note
Le mouvement inverse — reconsolider — est légitime et scandaleusement sous-utilisé. Fusionner deux services qui changent toujours ensemble, ou ramener dans le monolithe un service dont le besoin a disparu, est une amélioration, pas un aveu. L’équipe de Prime Video l’a documenté publiquement en 2023 sur un service de surveillance audio/vidéo, avec 90 % de coût en moins après retour à une architecture monolithique.
Ce que l’extraction change dans le code
Le code appelant ne bouge pas — c’est le retour sur investissement des étapes 2 à 4. Mais notez ce qui apparaît dans la façade : un délai d’attente, une clé d’idempotence, une politique de reprise, une erreur technique qui n’existait pas. Ces quatre lignes sont le prix du réseau, rendu visible.
Comment les services communiquent
Sujet qui mériterait sa propre page ; l’essentiel tient en une règle.
| Synchrone — REST, gRPC | Asynchrone — messages, événements | |
|---|---|---|
| Convient à | Une question dont on a besoin de la réponse maintenant | La propagation d’un fait déjà survenu |
| Couple | La disponibilité : si B tombe, A tombe | Rien, au prix d’un délai |
| Impose | Des délais d’attente, des reprises, des disjoncteurs | La cohérence différée, l’idempotence, l’ordre non garanti |
| Débogage | Simple : une trace | Difficile : plus de fil d’exécution unique |
La règle : synchrone pour interroger, asynchrone pour notifier. Un service qui appelle synchroniquement pour annoncer quelque chose a choisi le mauvais style, et il vient d’attacher sa disponibilité à celle de son voisin sans nécessité.
Côté publication, le problème de la double écriture — enregistrer en base et publier sur le bus, sans atomicité — et sa solution, le patron outbox, sont traités dans Événements de domaine.
Les pièges
Le monolithe distribué. N services, un modèle ou une base partagés. Tout le coût, aucun bénéfice. C’est le résultat par défaut d’un découpage fait avant la modularisation.
Le service par entité. ServiceClient, ServiceAdresse, ServiceContact :
trois services pour ce qui est un seul agrégat. Chaque cas d’usage devient une
chorégraphie réseau, et une transaction devient impossible. Un service correspond à
un contexte, pas à une table.
Les transactions distribuées. Dès qu’on cherche un commit en deux phases, le diagnostic est posé : la frontière est au mauvais endroit. La réponse n’est pas un outil, c’est de déplacer la frontière — ou d’accepter la cohérence différée.
Le service partagé que tout le monde appelle. Extraire « le service Client » que les huit autres interrogent crée un point de défaillance unique et un goulet d’étranglement organisationnel : plus personne ne livre sans son accord.
L’absence de politique de contrat. Sans versionnement ni tests de contrat, chaque déploiement est un pari. C’est le premier prérequis à sauter, et le premier à manquer.
Pas de retour en arrière prévu. Une extraction doit pouvoir être annulée. Si la façade a disparu et que quinze appelants tapent directement sur l’API HTTP, c’est devenu impossible.
Comparaison
| Monolithe | Modulaire | Microservices | |
|---|---|---|---|
| Coût d’entrée | Nul | Faible | Élevé : plateforme, observabilité, contrats |
| Déplacer une frontière | Facile | Facile | Très coûteux |
| Tester de bout en bout | Trivial | Trivial | Difficile |
| Déboguer | Une pile d’appels | Une pile d’appels | Traces distribuées |
| Transactions | ACID partout | ACID partout | Compensation |
| Échelle sélective | Non | Non | Oui |
| Cadence indépendante | Non | Non | Oui |
| Autonomie d’équipe | Faible | Moyenne | Élevée |
| Nombre d’équipes visé | 1 – 2 | 1 – 8 | Plus de 8 |
| Défaillance partielle | Impossible | Impossible | Permanente |
Le tableau se lit ligne par ligne, et non colonne par colonne : les trois avantages en gras de la dernière colonne sont les seuls que la distribution apporte. Si aucun des trois n’est un besoin nommé et daté, la colonne du milieu gagne.
Références
- Sam Newman, Building Microservices, 2ᵉ édition, 2021 — la référence
- Sam Newman, Monolith to Microservices, 2019 — spécifiquement sur l’extraction progressive
- https://martinfowler.com/bliki/MonolithFirst.html
- https://martinfowler.com/bliki/MicroservicePremium.html — le prix d’entrée
- https://martinfowler.com/articles/microservice-trade-offs.html
- Matthew Skelton & Manuel Pais, Team Topologies, 2019 — le versant organisationnel
- https://shopify.engineering/shopify-monolith-modular — un monolithe modulaire à très grande échelle
- https://www.primevideotech.com/video-streaming/scaling-up-the-prime-video-audio-video-monitoring-service-and-reducing-costs-by-90 — un cas de reconsolidation documenté