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 :
Orchestration
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
- Recevoir les requêtes HTTP
- Valider les paramètres d’entrée
- Convertir DTO → Domain
- Appeler le use case
- Convertir Domain → DTO
- 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={}®ion={}", 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®ion=FRController 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); // ValidationUse 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érique2. Nommage Explicite
// ✅ BON : Verbe d'action
GetTrendsUseCase
CreateOrderUseCase
SendEmailUseCase
// ❌ MAUVAIS : Nom vague
TrendUseCase
OrderManager3. 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.
| Composant | Rôle | Exemple |
|---|---|---|
| Use Case | Orchestrer la logique métier | GetTrendsUseCase |
| Controller | Point d’entrée HTTP | TrendController |
| DTO Request | Recevoir les données HTTP | CreateUserRequestDto |
| DTO Response | Renvoyer les données JSON | TrendResponseDto |
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.