Skip to Content
02 ArchitectureCouche Application

Couche Application

La couche Application orchestre la logique métier du domain via les Use Cases.

La couche Application est le chef d’orchestre : elle coordonne le domain sans contenir de logique métier.


Rôle de la Couche Application

La couche Application a 3 responsabilités principales :

1. Orchestration des Use Cases

  • Appeler les entités du domain
  • Coordonner plusieurs ports de sortie
  • Gérer les transactions (si nécessaire)

Exemple :

@Singleton public class CreateOrderUseCase { private final OrderRepository orderRepository; private final EmailService emailService; public Order execute(CreateOrderCommand command) { // 1. Créer l'entité domain Order order = new Order(command.items()); // 2. Valider order.validate(); // 3. Sauvegarder orderRepository.save(order); // 4. Envoyer notification emailService.sendOrderConfirmation(order); return order; } }

CE QUE LA COUCHE APPLICATION NE FAIT PAS :

  • ❌ Pas de logique métier (c’est dans le domain)
  • ❌ Pas d’implémentation technique (c’est dans l’infrastructure)

Structure de la Couche Application

      • GetTrendsUseCase.java
      • CreateUserUseCase.java
      • UpdateUserUseCase.java

Organisation :

  • port/input/ = Controllers REST (points d’entrée)
  • usecase/ = Use Cases (orchestration du métier)
  • dto/ = DTOs pour l’API (conversion Domain ↔ JSON)

Use Cases

Les Use Cases représentent les actions métier de l’application.

Anatomie d’un Use Case

Injection des Dépendances

@Singleton @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) public class GetTrendsUseCase { TrendRepository trendRepository; // Port de sortie }

Méthode Execute

public Optional<TrendResult> execute(String keyword, String region) { // 1. Créer les objets domain TrendQuery query = new TrendQuery(keyword, region); // 2. Appeler le port de sortie return trendRepository.getTrends(query); }

Gestion des Erreurs

public TrendResult execute(String keyword, String region) { TrendQuery query = new TrendQuery(keyword, region); return trendRepository.getTrends(query) .orElseThrow(() -> new TrendNotFoundException(keyword, region)); }

Exemple Complet

@Singleton @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) public class GetTrendsUseCase { TrendRepository trendRepository; Logger logger; /** * Récupère les tendances pour un mot-clé et une région. * * @param keyword Le mot-clé recherché * @param region Le code région (ex: "US", "FR") * @return Le résultat des tendances * @throws TrendNotFoundException Si aucune tendance n'est trouvée */ public TrendResult execute(String keyword, String region) { logger.info("Getting trends for keyword={}, region={}", keyword, region); // Créer l'objet domain (validation incluse) TrendQuery query = new TrendQuery(keyword, region); // Appeler le port de sortie TrendResult result = trendRepository.getTrends(query) .orElseThrow(() -> new TrendNotFoundException(keyword, region)); logger.info("Found trend with score={}", result.getInterestScore()); return result; } }

Controllers (Ports d’Entrée)

Les Controllers sont les points d’entrée de l’application (API REST).

Responsabilités des Controllers

  1. Recevoir les requêtes HTTP
  2. Valider les paramètres d’entrée
  3. Convertir DTO → Domain
  4. Appeler le use case
  5. Convertir Domain → DTO
  6. Retourner la réponse HTTP

Exemple Complet

@Controller("/api/trends") @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) public class TrendController { GetTrendsUseCase getTrendsUseCase; Logger logger; @Get public TrendResponseDto getTrends( @QueryValue @NotBlank String keyword, @QueryValue(defaultValue = "US") String region ) { logger.info("GET /api/trends?keyword={}&region={}", keyword, region); // Appeler le use case (objets domain) TrendResult result = getTrendsUseCase.execute(keyword, region); // Convertir Domain → DTO return TrendResponseDto.fromDomain(result); } @Get("/popular") public List<TrendResponseDto> getPopularTrends() { // Logique pour récupérer les tendances populaires List<TrendResult> results = // ... use case return results.stream() .map(TrendResponseDto::fromDomain) .collect(Collectors.toList()); } }

Gestion des Exceptions

@Controller("/api/users") public class UserController { private final GetUserUseCase getUserUseCase; @Get("/{id}") public UserResponseDto getUser(Long id) { try { User user = getUserUseCase.execute(id); return UserResponseDto.fromDomain(user); } catch (UserNotFoundException e) { throw new HttpStatusException( HttpStatus.NOT_FOUND, "User not found: " + id ); } } }

DTOs (Data Transfer Objects)

Les DTOs convertissent entre le domain et l’API REST.

DTO de Requête (Input)

@Data public class CreateUserRequestDto { @NotBlank private String name; @Email private String email; @NotNull private Integer age; }

DTO de Réponse (Output)

@Data public class TrendResponseDto { private String keyword; private String region; private Integer interestScore; private String popularityLevel; private LocalDateTime queriedAt; /** * Convertit une entité domain en DTO. */ public static TrendResponseDto fromDomain(TrendResult result) { TrendResponseDto dto = new TrendResponseDto(); dto.setKeyword(result.getKeyword()); dto.setRegion(result.getRegion()); dto.setInterestScore(result.getInterestScore()); dto.setPopularityLevel(result.getPopularityLevel()); // Règle métier dto.setQueriedAt(result.getQueriedAt()); return dto; } }

Méthode statique fromDomain() : Convention pour convertir Domain → DTO de manière explicite et lisible.


Flux Complet : HTTP → Domain → HTTP

Voyons le flux complet d’une requête HTTP.

Requête HTTP arrive

GET /api/trends?keyword=java&region=FR

Controller reçoit et valide

@Get public TrendResponseDto getTrends( @QueryValue @NotBlank String keyword, @QueryValue(defaultValue = "US") String region ) { // keyword = "java", region = "FR"

Controller appelle le Use Case

TrendResult result = getTrendsUseCase.execute(keyword, region);

Use Case crée les objets Domain

public TrendResult execute(String keyword, String region) { TrendQuery query = new TrendQuery(keyword, region); // Validation

Use Case appelle le Port de Sortie

return trendRepository.getTrends(query) .orElseThrow(() -> new TrendNotFoundException(keyword, region)); }

Controller convertit Domain → DTO

return TrendResponseDto.fromDomain(result); }

Réponse JSON renvoyée

{ "keyword": "java", "region": "FR", "interestScore": 85, "popularityLevel": "HIGH", "queriedAt": "2025-01-15T10:30:00" }

Use Case Complexe : Orchestration Multiple

Exemple d’un use case qui coordonne plusieurs ports.

@Singleton @RequiredArgsConstructor public class CreateOrderUseCase { private final UserRepository userRepository; private final ProductRepository productRepository; private final OrderRepository orderRepository; private final EmailService emailService; private final PaymentService paymentService; @Transactional public Order execute(CreateOrderCommand command) { // 1. Récupérer l'utilisateur User user = userRepository.findById(command.getUserId()) .orElseThrow(() -> new UserNotFoundException(command.getUserId())); // 2. Vérifier les produits List<Product> products = command.getProductIds().stream() .map(id -> productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException(id))) .collect(Collectors.toList()); // 3. Créer la commande (logique métier dans le domain) Order order = Order.create(user, products); order.validate(); // 4. Traiter le paiement Payment payment = paymentService.processPayment( order.getTotalAmount(), command.getPaymentMethod() ); if (!payment.isSuccessful()) { throw new PaymentFailedException(); } // 5. Sauvegarder la commande Order savedOrder = orderRepository.save(order); // 6. Envoyer confirmation email emailService.sendOrderConfirmation(user.getEmail(), savedOrder); return savedOrder; } }

Le use case orchestre plusieurs ports de sortie, mais la logique métier reste dans le domain (Order.create(), order.validate()).


Bonnes Pratiques

1. Un Use Case = Une Action Métier

// ✅ BON : Use cases spécifiques GetTrendsUseCase CreateUserUseCase UpdateUserUseCase DeleteUserUseCase // ❌ MAUVAIS : Use case fourre-tout UserService // Trop générique

2. Nommage Explicite

// ✅ BON : Verbe d'action GetTrendsUseCase CreateOrderUseCase SendEmailUseCase // ❌ MAUVAIS : Nom vague TrendUseCase OrderManager

3. Use Case Testable

@Test void shouldGetTrendsWhenKeywordExists() { // ARRANGE TrendRepository mockRepo = mock(TrendRepository.class); TrendResult mockResult = TrendResult.builder() .keyword("java") .interestScore(85) .build(); when(mockRepo.getTrends(any())).thenReturn(Optional.of(mockResult)); GetTrendsUseCase useCase = new GetTrendsUseCase(mockRepo); // ACT TrendResult result = useCase.execute("java", "US"); // ASSERT assertThat(result.getKeyword()).isEqualTo("java"); assertThat(result.getInterestScore()).isEqualTo(85); }

Checklist Couche Application

Pour vérifier que votre couche Application est bien conçue :

  • Les use cases ont un nom explicite (verbe d’action)
  • Les use cases sont @Singleton
  • Les use cases n’ont PAS de logique métier (c’est dans le domain)
  • Les controllers valident les paramètres d’entrée (@NotBlank, @Valid)
  • Les controllers convertissent DTO ↔ Domain
  • Les DTOs ont une méthode fromDomain() statique
  • Les use cases sont testables sans infrastructure

Résumé

La couche Application orchestre le domain et expose les use cases via des controllers REST.

ComposantRôleExemple
Use CaseOrchestrer la logique métierGetTrendsUseCase
ControllerPoint d’entrée HTTPTrendController
DTO RequestRecevoir les données HTTPCreateUserRequestDto
DTO ResponseRenvoyer les données JSONTrendResponseDto

Règle d’or : Orchestration dans l’application, logique métier dans le domain.


Prochaines étapes

Maintenant que vous comprenez l’orchestration, explorez comment l’infrastructure implémente les ports de sortie.

Last updated on