Be Software Architecture Wise
Be Wise Apps

Read in English · Leer en español · Auf Deutsch lesen

Section 5.3 sur 5.8 · Intermédiaire · 26 min de lecture

Le monolithe modulaire

Leçon vidéo

5 min · narré en anglais, avec sous-titres
Écouter seulement (audio)
Lire la transcription

Un monolithe modulaire vous apporte une grande partie des avantages que l’on recherche dans les microservices, sans faire tourner un système distribué. Dans cette leçon, vous allez voir comment il est construit, comment ses frontières sont préservées, et où il échoue le plus souvent.

Pensez à un grand magasin. C’est un seul bâtiment, avec une seule porte d’entrée, des horaires d’ouverture communs et une seule équipe de sécurité. À l’intérieur, il est divisé en rayons : alimentation, vêtements, électronique. Chacun a son propre personnel, sa propre réserve et sa propre caisse. Si le rayon vêtements a besoin de quelque chose du rayon alimentation, il le demande au comptoir de l’alimentation au lieu d’aller se servir dans sa réserve. Un monolithe modulaire est un logiciel construit comme ce magasin.

Un monolithe modulaire est une unité de déploiement unique dont le niveau supérieur est divisé par domaine métier en modules : commandes, paiements, livraison, cartes. Il combine le partitionnement par domaine des microservices avec la simplicité d’un monolithe : un seul build, un seul déploiement, des appels en mémoire et des transactions locales.

Voici l’intérieur de l’un d’eux. La frontière extérieure est une unité de déploiement, l’application de livraison de repas, et à l’intérieur se trouvent des modules comme Commandes et Paiements. Chaque module a une petite API publique : les fonctions, les données et les événements que ses responsables s’engagent à garder stables. Commandes n’utilise Paiements qu’en appelant cette API. Tout le reste est interne : le modèle de domaine, les règles et les repositories. Il est interdit d’accéder aux éléments internes d’un autre module. Et chaque module possède son propre schéma de base de données, sans jointure entre schémas.

Dans un monolithe, n’importe quelle classe peut techniquement appeler n’importe quelle autre, donc les frontières ne tiennent que si quelque chose les fait respecter. Sinon, il se dégrade en grosse boule de boue en un an ou deux. Commencez par le masquage le plus fort que votre langage propose. Ajoutez des tests d’architecture dans le build, comme ArchUnit pour Java ou import-linter pour Python, qui échouent quand un module importe les éléments internes d’un autre. Les frameworks peuvent aider. Spring Modulith vérifie les règles des modules pour les applications Spring, et Packwerk, de Shopify, vérifie les dépendances entre paquets en Ruby. Et tracez des frontières de données : un schéma par module et, idéalement, un utilisateur de base de données qui ne peut atteindre que ce schéma. La revue de code n’empêchera pas une jointure commode ; les permissions de la base de données, si.

La propriété des données est le point où les monolithes modulaires échouent le plus souvent. Partager un serveur de base de données ne pose pas de problème ; partager des tables, si. Chaque table a exactement un module propriétaire, et seul le propriétaire y écrit. Les autres modules obtiennent les données par l’API du propriétaire, ou à partir des événements qu’il publie. Un rapport qui joignait les tables de trois modules a désormais besoin d’appels d’API ou de son propre modèle de lecture. C’est un coût réel, mais c’est le coût qu’entraînerait un passage ultérieur aux services, payé tôt et à bas prix. Les modules communiquent de deux façons, toutes deux en mémoire. Faites un appel quand vous avez besoin d’une réponse, par exemple pour savoir si un paiement a été autorisé. Publiez un événement quand vous n’en avez pas besoin, et quand plusieurs modules réagissent au même fait.

Voici un exemple commenté. Une start-up de livraison de repas comptant quatre équipes, environ vingt-cinq ingénieurs, veut une propriété indépendante sans faire tourner de microservices. L’architecte propose huit modules dans une seule application Python déployable, avec un schéma par module. Commandes est au centre. Il appelle trois modules par leurs API : Paiements pour payer, Cartes pour tarifer les articles, et Clients pour l’adresse. Restaurants met à jour les cartes par l’API de Cartes. Quand une commande est passée, Commandes publie un événement OrderPlaced, et Livraison l’écoute. Reporting et Notifications aussi. Commandes n’a pas besoin de savoir qui écoute. Les dépendances vont dans un seul sens, donc les cycles sont impossibles, et import-linter le vérifie à chaque build. Le tout est livré sous la forme d’une seule image de conteneur, exécutée en six réplicas, plus un worker pour les événements et les tâches d’arrière-plan.

Shopify est un cas réel bien connu. Plutôt que de découper en microservices son énorme monolithe Ruby on Rails, l’entreprise a réorganisé le code à partir de 2017 en composants nommés d’après les domaines métier, comme les commandes, l’expédition et la facturation. Ce n’a pas été un simple succès : des années plus tard, presque aucun composant n’avait de frontière complète et solide. Un monolithe modulaire exige un investissement soutenu, et introduire des frontières après coup est beaucoup plus difficile que de les préserver dès le départ. Le bénéfice, ce sont les options. Si un module a plus tard besoin de passer à l’échelle ou d’être mis en production seul, l’extraction est bien plus facile quand il a déjà une API explicite, possède ses données et ne communique avec les autres qu’à travers des API et des événements.

La section complète présente plus en détail les bonnes API de module, les subtilités des événements et l’histoire de Shopify. Ensuite, dans la section cinq point quatre, nous passerons à une forme très différente : les tubes et filtres. À très vite.

Ce qu’il faut savoir

Définition : un seul déploiement, plusieurs domaines

Un monolithe modulaire est une application construite et déployée comme une seule unité, mais dont le code est divisé, au niveau supérieur, en modules qui suivent les frontières métier : commandes, paiements, livraison, cartes. Chaque module possède une petite API publique délibérée que les autres modules peuvent utiliser, une partie interne plus grande à laquelle personne d’extérieur ne doit toucher, et ses propres données. Il combine le partitionnement par domaine des microservices avec l’unité de déploiement unique d’un monolithe classique.

L’idée n’est pas nouvelle. Les modules aux éléments internes cachés remontent au masquage de l’information de David Parnas (1972), et de grands monolithes bien structurés ont toujours existé. Ce qui est plus récent, c’est le nom et l’attention qu’on lui porte. Après une décennie au cours de laquelle de nombreuses organisations ont découpé leurs systèmes en microservices et découvert le coût de la distribution, des praticiens comme Simon Brown et Sam Newman ont soutenu que le monolithe modulaire est une option sous-estimée, et que beaucoup d’équipes seraient mieux servies par un monolithe modulaire que par un ensemble de services. Richards et Ford lui consacrent un chapitre entier dans la deuxième édition de Fundamentals of Software Architecture (2025).

La différence avec le style en couches tient au découpage du niveau supérieur. Une application en couches est divisée en présentation, métier et persistance, chaque domaine étant réparti sur les trois. Un monolithe modulaire est divisé en domaines, et chaque module peut être organisé en couches à l’intérieur. La différence avec les microservices tient au déploiement : les modules partagent un seul processus, une seule mise en production et généralement un seul serveur de base de données, et ils s’appellent les uns les autres en mémoire.

Anatomie d’un monolithe modulaire

Deux modules dans une unité de déploiement : chacun a une petite API publique, une partie interne plus grande et son propre schéma de base de données ; Commandes peut appeler l’API de Paiements, mais pas toucher à ses éléments internes ni à ses tablesUne unité de déploiement : l’application de livraisonModule CommandesAPI publiquefonctions, DTO, événementsInternemodèle métier, règles,repositoriesschéma commandesModule PaiementsAPI publiquefonctions, DTO, événementsInternemodèle métier, règles,repositoriesschéma paiementsappel APIinterditjointures interdites
Chaque module expose une petite API publique et possède son propre schéma ; accéder aux éléments internes ou aux tables d’un autre module est interdit.

API des modules et code interne

La règle la plus importante est qu’un module ne s’utilise qu’à travers son API. L’API est l’ensemble des fonctions, des objets de transfert de données (DTO) et des événements que les responsables du module s’engagent à garder stables ; tout le reste, y compris le modèle de domaine, les repositories et les tables de la base de données, est interne et peut changer à tout moment. En pratique, l’API est petite : un module de paiements peut exposer authorise_payment, refund, un type PaymentResult et un événement PaymentCaptured, tout en cachant des dizaines de classes pour les prestataires, les nouvelles tentatives, les registres comptables et le rapprochement.

Les bonnes API de module ont plusieurs traits communs. Elles parlent la langue du domaine (authorise_payment, et non insert_payment_row). Elles transmettent des données simples, comme des identifiants et de petits enregistrements immuables, plutôt que des objets internes, pour que les appelants ne puissent pas passer par un objet renvoyé pour atteindre les éléments internes du module. Elles sont assez grossières pour qu’un cas d’utilisation typique n’ait besoin que d’un ou deux appels, et non de vingt. Et elles sont conçues comme si un réseau devait un jour s’interposer entre l’appelant et l’appelé : pas d’objets mutables partagés, pas de dépendance à la transaction de base de données de l’appelant, des erreurs explicites. C’est ce dernier trait qui préserve la possibilité d’extraire le module plus tard.

À l’intérieur de la frontière, tout ce que l’équipe du module juge utile est permis : une structure en couches, une structure hexagonale, un modèle de domaine riche ou de simples scripts de transaction. Des modules différents peuvent faire des choix différents, car rien à l’extérieur n’en dépend.

# delivery_platform/payments/__init__.py  (the module's public API)
from .api import authorise_payment, refund, PaymentResult
from .events import PaymentCaptured

__all__ = ["authorise_payment", "refund", "PaymentResult", "PaymentCaptured"]

# delivery_platform/ordering/internal/checkout.py
from delivery_platform import payments             # allowed: public API
# from delivery_platform.payments.internal import ledger   # forbidden

def place_order(order, card_token):
    result = payments.authorise_payment(order.id, order.total, card_token)
    if not result.ok:
        raise PaymentDeclined(result.reason)
    order.confirm(result.payment_id)
    events.publish(OrderPlaced(order.id, order.restaurant_id))
Un paquet Python comme module : le paquet payments exporte une petite API, et ordering n’importe que cette API.

Faire respecter les frontières

Dans un monolithe, chaque classe peut techniquement appeler n’importe quelle autre, donc les frontières ne tiennent que si quelque chose les fait respecter. Sans contrôle, un raccourci pris sous la pression d’une échéance vers les éléments internes d’un autre module passe inaperçu en revue, et un monolithe modulaire se dégrade en grosse boule de boue (big ball of mud) en un an ou deux. Il existe plusieurs lignes de défense, à utiliser de préférence ensemble.

Visibilité du langage. Utilisez le masquage le plus fort que le langage propose : classes à visibilité package en Java, internal en Kotlin et en C#, les exports du Java Platform Module System, des modules de build séparés (sous-projets Maven ou Gradle, projets .NET) dont les dépendances sont déclarées explicitement. Python et Ruby n’offrent que des conventions (un trait de soulignement initial, un sous-paquet internal), donc ce sont les outils qui doivent faire le travail.

Tests d’architecture. Les tests d’architecture vérifient les dépendances dans le build. ArchUnit (Java) peut affirmer qu’aucune classe extérieure à un module ne touche à ses paquets internes et que les dépendances entre modules sont exemptes de cycles. import-linter (Python) propose un contrat forbidden, qui peut empêcher les autres modules d’importer le paquet interne d’un module tout en les laissant utiliser son API publique, un contrat layers dont les couches peuvent contenir plusieurs éléments frères mutuellement indépendants séparés par |, ce qui rend vérifiable un ordre de dépendance à sens unique entre modules, et un contrat independence pour les paquets qui ne doivent pas du tout dépendre les uns des autres, même indirectement.

Support des frameworks. Spring Modulith, pour les applications Spring Boot, traite chaque sous-paquet direct du paquet principal de l’application comme un module, considère le paquet de base du module comme son API et ses sous-paquets comme internes, et propose une vérification en une ligne (ApplicationModules.of(Application.class).verify()) qui échoue lorsqu’un module fait référence aux éléments internes d’un autre module, lorsque les dépendances entre modules forment un cycle ou, quand un module liste ses dépendances autorisées, lorsqu’il en utilise une qui ne figure pas dans la liste. Il peut aussi générer des diagrammes de composants de style C4 pour les modules. Dans le monde Ruby, Packwerk, l’outil open source de Shopify, analyse statiquement les références entre paquets, chacun déclaré par un fichier package.yml, et signale les dépendances non déclarées.

Frontières des données. Donnez à chaque module son propre schéma de base de données (ou au moins ses propres tables clairement préfixées) et, idéalement, son propre utilisateur de base de données, qui ne peut accéder qu’à ce schéma. La revue de code seule n’empêchera pas un JOIN commode entre modules ; les permissions de la base de données, si.

[importlinter:contract:payments-internals]
name = Only payments may use payments internals
type = forbidden
source_modules =
    delivery_platform.ordering
    delivery_platform.delivery
    delivery_platform.reporting
forbidden_modules =
    delivery_platform.payments.internal
# going through the public API (which itself uses internals) is fine
allow_indirect_imports = True

[importlinter:contract:module-order]
name = Module dependencies point one way
type = layers
layers =
    delivery_platform.reporting | delivery_platform.notifications
    delivery_platform.delivery
    delivery_platform.ordering | delivery_platform.restaurants
    delivery_platform.payments | delivery_platform.menus | delivery_platform.customers
Contrats import-linter (un par module en pratique) : les autres modules ne peuvent pas importer les éléments internes de payments, et les dépendances entre modules doivent suivre un seul sens, les modules d’une même ligne étant indépendants les uns des autres.

La propriété des données au sein d’un monolithe

La propriété des données est le point où les monolithes modulaires échouent le plus souvent. Partager un serveur de base de données ne pose pas de problème ; partager des tables, si. Si le module de livraison lit directement la table orders, l’équipe des commandes ne peut plus modifier cette table sans se coordonner avec la livraison, et les deux modules sont aussi étroitement couplés que s’ils n’en formaient qu’un. La règle est simple à énoncer : chaque table a exactement un module propriétaire, seul le propriétaire y écrit, et les autres modules obtiennent les données par l’API du propriétaire ou à partir des événements qu’il publie.

La règle a des conséquences que les équipes doivent accepter délibérément. Un rapport qui était autrefois une seule requête SQL joignant commandes, paiements et livreurs nécessite désormais soit des appels à trois API de modules, soit un modèle de lecture : un module de reporting qui s’abonne aux événements des autres et tient sa propre copie dénormalisée, optimisée pour les requêtes. Les clés étrangères entre schémas de modules sont généralement supprimées, car elles rendraient le schéma d’un module dépendant de celui d’un autre ; l’intégrité entre modules est alors assurée par l’application. Ces coûts sont réels, mais ce sont les mêmes que ceux qu’imposerait un passage ultérieur aux services, payés tôt et dans un cadre moins coûteux.

Il reste un avantage que les systèmes distribués perdent : des modules dans un même processus peuvent, quand c’est vraiment nécessaire, partager une seule transaction locale de base de données. Utilisez-le avec parcimonie. Chaque utilisation couple opérationnellement les données des modules et devra être défaite si un module est un jour extrait.

Communication entre modules : appels ou événements

Les modules communiquent de deux façons, toutes deux en mémoire. Un appel interne au processus à l’API d’un autre module est simple, rapide et immédiat : Commandes appelle payments.authorise_payment et obtient le résultat aussitôt, et une exception se propage normalement. Il crée une dépendance directe, de l’appelant vers l’appelé, et l’appelant ne peut pas terminer avant l’appelé. Utilisez des appels quand l’appelant a besoin d’une réponse pour continuer, par exemple pour savoir si le paiement a été autorisé.

Un événement interne au processus inverse la dépendance. Commandes publie OrderPlaced sans savoir ni se soucier de qui écoute ; Livraison, Notifications et Reporting s’y abonnent. Ajouter une nouvelle réaction revient à ajouter un écouteur, sans toucher à Commandes. Utilisez des événements quand l’émetteur n’a pas besoin de réponse, et quand plusieurs modules réagissent au même fait.

Les événements introduisent des subtilités qui échappent souvent aux équipes qui les découvrent. Si un écouteur s’exécute dans la transaction de l’émetteur, une défaillance de l’écouteur peut annuler la commande ; s’il s’exécute après le commit, la commande est enregistrée mais le travail de l’écouteur peut être perdu si le processus plante entre les deux. Spring Modulith traite ce problème avec un registre de publication d’événements qui enregistre chaque paire événement-écouteur dans la même transaction que la modification métier et ne la marque comme terminée que lorsque l’écouteur a réussi, afin que les paires inachevées puissent faire l’objet d’une nouvelle tentative ; un équivalent fait main est l’outbox transactionnelle décrite à la section 6.7. Les écouteurs doivent aussi être idempotents, pour qu’un événement rejoué ne cause aucun dommage. Aucun des deux styles n’est meilleur ; la plupart des monolithes modulaires utilisent des appels pour les requêtes et les commandes qui exigent une réponse, et des événements pour signaler des choses qui se sont produites.

Le monolithe modulaire de Shopify

L’application principale de Shopify est l’une des plus grandes bases de code Ruby on Rails existantes, sur laquelle plus d’un millier de développeurs ont travaillé pendant plus de dix ans. Dans Deconstructing the Monolith (Shopify Engineering, février 2019), Kirsten Westeinde a expliqué pourquoi l’entreprise ne l’a pas découpée en microservices et l’a plutôt transformée, à partir de 2017, en monolithe modulaire. Les difficultés étaient connues : le code de domaines métier sans rapport était enchevêtré, les modifications avaient des effets inattendus ailleurs, et l’intégration des nouveaux arrivants était lente. Les microservices ont été envisagés et jugés porteurs de leurs propres coûts (pipelines et infrastructures séparés, latence et défaillances du réseau, refactorisation plus difficile entre services) ; l’équipe a donc choisi d’obtenir la modularité sans multiplier les unités de déploiement.

La première étape a été de réorganiser le code, de la structure Rails standard (modèles, vues et contrôleurs) en composants nommés d’après les domaines métier, comme les commandes, l’expédition, les stocks et la facturation, en une seule grande modification automatisée. La suivante a été l’isolation : chaque composant devait avoir une API publique et la propriété exclusive de ses données, et un outil interne, Wedge, suivait les progrès de chaque composant en enregistrant les appels qui traversaient les frontières pendant l’exécution de la suite de tests. Un point d’étape de septembre 2020 faisait état de 37 composants dans un monolithe central d’environ 2,8 millions de lignes de Ruby, et le même mois Shopify a publié en open source Packwerk, un outil d’analyse statique permettant de déclarer et de vérifier les dépendances entre paquets.

Cette histoire est instructive parce que ce n’est pas un simple succès. Le point d’étape de 2020 indiquait que les idées d’origine tenaient toujours mais que presque tous les détails avaient changé, et que presque aucun composant n’avait encore de frontière complète et solide. Un bilan rétrospectif de Packwerk publié en 2024 rapportait que ses contrôles de confidentialité avaient été supprimés, que certains paquets organisés par nom plutôt que par comportement à l’exécution induisaient les développeurs en erreur, et qu’isoler complètement ne serait-ce qu’un paquet de base avait pris de nombreux mois ; Packwerk était devenu un garde-fou sur les frontières clés plutôt que le cœur de la stratégie. La leçon pour les architectes est qu’un monolithe modulaire est une direction à suivre qui exige un investissement soutenu, et qu’introduire après coup des frontières dans une grande base de code existante est beaucoup plus difficile que de les préserver dès le départ.

Exemple commenté

Exemple commenté : la plateforme de livraison de repas en monolithe modulaire

Une start-up de livraison de repas comptant quatre équipes (environ 25 ingénieurs) veut une propriété indépendante sans le coût d’exploitation des microservices. L’architecte propose huit modules dans une seule application Python déployable, avec un seul serveur PostgreSQL et un schéma par module.

ClientsComptes, adresses, préférences. API : get_customer, get_delivery_address. Équipe A.
Restaurants ; Cartes (deux modules)Restaurants gère les comptes des restaurants et leurs horaires d’ouverture, et met à jour les cartes par l’API de Cartes ; Cartes fournit les prix et la disponibilité à tous les autres (price_items, is_open). Équipe B.
CommandesPanier, étape de paiement, cycle de vie de la commande. Appelle Clients, Cartes et Paiements ; publie OrderPlaced et OrderCancelled. Équipe C.
PaiementsAutorisation, capture et remboursements via un prestataire externe. API : authorise_payment, refund ; publie PaymentCaptured. Équipe C.
LivraisonAffectation des livreurs et suivi. Écoute OrderPlaced ; publie CourierAssigned et Delivered. Équipe D.
Notifications et ReportingPurs écouteurs : envoient des messages et tiennent à jour des modèles de lecture. Ils n’appellent aucun autre module. Équipe A.
Règle de dépendancePaiements, Cartes et Clients ne dépendent de personne ; Commandes et Restaurants les appellent ; Livraison réagit aux événements de Commandes ; Notifications et Reporting se contentent d’écouter. Vérifié par import-linter dans le build.
DéploiementUne seule image de conteneur, exécutée en six réplicas derrière un équilibreur de charge, plus un processus worker (même code) qui gère les écouteurs d’événements et les tâches d’arrière-plan.

Chaque équipe possède des modules entiers, donc la plupart des modifications ne touchent qu’un module et qu’une équipe.

La règle de dépendance rend les cycles impossibles : si Paiements a un jour besoin de connaître les commandes, il reçoit un identifiant de commande, et non un import de Commandes.

Carte des modules de la plateforme de livraison de repas

Carte des modules de la plateforme de livraison de repas : huit modules dans une seule application déployable ; Commandes appelle les API de Paiements, Cartes et Clients, Restaurants appelle Cartes, Commandes publie des événements OrderPlaced consommés par Livraison, Notifications et Reporting, et Livraison publie CourierAssigned vers NotificationsLivraison de repas : une seule application déployableClientsCartesRestaurantsCommandesPaiementsLivraisonReportingNotificationspayertariferadressemodifierOrderPlacedOrderPlacedOrderPlacedCourierAssignedappel interne à l’API du moduleévénement interne
Les flèches pleines sont des appels à l’API d’un autre module ; les flèches en pointillés sont des événements internes au processus. Commandes appelle trois modules, et ses écouteurs ne dépendent que des événements qu’il publie.

Caractéristiques du monolithe modulaire

CaractéristiqueÉvaluationJustification
SimplicitéMoyen à élevéUn seul déploiement et des appels internes au processus, mais trouver de bonnes frontières de modules demande de la réflexion.
Coût globalÉlevé (peu coûteux)Infrastructure et exploitation d’un monolithe ; un peu d’effort supplémentaire pour l’outillage des frontières.
Modifiabilité pour les changements métierÉlevéLe partitionnement par domaine garde la plupart des modifications dans un seul module, détenu par une seule équipe.
TestabilitéMoyen à élevéLes modules peuvent être testés par leurs API en remplaçant leurs voisins ; les tests du système complet s’exécutent dans un seul processus.
DéployabilitéBas à moyenToujours une seule unité de déploiement : chaque mise en production livre tous les modules, même si un rayon d’impact plus petit par modification aide.
Scalabilité et élasticitéBas à moyenLes réplicas font passer à l’échelle toute l’application ; un module ne peut pas passer à l’échelle seul, même si des processus worker peuvent isoler la charge d’arrière-plan.
Tolérance aux pannesBasUn seul processus : un plantage ou une fuite de mémoire dans n’importe quel module les affecte tous.
PerformancesÉlevéLes appels entre modules se font en mémoire, sans sérialisation ni latence réseau.
ÉvolutivitéÉlevéDes API explicites et la propriété des données rendent les modules faciles à restructurer ou à extraire plus tard.

Quand l’utiliser, quand l’éviter, et les anti-patterns

Un monolithe modulaire est un bon choix par défaut pour les nouveaux produits dont le domaine est raisonnablement clair, pour les organisations comptant jusqu’à quelques équipes, pour les systèmes qui ont besoin d’une forte cohérence entre domaines métier, et pour les équipes dont la capacité d’exploitation est limitée. C’est aussi la cible naturelle pour moderniser un monolithe enchevêtré : y introduire des frontières est généralement moins coûteux et moins risqué que de le découper en services d’un seul coup. Il convient moins lorsque des parties du système ont besoin d’attributs de qualité réellement différents (un flux de positions de livreurs qui doit absorber d’énormes volumes d’écriture et un module de back-office à charge modeste), lorsque des dizaines d’équipes doivent mettre en production indépendamment plusieurs fois par jour, ou lorsque des parties doivent être écrites dans des langages différents ou s’exécuter dans des lieux différents pour des raisons de résidence des données. Ces besoins appellent des quanta d’architecture distincts.

Anti-patterns courants : les modules de nom seulement, où les dossiers suivent les domaines mais où n’importe quoi importe n’importe quoi ; la base de données partagée en libre-service, où les modules gardent leur propre code mais font des jointures sur les tables des autres ; la frontière bavarde, une API si fine qu’un cas d’utilisation nécessite des dizaines d’appels, ce qui sera pénible si le module est un jour extrait ; le noyau partagé qui grossit, un module common qui absorbe peu à peu de la logique métier et y couple chaque module ; et les modules par entité, un découpage par entité de données (un module Client, un module Commande, un module Produit dont chaque cas d’utilisation a besoin en même temps) au lieu d’un découpage par capacité métier, ce qui produit des modules incapables de faire quoi que ce soit seuls.

Préserver la possibilité d’extraire des services

Le monolithe modulaire est parfois présenté comme un tremplin vers les microservices, parfois comme une destination. Il peut être l’un ou l’autre, et l’essentiel est que la décision peut être reportée jusqu’au dernier moment responsable. Si un module a plus tard besoin de passer à l’échelle séparément ou d’être mis en production indépendamment, l’extraction est bien plus facile quand le module a déjà une API explicite, possède ses données et ne dépend des autres qu’à travers des API et des événements. Le travail devient en grande partie mécanique : placer une interface réseau devant l’API, déplacer le schéma dans sa propre base de données, remplacer les événements internes au processus par un courtier de messages (message broker), et remplacer toute transaction partagée par une saga explicite ou une compensation.

Sam Newman défend la même thèse dans Monolith to Microservices (2019) : un monolithe bien modularisé est le meilleur point de départ pour une décomposition, car la partie difficile, trouver les frontières et les faire respecter, est déjà faite. La section 5.8 examine les signes indiquant qu’il est temps d’extraire, et la section 11.6 couvre les techniques de migration incrémentale comme le strangler fig.

Vérifiez votre compréhension

Qu’est-ce qui distingue un monolithe modulaire d’un monolithe en couches ?

Son niveau supérieur est partitionné par domaine métier plutôt que par couche technique ; chaque module a une API explicite, des éléments internes privés et ses propres données, et peut être organisé en couches à l’intérieur.

Pourquoi les API de module doivent-elles transmettre des identifiants et des enregistrements simples plutôt que des objets internes du domaine ?

Les objets internes renvoyés permettent aux appelants d’atteindre les éléments internes du module et les couplent à son modèle ; des données simples laissent les éléments internes libres de changer et l’API prête à traverser un réseau plus tard.

Un rapport a besoin de données des schémas commandes et paiements. Quelles sont deux options qui respectent la propriété des données ?

Appeler les API des deux modules et combiner les résultats, ou construire un modèle de lecture dans un module de reporting qui écoute les événements des deux modules et tient sa propre copie dénormalisée.

Quel risque introduit un écouteur d’événement interne au processus qui s’exécute après le commit de la transaction de l’émetteur ?

Si le processus plante après le commit mais avant l’exécution de l’écouteur, le travail de l’écouteur est perdu, à moins que l’événement ne soit enregistré durablement, par exemple dans un registre de publication d’événements ou une outbox, puis rejoué.

Suivant · 5.4Tubes et filtresDes données qui circulent à travers une chaîne d’étapes indépendantes : producteurs, transformateurs, testeurs et consommateurs reliés par des tubes. Des tubes Unix à l’ETL, aux compilateurs et au traitement de flux, avec le débit, la contre-pression et la gestion des erreurs.Continuer →

Sources et lectures complémentaires

Une question sur cette section ? ArchitectBot, l’assistant d’étude IA du cours, répond d’abord à partir du cours.

Demander : « Nous avons un monolithe Django avec des apps pour les commandes, les paiements et la livraison, mais elles importent toutes les modèles les unes des autres. Comment en faire un vrai monolithe modulaire, étape par étape ? »