Skip to Content
03 ImplementationGuide de Création des Ports

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.

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

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 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&region=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 (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èreSéparéeCentralisée DomainCentralisé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.

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 (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 - 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 PortEmplacementDéfinit parImplémenté parExemple
Output Portdomain/port/output/Domain (besoins)Infrastructure (adapters)TrendRepository
Input Portapplication/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.

Last updated on