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.
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.
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 :
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.
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 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.
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.
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.
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 :
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.
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é.
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.
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.
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.
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 :
Comment rester sain :
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 !
Les deux premiers articles parlaient d’erreurs banales. Celui-ci, c’est deux mois de perdus parce que…
Les insights inspirants d’Hélène Ly. "Le recrutement tech n’est pas une bataille, mais une collaboration.…
Un nouveau capitaine technique débarque à la barre de WeLoveDevs ! Après le rachat par…
L’AI Act pour les développeurs, c’est la première loi vraiment impactante depuis le RGPD. Et…
"Venez, faites le module 1 et on en reparle." C’est le défi lancé par Gaetan…
L’OWASP Top 10, c’est un outil pour les développeurs web. Et pourtant, il est largement…