Aller au contenu

Architecture monolithique modulaire

Introduction

Une application peut devenir difficile à faire évoluer sans pour autant être distribuée sur plusieurs serveurs. Lorsque les fonctionnalités partagent leurs classes, une modification locale peut avoir des effets dans différentes parties de code.

L’architecture monolithique modulaire répond à ce problème en conservant une application déployée comme un ensemble, tout en organisant son code autour de modules métier aux responsabilités établies.

Qu’est-ce que l’architecture monolithique modulaire ?

Un monolithe modulaire possède généralement un point d’entrée et un déploiement communs, mais son code est réparti en modules tels que Catalogue, Commandes et Facturation. Chaque module prend en charge une capacité métier identifiable : ses cas d’utilisation, ses règles, ses contrats et, selon le découpage retenu, sa persistance.

La distinction essentielle porte sur les dépendances. Le module Commandes peut demander au module Catalogue si un produit est disponible via un contrat prévu à cet effet. Il ne devrait pas modifier directement les entités ou les tables internes du catalogue.

La modularité n’impose pas une structure unique. Un module peut correspondre à un projet C#, à plusieurs projets, ou à un ensemble de dossiers lorsque la taille de l’application le justifie.

Exemple d’organisation avec .NET

Architecture Monolithique-Modulaire - Arborescence

Le projet Application.Api assemble les modules et héberge l’application ASP.NET Core. Les projets *.Contracts exposent uniquement ce que d’autres modules sont autorisés à utiliser. Le SharedKernel (optionnel), contient un petit nombre de concepts réellement communs ; il ne doit pas contenir du code en quantité et partagé pour simplifier son utilisation.

L’injection de dépendances intégrée à ASP.NET Core permet d’enregistrer les services des modules au démarrage de l’application.

Comment les modules communiquent-ils ?

Pour une opération demandant une réponse immédiate, un module peut utilisé un contrat C# explicite exposé par un autre module. Pour signaler qu’un fait métier s’est produit, il peut publier un événement interne auquel d’autres modules réagissent.

Par exemple, Commandes peut lever l'évènement CommandeValidee. Facturation réagit alors à cet événement, sans que le code de validation d’une commande connaisse les classes internes de facturation. Toutefois, ce choix exige de préciser quand les gestionnaires s’exécutent, comment leurs erreurs sont traitées et quelles opérations participent à la même transaction.

Une base de données commune est possible. Elle ne dispense pas de définir la propriété des données : chaque module est responsable des écritures sur ses propres tables. Les lectures entre modules doivent être définies explicitement, particulièrement lorsque des jointures directes créeraient un couplage fort qui risque de rester dans le temps.

Les avantages

Des fonctionnalités plus faciles à retrouver

Les règles et les traitements d’une fonctionnalité sont regroupés dans un module identifiable.

Un déploiement simple

L’application reste déployée comme un ensemble, sans exploitation de plusieurs services indépendants.

Des échanges directs

Les modules peuvent communiquer dans le même processus, sans latence réseau pour chaque appel.

Des transactions plus accessibles

Lorsque les données sont contenues dans la même base de données, certaines opérations peuvent partager une transaction.

Une évolution progressive

Les modules peuvent être introduits au fur et à mesure que les responsabilités métier s'affinent.

Les limites

Un déploiement commun

La modification d’un module conduit généralement à redéployer l’application dans son intégralité.

Une mise à l’échelle globale

Si un module consomme beaucoup de ressources, l'augmentation du nombre d’instances doit être réaliser sur les autres modules.

Une isolation limitée des défaillances

Une exception non maîtrisée ou une forte consommation de ressources dans un module peut affecter toute l’application.

Des frontières faciles à contourner

Le même processus et la même base de données rendent tentants les appels aux classes internes (entités) et les accès directs aux tables d’un autre module.

Quand ce choix est-il pertinent ?

Cette architecture convient lorsque l’application couvre plusieurs capacités métier distinctes, sans que leur déploiement indépendant soit une plus-value. Elle peut aussi être adaptée pour une application métier en croissance, ou pour la refonte progressive d’un monolithe dont les responsabilités sont mélangées.

Le choix mérite d’être réexaminé lorsqu’une partie de l’application a des exigences fortes de déploiement indépendant, de montée en charge spécifique ou d’isolation opérationnelle. Un module bien délimité peut faciliter une extraction future, qui restera un projet à part entière : elle demandera de traiter les données, les transactions et la communication via le réseau.

Bonnes pratiques avec .NET

Définir les modules par capacité métier

Regrouper les règles métier et les cas d’utilisation qui évoluent ensemble.

Exposer des contrats restreints

Publier les opérations et les événements nécessaires aux autres modules sans exposer les types internes ni les entités EF Core.

Contrôler les références entre projets

Faire dépendre un module des contrats autorisés et vérifier régulièrement qu’aucune référence ne contourne cette règle.

Attribuer la propriété des données

Désigner le module responsable de chaque donnée et éviter qu’un autre module modifie directement les tables contenant ces données.

Encadrer EF Core par module

Organiser les configurations et les migrations selon les responsabilités retenues, puis vérifier les accès à la base de données.

Tester les frontières

Vérifier les règles de chaque module, leurs contrats et les parcours qui traversent plusieurs modules.

Surveiller le code partagé

Partager uniquement le code communs pour éviter de recréer un couplage global.