Aller au contenu

Monolithe modulaire

Un seul déploiement, des frontieres claires — la topologie qui couvre 90% des besoins réels.


Le choix par défaut

Le monolithe modulaire est une application déployée en un seul bloc, mais dont le code interne est organisé en modules avec des frontieres explicites. Les modules communiquent via des interfaces définies, pas via des appels directs entre classes.

C'est la topologie la plus sous-estimée. La majorité des projets qui choisissent des microservices "par défaut" auraient été plus rapides, plus simples et plus fiables avec un monolithe modulaire bien structure. La raison est simple : la complexité opérationnelle des microservices est un coût fixe qui ne se justifie que quand les benefices (scalabilité indépendante, autonomie d'équipe) depassent ce coût. Pour une équipe de 3 a 15 personnes, c'est rarement le cas.

graph TB
    subgraph Monolith["Application (un seul deploiement)"]
        API["API Layer"]
        subgraph Modules
            U["Users"]
            O["Orders"]
            P["Payments"]
            I["Inventory"]
        end
        API --> U
        API --> O
        O -->|interface| P
        O -->|interface| I
        P -->|interface| I
    end
    DB[("Database")]
    Monolith --> DB

Les modules ont chacun leur propre modèle de données (schémas séparés ou préfixes de tables). La communication inter-modules passe par des interfaces explicites — jamais par un import direct dans les couches internes d'un autre module.


Structure interne

Structure typique d'un monolithe modulaire :

src/
  modules/
    users/
      domain/          # entites, value objects
      application/     # use cases, ports
      infrastructure/  # persistence, adapters
      api/             # controllers, schemas HTTP
    orders/
      domain/
      application/
      infrastructure/
      api/
    payments/
      ...
  shared/
    events/            # types d'evenements inter-modules
    kernel/            # types partages (UserId, Money)
  app.py               # bootstrap, injection de dependances

La règle d'or : un module ne peut importer que l'interface publique d'un autre module (son api/ ou son application/ports/), jamais ses couches internes (domain/, infrastructure/).

Chaque module possédé son propre schéma de base de données. Les jointures cross-schéma sont interdites. Si un module a besoin d'une donnée d'un autre module, il passe par l'interface publique. Cette contrainte semble rigide, mais elle est la garantie que le module pourra être extrait en service indépendant le jour ou c'est nécessaire.


Enforcer les frontieres

Les frontieres de modules ne tiennent que si elles sont verifiees automatiquement. La discipline humaine ne suffit pas — un import interdit qui passe en code review un vendredi soir crée un couplage qui mettra des mois a être détecté.

ArchUnit (Java/Kotlin) :

@ArchTest
static final ArchRule modules_respect_boundaries =
    slices().matching("com.app.modules.(*)..")
        .should().notDependOnEachOther()
        .as("Les modules ne doivent pas dependre directement les uns des autres");

@ArchTest
static final ArchRule no_internal_access =
    noClasses()
        .that().resideInAPackage("..modules.orders..")
        .should().accessClassesThat()
        .resideInAPackage("..modules.payments.domain..")
        .as("Orders ne doit pas acceder aux internals de Payments");

Règles de dépendances (Python — import-linter) :

[importlinter]
root_package = app

[importlinter:contract:module-boundaries]
name = Module boundaries
type = independence
modules =
    app.modules.users
    app.modules.orders
    app.modules.payments

TypeScript — eslint-plugin-boundaries :

{
  "rules": {
    "boundaries/element-types": [2, {
      "default": "disallow",
      "rules": [
        { "from": "orders", "allow": ["payments/api", "inventory/api"] }
      ]
    }]
  }
}

Ces verifications tournent dans la CI. Un build qui viole une frontiere échoué. C'est non negociable.


Migration vers le monolithe modulaire

La plupart des monolithes existants ne sont pas modulaires — ce sont des "big balls of mud" ou tout appelle tout. La migration suit un chemin progressif :

flowchart LR
    A["Big Ball of Mud"] -->|"1. Identifier\nles modules"| B["Modules\nimplicites"]
    B -->|"2. Extraire\nles interfaces"| C["Modules avec\ninterfaces"]
    C -->|"3. Separer\nles schemas"| D["Monolithe\nmodulaire"]
    D -->|"4. Si besoin"| E["Extraction\nen service"]

Étape 1 — Identifier les modules candidats. Chercher les clusters de code qui changent ensemble. L'analyse des commits git (quels fichiers sont commites ensemble) révélé les frontieres naturelles mieux que les diagrammes UML. Des outils comme code-maat ou git log --format aident à quantifier le couplage temporel.

Étape 2 — Extraire les interfaces. Pour chaque module identifié, définir une interface publique (ports, DTOs). Remplacer progressivement les imports internes par des appels à l'interface. C'est le travail le plus long.

Étape 3 — Séparer les schémas de données. Chaque module obtient son propre schéma ou préfixe de tables. Les jointures cross-module sont remplacees par des appels à l'interface. Les vues materialisees peuvent servir de tampon temporaire.

Étape 4 — Extraction optionnelle. Une fois qu'un module est isole (interface propre, schéma propre, pas de couplage), l'extraire en service indépendant est une opération mecanique, pas une reecriture.


Quand extraire un module en service

L'extraction d'un module en service indépendant a un coût significatif : serialisation réseau, latence, complexité opérationnelle, gestion des pannes partielles. Elle ne se justifie que quand un benefice concret compense ce coût.

Signaux d'extraction :

Signal Pourquoi ca justifie l'extraction
Scalabilité differentielle Un module reçoit 50x plus de trafic que les autres et doit scaler independamment
Équipe autonome Une équipe de 5+ personnes travaille exclusivement sur ce module et veut son propre cycle de release
Technologie différente Le module beneficierait d'un runtime différent (ML en Python, temps réel en Go)
Fréquence de déploiement Le module change 10x plus souvent que le reste et le déploiement du monolithe complet est trop lent
Isolation de panne Un bug dans ce module ne doit pas faire tomber le reste du système

Signaux de non-extraction :

  • Le module est petit (< 5000 LOC) — le coût opérationnel d'un service depassera le benefice
  • Le module partage beaucoup de données avec d'autres modules — l'extraction creera un monolithe distribué
  • L'équipe n'a pas de CI/CD mature ni de monitoring distribué
  • La motivation est "parce que c'est la bonne pratique" — ca ne suffit pas

Ne pas extraire prematurement

L'extraction prematuree est plus coûteuse que l'extraction tardive. Un module bien isole dans un monolithe peut être extrait en quelques semaines. Un service mal découpé peut prendre des mois a corriger. En cas de doute, ne pas extraire.


Avantages du monolithe modulaire

  • Déploiement simple — un seul artefact, une seule pipeline
  • Debugging direct — stack trace complète, pas de tracing distribué
  • Refactoring facile — renommer une interface est une opération locale
  • Tests d'intégration sans overhead réseau
  • Montee en compétence rapide pour les nouveaux développeurs
  • Transactions ACID — les opérations cross-modules peuvent partager une transaction si nécessaire

Quand choisir le monolithe modulaire :

Signal Implication
Équipe < 10 personnes Le coût de coordination des microservices dépassé le benefice
Domaine bien compris Les frontieres peuvent être tracees avec confiance
Time-to-market prioritaire Livrer vite demande de minimiser la friction opérationnelle
Pas de besoin de scalabilité differentielle Toutes les parties du système grandissent ensemble

Le monolithe modulaire n'est pas un compromis

Le monolithe modulaire n'est pas un compromis — c'est souvent la meilleure architecture pour 90% des projets. Commencez par la. Si un jour un module doit être extrait, les frontieres propres rendront l'opération mecanique plutôt que chirurgicale.


Chapitre suivant : Microservices — autonomie des services, prérequis opérationnels et pieges du monolithe distribué.