Guide de Création des Ports
Guide pratique pour créer et organiser les Ports Input et Output dans l’architecture hexagonale avec Micronaut.
Ce guide vous montre comment créer des ports (interfaces) qui définissent les contrats entre les différentes couches de votre application.
Qu’est-ce qu’un Port ?
Un Port est une interface qui définit un contrat entre deux couches de l’application.
Ports de Sortie (Output)
Output Ports
Emplacement : domain/port/output/
Rôle :
- Définir comment le domain accède aux ressources externes
- Interface définie PAR le domain (ses besoins)
- Implémentée DANS l’infrastructure (adapters)
Exemple :
// domain/port/output/TrendRepository.java
public interface TrendRepository {
Optional<TrendResult> getTrends(TrendQuery query);
}Qui l’utilise :
- Use Cases (application layer)
- Services métier (domain layer)
Pourquoi les Output Ports sont dans le Domain ?
Règle Fondamentale : Les ports de sortie (Output Ports) sont TOUJOURS dans domain/port/output/, JAMAIS dans application/.
Principe d’Inversion de Dépendances
Le Domain Définit ses Besoins
Le Domain Définit ses Besoins
C’est le domain qui dit : “J’ai besoin d’un repository pour récupérer des trends”.
// domain/port/output/TrendRepository.java
package org.smoka.domain.port.output;
public interface TrendRepository {
/**
* Récupère les tendances pour une requête donnée
*/
Optional<TrendResult> getTrends(TrendQuery query);
}Le domain exprime un contrat, pas une implémentation.
Créer un Output Port
Identifier le Besoin du Domain
Posez-vous la question : “Qu’est-ce que mon domain a besoin de faire ?”
Exemples :
- Récupérer des données d’une API externe →
TrendRepository - Sauvegarder des entités en BDD →
UserRepository - Envoyer des notifications →
NotificationPort - Accéder à un cache →
CachePort
Créer l’Interface dans domain/port/output/
// domain/port/output/TrendRepository.java
package org.smoka.domain.port.output;
import org.smoka.domain.model.trends.TrendQuery;
import org.smoka.domain.model.trends.TrendResult;
import java.util.Optional;
public interface TrendRepository {
/**
* Récupère les tendances pour une requête donnée
*
* @param query La requête contenant le mot-clé et la région
* @return Un Optional contenant le résultat, ou vide si non trouvé
*/
Optional<TrendResult> getTrends(TrendQuery query);
}Définir les Méthodes du Contrat
Bonnes pratiques :
- Utiliser des objets du domain (TrendQuery, TrendResult)
- Éviter les types techniques (HttpRequest, JsonNode)
- Documenter avec Javadoc
- Préférer
Optional<T>pour les retours pouvant être vides
public interface CountryRepository {
// ✅ BON : Types du domain
List<Country> findAll();
Optional<Country> findByName(String name);
// ❌ MAUVAIS : Types techniques
JsonNode getCountriesJson(); // JsonNode = Jackson
HttpResponse<String> fetchCountries(); // HttpResponse = framework
}Utiliser le Port dans un Use Case
// application/usecase/GetTrendsUseCase.java
@Singleton
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class GetTrendsUseCase {
TrendRepository trendRepository; // ← Port de sortie injecté
public Optional<TrendResult> execute(String keyword, String region) {
TrendQuery query = new TrendQuery(keyword, region, LocalDateTime.now());
return trendRepository.getTrends(query);
}
}Micronaut injecte automatiquement l’implémentation (adapter) via @Singleton.
Créer l’Adapter dans infrastructure/adapter/
// infrastructure/adapter/MockTrendRepositoryAdapter.java
@Singleton
public class MockTrendRepositoryAdapter implements TrendRepository {
@Override
public Optional<TrendResult> getTrends(TrendQuery query) {
// Implémentation technique
TrendResult result = TrendResult.builder()
.keyword(query.keyword())
.region(query.region())
.interestScore(generateRandomScore())
.queriedAt(LocalDateTime.now())
.build();
return Optional.of(result);
}
private int generateRandomScore() {
return ThreadLocalRandom.current().nextInt(50, 101);
}
}Le domain définit le contrat (TrendRepository), l’infrastructure l’implémente (MockTrendRepositoryAdapter). Changez l’adapter sans toucher au domain !
Créer un Input Port
Choisir le Type de Port d’Entrée
REST API
REST API avec @Controller
Le plus courant dans les applications Micronaut.
@Controller("/api/trends")
public class TrendController {
@Get
public TrendResponseDto getTrends(@QueryValue String keyword) {
// ...
}
}Cas d’usage : API HTTP classique, microservices REST
Créer le Controller dans application/port/input/
// application/port/input/TrendController.java
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
@Controller("/api/trends")
public class TrendController {
GetTrendsUseCase getTrendsUseCase; // ← Use case injecté
@Get
public TrendResponseDto getTrends(
@QueryValue @NotBlank String keyword,
@QueryValue(defaultValue = "US") String region
) {
return getTrendsUseCase.execute(keyword, region)
.map(TrendResponseDto::fromDomain)
.orElseThrow(() -> new ResourceNotFoundException(
"No trends found for keyword: " + keyword
));
}
}Définir les Responsabilités du Port d’Entrée
Un bon port d’entrée doit :
✅ Validation des paramètres
@QueryValue @NotBlank @Pattern(regexp = "^[a-zA-Z0-9\\s\\-_]+$") String keyword✅ Conversion HTTP → Domain
// HTTP: ?keyword=java®ion=US
// → Domain: TrendQuery(keyword="java", region="US")
getTrendsUseCase.execute(keyword, region)✅ Appel du Use Case
getTrendsUseCase.execute(keyword, region)✅ Conversion Domain → DTO de réponse
.map(TrendResponseDto::fromDomain)✅ Gestion des erreurs HTTP
.orElseThrow(() -> new ResourceNotFoundException(...));Créer le DTO de Réponse
// application/port/input/dto/TrendResponseDto.java
@Value
public class TrendResponseDto {
String keyword;
String region;
Integer interestScore;
List<String> relatedTopics;
LocalDateTime queriedAt;
public static TrendResponseDto fromDomain(TrendResult result) {
return new TrendResponseDto(
result.getKeyword(),
result.getRegion(),
result.getInterestScore(),
result.getRelatedTopics(),
result.getQueriedAt()
);
}
}Variantes d’Organisation des Ports
Il existe 3 approches pour organiser les ports. Voici laquelle choisir selon votre projet.
Approche Séparée
Approche Séparée (Recommandée)
Structure :
- TrendRepository.java
- TrendController.java
Principe :
- Output Ports dans
domain/port/output/(besoins du domain) - Input Ports dans
application/port/input/(controllers REST)
Avantages :
- ✅ Clarté maximale
- ✅ Respect strict du DDD
- ✅ Tests d’architecture faciles
- ✅ Impossible de créer un output port dans
application/par erreur
Quand l’utiliser :
- Projets moyens/grands (10-50 use cases)
- Vous voulez une clarté maximale
- Le controller EST le port d’entrée (pas d’interface use case)
Comparaison
| Critère | Séparée | Centralisée Domain | Centralisée Unique |
|---|---|---|---|
| Clarté | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Simplicité | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| Respect DDD | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| Découplage | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| Nb fichiers | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
Recommandation : Commencez avec l’approche séparée (clarté maximale). Migrez vers centralisée domain si vous créez des interfaces use case.
Plusieurs Adapters pour le Même Port
Un des grands avantages de l’architecture hexagonale : on peut avoir plusieurs implémentations d’un même port.
Mock vs API
Plusieurs Implémentations
// domain/port/output/TrendRepository.java
public interface TrendRepository {
Optional<TrendResult> getTrends(TrendQuery query);
}
// infrastructure/adapter/MockTrendRepositoryAdapter.java
@Singleton
@Requires(property = "app.trend-source", value = "mock")
public class MockTrendRepositoryAdapter implements TrendRepository {
// Génère des données mockées
}
// infrastructure/adapter/GoogleTrendsApiAdapter.java
@Singleton
@Requires(property = "app.trend-source", value = "google-api")
public class GoogleTrendsApiAdapter implements TrendRepository {
// Appelle la vraie API Google Trends
}
// infrastructure/adapter/DatabaseTrendRepositoryAdapter.java
@Singleton
@Requires(property = "app.trend-source", value = "database")
public class DatabaseTrendRepositoryAdapter implements TrendRepository {
// Récupère depuis une BDD
}Bonnes Pratiques
Nommage des Ports
Output Ports
Output Ports (Interfaces)
Convention : Suffixe Repository ou Port
// ✅ BON
TrendRepository.java
NotificationPort.java
CachePort.java
EmailService.java
// ❌ MAUVAIS
TrendRepositoryImpl.java // "Impl" réservé aux adapters
TrendAdapter.java // Adapter = implémentation, pas interfaceÉviter les Types Techniques dans les Ports
❌ Mauvais
// ❌ MAUVAIS - Types techniques dans l'interface
public interface CountryRepository {
JsonNode getCountriesJson(); // Jackson
HttpResponse<String> fetchCountries(); // HTTP
ResponseEntity<List<Country>> findAll(); // Spring
}Problèmes :
- Le domain dépend de Jackson, HTTP, Spring
- Impossible de changer de framework sans casser le domain
Documentation avec Javadoc
/**
* Repository pour récupérer les tendances de recherche.
*
* Ce port de sortie abstrait la source de données (API, BDD, cache, etc.).
*/
public interface TrendRepository {
/**
* Récupère les tendances pour une requête donnée.
*
* @param query La requête contenant le mot-clé et la région
* @return Un Optional contenant le résultat, ou vide si aucune tendance trouvée
* @throws IllegalArgumentException si query est null ou invalide
*/
Optional<TrendResult> getTrends(TrendQuery query);
}Documenter les ports (interfaces) est crucial car ils définissent les contrats. Les adapters peuvent avoir moins de javadoc.
Checklist de Création d’un Port
Output Port
- L’interface est dans
domain/port/output/ - Elle utilise uniquement des types du domain (pas de Jackson, HTTP, etc.)
- Les méthodes sont documentées avec Javadoc
- Elle ne dépend d’aucun framework
- Elle peut être facilement mockée dans les tests
- Le nom est explicite (
TrendRepository,NotificationPort)
Input Port
- Le controller est dans
application/port/input/ - Il valide les paramètres d’entrée (@NotBlank, @Pattern)
- Il convertit les données HTTP → objets du domain
- Il appelle les use cases (pas de logique métier directement)
- Il gère les erreurs HTTP (404, 400, 500)
- Il convertit les objets du domain → DTOs de réponse
Résumé
Les ports définissent les contrats entre les couches. Les adapters fournissent les implémentations.
| Type de Port | Emplacement | Définit par | Implémenté par | Exemple |
|---|---|---|---|---|
| Output Port | domain/port/output/ | Domain (besoins) | Infrastructure (adapters) | TrendRepository |
| Input Port | application/port/input/ | Application (orchestration) | Application (controllers) | TrendController |
Règle d’or : Le domain définit ce dont il a besoin (output ports). L’application définit comment on y accède (input ports).
Prochaines étapes
Maintenant que vous savez créer des ports, apprenez à implémenter les adapters qui les utilisent.