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
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ère | Séparée (Notre choix) | Centralisée Domain | Centralisé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
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
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épendant | Approche Séparée ✅ |
| Découplage maximal + tous ports dans domain | Approche Centralisée Domain |
| Simplicité + petit projet | Approche 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.
- Dependency Flow → - Comprendre le flux de dépendances
- Domain Layer → - Structurer le cœur métier
- Ports Guide → - Créer vos premiers ports