Passer au contenu principal

L’architecture modulith, ce n’est pas réservé aux développeurs Spring.
Ce n’est pas une feature de framework, c’est un vrai choix d’architecture.

Spring Modulith est connu : ses talks font salle comble à chaque Devoxx, même en Belgique. Mais l’idée dépasse Java. NestJS, par exemple, propose les Modules

La programmation modulaire existe depuis les années 70. Le Domain-Driven Design, lui, parle de bounded contexts depuis plus de dix ans. Aujourd’hui, ces concepts prennent une autre dimension. Le modulith devient une troisième voie, entre le microservice et le monolithe. Proche du monolithe, il s’adresse pourtant à tous les développeurs, tous les langages.

Dans cet article, on va décrire ce qu’est une architecture modulith. Ce qu’elle n’est pas. Comment la comparer au monolithe, aux migroservices et aux microservices. On verra aussi les anti-patterns et les bonnes pratiques à appliquer dès demain.

En 2025, on parle encore de monolithe vs microservices

Et il y a de bonnes raisons d’en parler encore aujourd’hui.

L’expérience développeur est directement impactée par ce choix. J’ai posé la question à Vincent Vauban, qui résume mieux que moi :
« Un seul repo, un build, un point d’entrée : c’est moins de friction, plus de flow. »

C’est aussi une question de compétences. J’ai recruté des devs backend pour un monolithe Spring. Beaucoup venaient du microservice. Ils ne connaissaient pas la différence entre JPA et Hibernate. Leurs services ne voyaient jamais à un SGBD. Ils parlaient seulement en HTTP à d’autres microservices.

On trouve des articles qui parlent d’un “pendule”. Comme si les entreprises basculaient sans cesse entre microservices et monolithes. En réalité, les cas sont rares. On cite Uber, Amazon Prime, Segment. Le bénéfice est clair : réduire les appels HTTP, simplifier les déploiements, baisser les coûts. Par exemple, InVision a aussi communiqué sur un retour au monolithe, mais juste avant de fermer son service.

Les macroservices ou les “migroservices”.

François Teychene parlait déjà de “migroservices” en 2019, lors de BDXio (captation).

Il désignait par ce terme de gros microservices, autonomes, centrés sur un domaine. L’idée était simple : éviter la dérive qui consiste à créer un microservice pour chaque verbe HTTP (et oui, on a tous vu ça en prod).

Il rappelait aussi deux points essentiels : 

  • D’abord, une base de données ne doit pas être partagée entre deux microservices. C’est un anti-pattern. Il utilisait l’image du restaurant : tu ne vas pas te servir en cuisine, tu passes par le serveur, c’est son rôle. 
  • Ensuite, il insistait sur la notion de contexte bien délimité : un service pour les commandes, un autre pour le menu, un pour la cuisine. Mais pas un service qui fait tout le restaurant — sauf si c’est un foodtruck.

Ces principes restent utiles quand on parle d’architecture modulith. On conserve l’idée d’un domaine clair, avec un contrat net. La différence, c’est que le déploiement reste celui d’un monolithe : un seul build, un seul process.

Qu’est-ce qu’une architecture modulaire ?

Un module, dans un modulith, correspond lui aussi à un domaine ou un contexte. Il expose un contrat public — une interface — que les autres modules consomment. Le but est de ne jamais avoir besoin d’ouvrir ce module pour aller chercher directement dans ses entrailles.

Certains frameworks matérialisent cette frontière dans le code. C’est le cas de Spring Modulith, avec ses annotations @ApplicationModule et @ModuleDependency. NestJS adopte la même logique avec son décorateur @Module().

On peut définir un module principal, puis importer ses modules enfants. Exemple en NestJS :

@Module({
  imports: [UsersModule, OrdersModule],
  controllers: [AppController],
  providers: [AppService],
})

export class AppModule {}

Chaque module peut disposer de ses propres tests d’intégration et de sa documentation. Mais au runtime, tout tourne dans le même processus. Le module n’est jamais packagé ni déployé séparément. C’est ce qui distingue le modulith : une modularité forte, mais toujours dans un déploiement unique.

Et on peut bien sûr garder une architecture hexagonale ou DDD.

src/
 ├─ orders/
 │   ├─ application/
 │   ├─ domain/
 │   └─ infrastructure/
 ├─ inventory/
 │   ├─ application/
 │   ├─ domain/
 │   └─ infrastructure/
 └─ shared/
     ├─ kernel/        # règles techniques réutilisées
     └─ ui/            # design system

Sur une architecture plus simple avec Nest on trouvera cette arborescence :

modules/
 ├─ orders/
 │   ├─ orders.controller.ts
 │   ├─ orders.service.ts
 │   └─ orders.entity.ts
 ├─ inventory/
 │   ├─ inventory.controller.ts
 │   ├─ inventory.service.ts
 │   └─ inventory.entity.ts
 └─ shared/
     └─ logger.ts

Quelle est la différence entre une architecture modulith et un microservice ?

Ce qui m’intéresse avec une architecture modulith, c’est de découpler correctement le code sans payer le coût d’infrastructure des microservices.
Même avec un cloud moderne et une bonne chaîne DevOps, le run finit par prendre énormément de temps. Maintenir dix systèmes n’a rien à voir avec en maintenir un.

Surtout si la scalabilité n’est pas un besoin immédiat sur ce domaine.
Si demain votre service “Commande” doit encaisser plus de charge, vous pourrez l’extraire en microservice et lui dédier quelques VM.
En attendant, c’est acceptable de rester en mode foodtruck : le serveur fait aussi la cuisine. L’important, c’est que la file avance.

40 microservices pour 3 personnes, c’est compliqué.

Un autre sujet, c’est le downscale humain. Les projets vivent par cycles. La hype monte vite, les équipes, moins. On a tendance à créer autant de VM et de microservices qu’il est possible d’en opérer avec les ressources du moment.

Ce choix est grisant quand l’équipe est au complet. Il devient lourd quand le budget baisse.

Bertrand et Michel aiment le build de projet. Ils innovent, ils gagnent des médailles. Maintenir, corriger des bugs, publier de petites features… c’est moins valorisé. Le DSI finit par vouloir réduire le budget ici, pour miser sur le nouveau projet “waouh”. Vos Rockstars suivront les budgets.

Bref, vous ne serez pas toujours dix. Projetez-vous avec deux personnes.
Combien d’effort pour garder tout en ligne ?
Et ne trichez pas avec “le pompier en chef”. Imaginez que deux juniors restent seuls à bord.
Dans ce cas, moins de microservices et des modules forts rendent la situation viable.

L’architecture modulith dénonce les grands anti-patterns du monolithe.

Vous vous dites peut-être : “Mais Damien, un modulith, c’est juste un monolith mieux codé.”

Oui, enfin… ça dépend de vos standards.

Même si votre monolith vous paraît propre, l’architecture modulith va souvent révéler des anti-patterns bien installés.

Et le premier que l’on croise, c’est la Data Access Layer.

En microservices, partager la même base est interdit. Chacun protège ses données. Si tu veux y toucher, tu demandes la permission. Le microservice, c’est un petit chef maniaque : pas de ticket JIRA, pas d’accès.

On est venus péter la DAL - Trois ouvriers avec des outils lourds essaient de la détruire. C'est toi qui mets en place l'architecture modulith


On est venu péter la DAL (et toutes les couches de l’oignon).

Dans un modulith, le piège existe aussi. On crée vite une DAL remplie de DTO (Data Transfer Objects). C’est le modèle “oignon” : couche sur couche. À chaque fois que tu en épluches une, tu pleures.
Couches `controller`, puis `/service` et `/repository`. C’est le package par layer que tu repères d’un coup d’œil en ouvrant le projet.

En théorie, ces couches ne contiennent pas de métier. Pourtant, on finit toujours par en glisser. Exemple :

  • un client qui passe une commande → on veut son adresse de livraison,
  • le même client à la caisse → on veut savoir comment il paie.

Résultat : les DTO gonflent, les règles s’emmêlent, et l’isolement des domaines disparaît.

Rappel important : si vous voulez parler au client, parlez au module Client. Pas directement à la base.
Un monolith aura sans doute une seule base branchée. Cependant, vous pouvez isoler les modules avec des schémas dédiés.

C’était censé être juste un Design System.

De plus en plus de frameworks mélangent contrôleur et vue. La logique d’orchestration quitte le contrôleur pour se cacher dans les écrans. Le problème c’est que le métier s’imisce.

Reprenons le foodtruck. Un client est végétarien : la vue Menu masque les options viande. Le client ne parle pas français : la vue Menu se traduit en anglais. Petit à petit, la présentation connaît les règles métier.

Il ne montre plus seulement des données ; il prend des décisions.

À la fin, les vues sont chargées.Elles “tartinent” du métier partout, comme une sauce industrielle sur des spaghetti cuisson rapide.Et quand on te demande de réutiliser cette vue ailleurs, tu découvres qu’elle fait bien plus que le café.

Illustration comique avec un peintre qui a dessiné une cuisine en trompe l'oeil au lieu d'un mur blanc.

Dans le monde du foodtruck, c’est le serveur qui répond aux questions. Sa carte ne change pas selon le client.
C’est lui qui guide, explique, traduit si besoin. La carte, elle, reste stable.

C’est pareil dans une architecture modulith : chaque module garde ses vues, et le design system reste le plus frugal possible.
Les règles de métier vivent ailleurs. Les composants doivent être neutres, comme une carte de menu qui ne s’adapte pas à chaque client.

C’est l’armée des Helpers.

Vous la connaissez, cette dépendance commune : la shared-lib, la core-dependency maison.
Ce petit bout de code que j’ai fini par utiliser partout. Un sucre syntaxique, placé dans un module à part, injecté partout.

Oui mais non.
Demain, si je modifie ce Helper pour une feature, tout le monolithe peut casser.
Et là, c’est le festival des régressions en production.
Factoriser le code à outrance, c’est introduire du couplage.

En réalité, le métier s’infiltre partout.
L’architecture modulith lui donne une place : chaque domaine au centre de son module.
Il peut y avoir des Helpers, mais ils doivent rester dans le module.
Ils y seront testés, encapsulés. Et surtout, on arrête de les partager.

Partager les Helpers, c’est comme utiliser la même éponge pour nettoyer les toilettes, la vaisselle et le sol.

Ça devrait être un signal. Je sais pas où mettre ce bout de code ? C’est que le domaine où il devrait être n’existe pas. Alors dans le monde du DDD, ça nécessite de faire un workshop avec toute l’équipe et une cérémonie d’intronisation du nouveau domaine. Vous pouvez juste faire un minimum de documentation technique.

Le grand bus magique qui ressemble à l’histoire sans fin.

Les événements c’est bien en termes de découplage. Vous ne dites pas à l’autre ce qu’il doit faire. Charge à celui qui écoute de savoir ce qu’il va en faire.
La machine à laver a fini de tourner ? Elle fait de la musique mais elle ne me dit pas de venir la vider. C’est moi qui implémente la suite du protocole.

On peut même mettre un bus d’événement. Comme ça on ne sait même plus qui s’abonne à quoi et quoi s’abonne à qui.

L’architecture modulith aide à cadrer les événements.

Le problème c’est que ce contrat entre deux parties devient invisible, informel.

Résultat : un spaghetti asynchrone. Tu touches un champ, dix listeners se réveillent. Les dépendances deviennent invisibles. Le jour où tu renommes un event, tes tests passent… et la prod part en biais.

Pourquoi ça déraille :

  • Les events remplacent des contrats clairs. On “diffuse” au lieu de demander.
  • Les payloads gonflent (objets géants “au cas où”).
  • Les handlers font du métier caché et des effets de bord.
  • Rien n’est versionné ni documenté ; on “casse” sans s’en rendre compte.

Comment rester sain :

  • Event = fait métier, pas ordre ni query. “OrderPaid” oui ; “RecomputeAll” non.
  • Payload minimal, stable, versionné (v2 si besoin).
  • Un owner par type d’événement (le module du domaine).
  • Les listeners restent minces (réplication, notifications, projections).
  • Et encore une fois, documentez 

✅ Checklist – Qu’est-ce qu’un Modulith ?

  • Le module a un contexte métier clair (bounded context, pas une couche technique).
  • Il expose un contrat public (API, ports, événements), pas ses entrailles.
  • Il possède ses propres tests (unitaires et d’intégration).
  • Son domaine et ses règles vivent à l’intérieur, pas dans une shared-lib.
  • Il ne dépend pas directement des bases ou des DTO d’un autre module.
  • Les éventuels helpers restent internes au module.
  • Les frontières sont matérialisées dans le code (paquetages, namespaces, décorateurs).
  • Il peut être isolé et extrait sans casser l’application.

C’est quoi la Roadmap pour du Modulith ?

En vrai il y a pas mal d’urbanisme à faire.

On va pas commencer par réécrire tout le code.
Isolez d’abord un module et voyez ce que ça donne. Ça sera un peu votre domaine citadelle qui surplombe la plaine. Oui il va regarder le reste du code comme si c’était la plèbe.

Vous aurez sûrement besoin d’animer un atelier avec vos collègues pour organiser les domaines et résoudre les problèmes de dépendance cyclique à un certain point.

Si vous avez déjà adopté l’architecture hexagonale ou le DDD, vous êtes sûrement très proches. Pensez à matérialiser les frontières dans le code. Explicitez les contrats. Ajoutez une couche de tests sur le module et une documentation.

C’est parti !

Damien Cavaillès

Auteur Damien Cavaillès

Plus d'articles par Damien Cavaillès

Laisser un commentaire