Pourquoi l’industrie revient au monolithe (mais en mieux)

Pendant près d’une décennie, les microservices ont été présentés comme la solution universelle à tous les problèmes d’architecture logicielle. Chaque conférence technique, chaque article de blog semblait marteler le même message : découpez tout, distribuez tout, déployez indépendamment.

Puis la réalité du terrain a rattrapé les équipes.

Selon une étude d’InfoQ publiée en 2023, plus de 60 % des entreprises ayant migré vers des microservices rapportent une augmentation significative de leur complexité opérationnelle. Débogage distribué, gestion de la cohérence des données, explosion du nombre de dépôts, orchestration des déploiements… Le coût caché des microservices est devenu impossible à ignorer.

C’est dans ce contexte qu’émerge — ou plutôt réémerge — une approche pragmatique : le monolithe modulaire. Non pas un retour en arrière, mais un pas de côté intelligent.

Qu’est-ce qu’un monolithe modulaire exactement ?

Un monolithe modulaire est une application déployée comme une unité unique (un seul artefact, un seul processus), mais dont l’architecture interne est découpée en modules autonomes, chacun représentant un domaine métier distinct.

Contrairement au monolithe classique — souvent qualifié de “big ball of mud” — où les couches s’entremêlent et les dépendances forment un plat de spaghettis, le monolithe modulaire impose des frontières claires entre les modules.

Les trois piliers fondamentaux

  • Encapsulation forte : chaque module expose uniquement une API interne bien définie. Les détails d’implémentation restent privés.
  • Dépendances explicites : aucun module ne peut accéder directement à la base de données ou aux classes internes d’un autre module.
  • Cohésion métier : le découpage suit les frontières du domaine (Domain-Driven Design), pas les couches techniques.

Concrètement, un module “Commandes” ne va jamais interroger directement la table users gérée par le module “Utilisateurs”. Il passera par une interface publique.

Monolithe modulaire vs microservices : comparaison honnête

Pour bien comprendre l’intérêt de cette approche, comparons-la frontalement aux microservices sur des critères concrets.

CritèreMonolithe modulaireMicroservices
Complexité de déploiementFaible (1 artefact)Élevée (N artefacts, orchestration)
Latence inter-modulesQuasi nulle (appels en mémoire)Variable (réseau, sérialisation)
Cohérence des donnéesTransactions ACID simplesSaga patterns, eventual consistency
Scalabilité indépendanteNon (scaling vertical/horizontal global)Oui (scaling par service)
Taille d’équipe minimale2-5 développeurs10+ avec DevOps dédié
Coût d’infrastructureModéréÉlevé (Kubernetes, service mesh…)
Facilité de débogageÉlevée (stack trace unique)Faible (tracing distribué requis)
Évolutivité architecturaleMigration possible vers microservicesDifficile de revenir en arrière

Le constat est limpide : le monolithe modulaire offre 80 % des bénéfices structurels des microservices pour 20 % de leur complexité opérationnelle.

Chez Lueur Externe, agence web experte depuis 2003, nous constatons régulièrement que des projets auraient gagné des mois de développement en adoptant d’emblée une architecture modulaire plutôt qu’une distribution prématurée en microservices.

Comment structurer un monolithe modulaire : guide pratique

Étape 1 : identifier les domaines métier

Avant d’écrire la moindre ligne de code, cartographiez vos bounded contexts (contextes délimités au sens du Domain-Driven Design). Chaque contexte deviendra un module.

Prenons l’exemple d’une plateforme e-commerce :

  • Catalogue : gestion des produits, catégories, attributs
  • Panier : gestion du panier, calcul des prix
  • Commandes : workflow de commande, statuts, historique
  • Paiement : intégration PSP, transactions
  • Utilisateurs : authentification, profils, adresses
  • Expédition : calcul des frais, suivi des colis

Étape 2 : organiser l’arborescence du projet

L’organisation physique des fichiers doit refléter la modularité. Voici un exemple de structure pour une application PHP (Symfony) ou Node.js :

src/
├── Modules/
│   ├── Catalog/
│   │   ├── Application/        # Cas d'usage (services applicatifs)
│   │   ├── Domain/             # Entités, value objects, interfaces
│   │   ├── Infrastructure/     # Repositories, APIs externes
│   │   └── CatalogModule.php   # Point d'entrée, API publique du module
│   ├── Order/
│   │   ├── Application/
│   │   ├── Domain/
│   │   ├── Infrastructure/
│   │   └── OrderModule.php
│   ├── Payment/
│   │   ├── Application/
│   │   ├── Domain/
│   │   ├── Infrastructure/
│   │   └── PaymentModule.php
│   └── User/
│       ├── Application/
│       ├── Domain/
│       ├── Infrastructure/
│       └── UserModule.php
├── SharedKernel/               # Code partagé (events, interfaces communes)
│   ├── Events/
│   ├── ValueObjects/
│   └── Contracts/
└── Infrastructure/             # Infrastructure transverse (framework, config)
    ├── Http/
    ├── Persistence/
    └── Messaging/

Cette structure peut s’adapter à n’importe quel langage : Python (Django), Java (Spring Boot), TypeScript (NestJS), PHP (Laravel ou Symfony).

Étape 3 : définir les contrats entre modules

Chaque module expose une façade publique — une interface qui constitue le seul point de contact autorisé. Voici un exemple concret en PHP :

// src/Modules/User/UserModule.php
namespace App\Modules\User;

use App\Modules\User\Application\GetUserById;
use App\Modules\User\Application\DTO\UserSummaryDTO;

final class UserModule
{
    public function __construct(
        private readonly GetUserById $getUserById,
    ) {}

    /**
     * API publique : seule méthode accessible aux autres modules.
     * Retourne un DTO, jamais une entité interne.
     */
    public function getUserSummary(string $userId): ?UserSummaryDTO
    {
        return $this->getUserById->execute($userId);
    }
}

Points essentiels à retenir :

  • Les autres modules n’accèdent jamais aux classes internes (repositories, entités Doctrine, etc.).
  • La communication se fait via des DTO (Data Transfer Objects) et non des entités du domaine.
  • Les événements de domaine permettent la communication asynchrone entre modules sans créer de couplage direct.

Étape 4 : appliquer les règles d’architecture avec des outils

Les conventions ne suffisent pas. Il faut outiller le respect des frontières modulaires. Plusieurs outils existent selon les écosystèmes :

  • PHP : Deptrac — définit les règles de dépendances entre couches et modules
  • Java : ArchUnit — tests d’architecture automatisés
  • TypeScript/JavaScript : eslint-plugin-boundaries ou Nx avec des contraintes de visibilité
  • Python : import-linter

Exemple de configuration Deptrac pour interdire au module Order d’accéder aux internals du module User :

# deptrac.yaml
parameters:
  layers:
    - name: OrderModule
      collectors:
        - type: directory
          value: src/Modules/Order/.*
    - name: UserModulePublic
      collectors:
        - type: className
          value: App\\Modules\\User\\UserModule
    - name: UserModuleInternal
      collectors:
        - type: directory
          value: src/Modules/User/(?!UserModule).*
  ruleset:
    OrderModule:
      - UserModulePublic      # Autorisé
      # UserModuleInternal    # Interdit implicitement

Ces vérifications s’intègrent dans votre pipeline CI/CD et garantissent que personne ne “triche” en court-circuitant les interfaces.

Communication entre modules : synchrone et asynchrone

Deux patterns coexistent dans un monolithe modulaire :

Communication synchrone via les façades

C’est le cas le plus simple. Le module Commandes appelle la façade du module Utilisateurs pour récupérer les informations d’un client. L’appel est en mémoire, sans latence réseau.

Communication asynchrone via les événements de domaine

Quand un module doit notifier d’autres modules d’un changement d’état sans attendre de réponse, on utilise des événements :

// Dans le module Order, après la validation d'une commande
$this->eventDispatcher->dispatch(
    new OrderConfirmedEvent(
        orderId: $order->getId(),
        userId: $order->getUserId(),
        totalAmount: $order->getTotal()
    )
);

Le module Paiement écoute cet événement et déclenche le processus de paiement. Le module Expédition écoute le même événement pour préparer le colis.

L’avantage considérable : dans un monolithe, ces événements transitent en mémoire (via un event dispatcher synchrone ou une queue in-process). Pas de broker Kafka à gérer, pas de RabbitMQ à monitorer. Et si un jour vous extrayez un module en microservice, la migration consiste simplement à remplacer le dispatcher in-process par un broker externe.

Les erreurs classiques à éviter

Après avoir accompagné de nombreux clients sur des projets de structuration applicative, l’équipe de Lueur Externe a identifié des pièges récurrents :

1. Le faux modulaire : des dossiers sans frontières

Créer une arborescence en modules mais laisser les imports libres entre eux revient à peindre des lignes sur un parking sans barrières. Sans outillage de vérification, le monolithe redevient un “big ball of mud” en quelques mois.

2. La base de données partagée sans discipline

Dans un monolithe modulaire, tous les modules partagent souvent la même base de données physique. C’est acceptable, mais chaque module doit avoir son propre schéma logique (ou préfixe de tables) et ne jamais faire de JOIN entre les tables de deux modules différents.

3. Le sur-découpage prématuré

Créer 25 modules pour une application de 10 000 lignes de code est contreproductif. Commencez avec 3 à 5 modules alignés sur les domaines métier principaux. Affinez ensuite quand la complexité le justifie.

4. Ignorer les tests d’intégration inter-modules

Chaque contrat entre modules doit être couvert par des tests d’intégration. Si la façade du module Utilisateurs change sa signature, les tests du module Commandes doivent casser immédiatement.

Cas concret : e-commerce Prestashop et architecture modulaire

Prestashop, plateforme sur laquelle Lueur Externe est certifiée experte, a d’ailleurs amorcé cette transition avec ses versions récentes. Le cœur de Prestashop 8.x tend vers une architecture plus modulaire avec une séparation entre les commandes CQRS, les query buses et les modules tiers.

Pour les développeurs qui construisent des modules Prestashop custom, appliquer les principes du monolithe modulaire signifie :

  • Isoler la logique métier dans des services indépendants du framework
  • Exposer des interfaces claires consommables par d’autres modules
  • Utiliser les hooks Prestashop comme mécanisme d’événements inter-modules
  • Tester unitairement le domaine sans dépendre de la base de données

Ce type de rigueur architecturale transforme un simple module Prestashop en composant maintenable et testable sur le long terme.

Quand migrer vers des microservices malgré tout ?

Le monolithe modulaire n’est pas une fin en soi. C’est une étape stratégique. Voici les signaux qui indiquent qu’une extraction en microservice devient pertinente :

  • Un module a des besoins de scaling radicalement différents : le moteur de recherche catalogue reçoit 100x plus de requêtes que le module de gestion des retours.
  • Une équipe dédiée travaille exclusivement sur un module et ses cycles de release divergent fortement du reste.
  • Un module nécessite une stack technologique différente : un moteur de recommandation en Python alors que le reste est en PHP.
  • La taille de l’application dépasse 500 000 lignes de code et les temps de build/test deviennent prohibitifs.

La beauté du monolithe modulaire, c’est que cette extraction est chirurgicale : vous sortez un module avec son domaine, ses interfaces déjà définies, et vous remplacez les appels in-process par des appels réseau. Le travail de découpage a déjà été fait.

Métriques de succès : comment savoir si ça fonctionne

Pour évaluer la santé de votre monolithe modulaire, surveillez ces indicateurs :

  • Nombre de dépendances inter-modules : doit rester stable ou diminuer dans le temps
  • Couverture de tests par module : chaque module doit être testable indépendamment
  • Temps de compréhension pour un nouveau développeur : un bon module se comprend en moins d’une journée
  • Nombre de fichiers modifiés par fonctionnalité : si une feature touche systématiquement 4 modules, le découpage est probablement mauvais
  • Violations d’architecture détectées par CI : doit tendre vers zéro

Conclusion : le pragmatisme architectural comme philosophie

Le monolithe modulaire n’est ni glamour ni hype. C’est une approche pragmatique qui reconnaît une vérité simple : la plupart des applications n’ont pas besoin de la complexité des microservices pour être bien structurées, performantes et maintenables.

En imposant des frontières claires entre les domaines métier, en outillant le respect de ces frontières et en communiquant via des interfaces explicites, vous obtenez une base de code qui vieillit bien. Une base de code où l’ajout d’une fonctionnalité ne provoque pas de régressions en cascade. Une base de code prête pour le futur, qu’il soit monolithique ou distribué.

Chez Lueur Externe, nous accompagnons nos clients depuis plus de 20 ans dans la conception d’architectures web robustes — qu’il s’agisse de sites e-commerce Prestashop, d’applications métier sur mesure ou d’infrastructures AWS. Si vous souhaitez auditer votre application existante ou démarrer un nouveau projet sur des bases architecturales solides, nos experts sont à votre disposition pour en discuter.