DDD + Modulith : Concevoir une architecture propre… sans le piège des microservices

Domain Driven Design

Introduction

Dans beaucoup d’organisations, l’architecture microservices est devenue un réflexe pavlovien. Pourtant, elle introduit souvent plus de complexité opérationnelle que de valeur métier lorsqu’elle est adoptée prématurément.

Une alternative élégante émerge : la combinaison du Domain-Driven Design (DDD) pour la structure métier et du Modulith pour la structure technique.

L’objectif ? Une application modulaire, maintenable et évolutive, tout en restant dans un monolithe maîtrisé.


Le problème : La complexité accidentelle

Avant de parler solution, regardons la réalité du terrain. Une architecture microservices mal justifiée entraîne :

  • Orchestration complexe (CI/CD, déploiement, réseau).
  • Observabilité difficile (logs distribués, traçabilité).
  • Couplage distribué : le pire des deux mondes (dépendances réseau fragiles).

Résultat : On complexifie le système sans améliorer la qualité logicielle.


La philosophie : Un monolithe, mais segmenté

L’approche Modulith (notamment via des outils comme Spring Modulith) consiste à découper l’application en unités fortement encapsulées selon des règles strictes :

  1. Un module = Un Bounded Context.
  2. Une API publique exposée.
  3. Une implémentation interne strictement cachée.

En résumé : on développe avec la rigueur des microservices, mais sans la latence du réseau.


1. Structurer le domaine avec le DDD

Le DDD impose une règle d’or : Le code doit refléter le métier, pas la base de données.

L’Agrégat : Le gardien des invariants

Voici un exemple minimal de logique métier encapsulée :

@Getter(AccessLevel.PACKAGE)
final class Project {
    private final ProjectId id;
    private final String label;
    private final Status status;

    @Builder(access = AccessLevel.PRIVATE)
    private Project(ProjectId id, String label) {
        this.id = id;
        this.label = label;
        this.status = Status.ACTIVE;
    }
    
    static Project create(String label) {
      return Project.builder()
			  .label(label);
    }

    void deactivate(String reason) {
        if (this.status == Status.INACTIVE) {
            throw new IllegalStateException("Project is already inactive");
        }
        this.status = Status.INACTIVE;
        // Publication d'un événement métier ici
    }
}

Le Value Object : La puissance du typage

public record ProjectId(Long value) {
    public ProjectId {
        if (value == null) throw new IllegalArgumentException("ProjectId cannot be null");
    }
}

Bénéfice : On évite les erreurs de type « Long vs Long » et on apporte une sémantique claire au code.


2. L’encapsulation Modulith : « Internal by default »

Une organisation de fichiers typique respecte cette hiérarchie :

  • project/api/ : Interfaces exposées.
  • project/internal/ : Logique métier et entités (inaccessibles de l’extérieur).
  • project/infrastructure/ : Persistance et adaptateurs.

La Façade : L’unique porte d’entrée

// Dans le package .api
public interface ProjectFacade {
    ProjectId createProject(CreateProjectCommand command);
    void deactivateProject(ProjectId id, String reason);
}

// Dans le package .internal (Package-private)
class ProjectFacadeImpl implements ProjectFacade {
    private final ProjectService projectService;

    @Override
    public ProjectId createProject(CreateProjectCommand command) {
		  return projectService.createProject(command);
	  }
	  
	  @Override
  	public void deactivateProject(ProjectId projectId, String reason) {
  		projectService.deactivate(projectId, reason);
  	}

}

Grâce à la visibilité par défaut de Java (package-private), il est physiquement impossible pour un autre module d’accéder à l’implémentation. L’encapsulation est réelle, pas théorique.


3. Communication : Synchrone ou Événementielle ?

Le Modulith offre deux leviers :

  1. Appel direct (Synchrone) : Simple et lisible pour les opérations critiques.
  2. Domain Events (Asynchrone) : Un module émet un ProjectDeactivatedEvent. Les autres modules réagissent sans connaître l’émetteur. C’est la clé pour extraire un module en microservice plus tard sans douleur.

4. Tester l’architecture : Le garde-fou

Le vrai « Game Changer » est la capacité de tester les règles de dépendances automatiquement.

@ApplicationModuleTest
class ArchitectureTest {
    @Test
    void verifyModularity() {
        var modules = ApplicationModules.of(MyApplication.class);
        modules.verify(); // Échoue si un module accède à l'interne d'un autre
    }
}

Pourquoi cette approche change tout

AvantAprès
Architecture « Plat de spaghetti »Modules aux frontières nettes
Dépendances implicites et fragilesContrat d’interface (API) explicite
Logique métier noyée dans la techniqueMétier central et protégé


Conclusion

Le vrai luxe aujourd’hui n’est pas de posséder une flotte de microservices complexes. C’est de posséder une architecture que l’on comprend encore six mois après la mise en production.

DDD + Modulith permet de construire des systèmes robustes et scalables en retardant la distribution au moment où elle devient réellement nécessaire.

L’idée à retenir : Commencez simple, structurez fort, et distribuez seulement si la charge l’impose.

Commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *