Skip to Content
02 ArchitectureVariantes d'Organisation

Variantes d’Organisation

Il existe plusieurs approches pour organiser les ports et adapters dans l’architecture hexagonale. Chaque approche a ses avantages et inconvénients.

Ce chapitre compare 3 variantes d’organisation pour vous aider à choisir celle qui convient le mieux à votre projet.


Les 3 Approches

Il n’y a pas de mauvaise approche, seulement des trade-offs selon la taille et les besoins du projet.

1. Approche Séparée ✅

Notre choix actuel - Ports répartis entre domain/ et application/.

Structure

Principe

  • Output Ports dans domain/port/output/ - Le domain définit ses besoins
  • Input Ports dans application/port/input/ - Points d’entrée (Controllers REST)
  • Séparation sémantique claire

Exemple de Code

// domain/port/output/TrendRepository.java package org.smoka.domain.port.output; public interface TrendRepository { Optional<TrendResult> getTrends(TrendQuery query); } // application/port/input/TrendController.java package org.smoka.application.port.input; @Controller("/api/trends") @RequiredArgsConstructor public class TrendController { private final GetTrendsUseCase getTrendsUseCase; @Get public TrendResponseDto getTrends(@QueryValue String keyword) { return getTrendsUseCase.execute(keyword) .map(TrendResponseDto::fromDomain) .orElseThrow(() -> new NotFoundException()); } } // application/usecase/GetTrendsUseCase.java @Singleton @RequiredArgsConstructor public class GetTrendsUseCase { private final TrendRepository repository; // Interface du domain public Optional<TrendResult> execute(String keyword) { return repository.getTrends(new TrendQuery(keyword)); } }

Avantages

Clarté maximale - On voit immédiatement que les output ports appartiennent au domain

Respect strict du DDD - Le domain définit explicitement ses besoins

Séparation forte - Domain = besoins (output), Application = entrées (input)

Tests d’architecture stricts - Impossible de créer un output port dans application/ par erreur

Domain 100% indépendant - Aucune dépendance vers l’application

Inconvénients

Plus verbeux - Deux dossiers port/ distincts

Peut sembler complexe - Pour de petites applications simples

Quand l’utiliser ?

  • ✅ Vous voulez une clarté maximale
  • ✅ Votre Controller EST le port d’entrée (pas d’interface use case)
  • ✅ Vous voulez que le domain ne dépende de RIEN
  • ✅ Projet moyen à grand (10-50 use cases)

Comparaison des 3 Approches

Tableau comparatif pour vous aider à choisir l’approche la plus adaptée à votre projet.

CritèreSéparée (Notre choix)Centralisée DomainCentralisée Unique
Clarté sémantique⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Simplicité structure⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Respect DDD strict⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Testabilité Use Cases⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Verbosité (nb fichiers)⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Risque de violation⭐⭐⭐⭐⭐ (très faible)⭐⭐⭐⭐⭐⭐⭐ (plus élevé)
Découplage controllers⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

Notre recommandation : L’approche Séparée offre le meilleur équilibre pour les projets moyens à grands.


Pourquoi Notre Choix est Solide

Notre approche sépare les ports pour maximiser la clarté sémantique et le respect du DDD.

1. Sémantique Claire

✅ Output Ports = Besoins du DOMAIN → domain/port/output/ ✅ Input Ports = Entrées de l'APPLICATION → application/port/input/

Le domain définit ce dont il a besoin (repositories, services externes). L’application définit comment on y accède (REST, CLI, GraphQL).

2. Respect du Principe de Responsabilité Unique

Domain Layer

Rôle : “Voici ce dont j’ai besoin pour fonctionner”

// domain/port/output/TrendRepository.java public interface TrendRepository { Optional<TrendResult> getTrends(TrendQuery query); } // domain/port/output/EmailService.java public interface EmailService { void sendNotification(User user, String message); }

Le domain définit ses besoins via des interfaces (output ports).

3. Tests d’Architecture Plus Stricts

Avec nos 15 règles ArchUnit, on peut vérifier que le domain ne dépend de rien :

@Test void domainShouldNotDependOnAnything() { noClasses() .that().resideInAPackage("..domain..") .should().dependOnClassesThat().resideInAnyPackage( "..application..", // ← Impossible de dépendre de application/port/input "..infrastructure.." ) .check(classes); }

Si les ports étaient tous dans domain/, il faudrait autoriser domain.port.input → moins strict.

4. Adapté aux Controllers Directs

Dans notre app, le Controller EST le port d’entrée (pas d’interface use case). Il est donc logique qu’il soit dans application/port/input/.

Si on créait des interfaces de use cases, on pourrait basculer vers l’approche “Centralisée Domain”.


Migration vers l’Approche Centralisée Domain

Si demain vous voulez découpler les controllers des use cases, voici comment migrer.

Créer l’interface dans le domain

// domain/port/input/GetTrendsUseCase.java package org.smoka.domain.port.input; import org.smoka.domain.model.TrendResult; import java.util.Optional; public interface GetTrendsUseCase { Optional<TrendResult> execute(String keyword, String region); }

Renommer le use case actuel en “Impl”

// application/usecase/GetTrendsUseCaseImpl.java package org.smoka.application.usecase; @Singleton @RequiredArgsConstructor public class GetTrendsUseCaseImpl implements GetTrendsUseCase { private final TrendRepository trendRepository; @Override public Optional<TrendResult> execute(String keyword, String region) { // Logique métier inchangée TrendQuery query = new TrendQuery(keyword, region); return trendRepository.getTrends(query); } }

Déplacer le controller dans infrastructure

// infrastructure/adapter/rest/TrendController.java package org.smoka.infrastructure.adapter.rest; @Controller("/api/trends") @RequiredArgsConstructor public class TrendController { private final GetTrendsUseCase getTrendsUseCase; // ← Interface du domain @Get public TrendResponseDto getTrends(@QueryValue String keyword) { return getTrendsUseCase.execute(keyword, "US") .map(TrendResponseDto::fromDomain) .orElseThrow(() -> new NotFoundException()); } }

Mettre à jour les tests d’architecture

@Test void controllersShouldResideInInfrastructure() { classes() .that().haveSimpleNameEndingWith("Controller") .should().resideInAPackage("..infrastructure.adapter.rest..") .check(classes); }

Avantage : Maintenant vous pouvez facilement mocker GetTrendsUseCase dans les tests du controller !


Recommandations par Taille de Projet

Petit Projet (< 10 use cases)

Approche recommandée : Centralisée Unique ou Séparée

La simplicité prime. Peu de risque de violation architecturale.

Pourquoi ?

  • ✅ Structure simple et rapide à mettre en place
  • ✅ Facile à comprendre pour une petite équipe
  • ✅ Moins de fichiers = navigation plus rapide
  • ✅ Tests d’architecture optionnels

Exemple : Application de gestion de TODO, blog personnel, POC


Checklist de Validation

Pour vérifier que votre organisation respecte l’architecture hexagonale :

  • Le domain ne dépend de RIEN (pas d’import depuis infrastructure ou application)
  • Les ports de sortie sont des interfaces dans le domain
  • Les adaptateurs implémentent les ports de sortie
  • Les use cases dépendent des interfaces (pas des implémentations)
  • On peut remplacer un adapter sans toucher au domain
  • Les tests du domain n’ont pas besoin d’infrastructure réelle
  • La navigation dans le code est intuitive (structure claire)

Si toutes les cases sont cochées, votre organisation est cohérente avec l’architecture hexagonale ! 🎉


Résumé

Il n’y a pas de mauvaise approche, seulement des trade-offs selon vos besoins.

Si vous voulez…Choisissez…
Clarté maximale + domain indépendantApproche Séparée
Découplage maximal + tous ports dans domainApproche Centralisée Domain
Simplicité + petit projetApproche Centralisée Unique

La règle d’or reste la même : Le domain ne dépend de rien, et définit ses besoins via des interfaces (output ports). 🎯


Prochaines Étapes

Maintenant que vous comprenez les variantes d’organisation, explorez les patterns d’implémentation.


Ressources

Last updated on