Couche Domain
La couche Domain contient le cœur métier de l’application. C’est le cerveau de votre système.
Règle d’or : Le domain est PUR et ne dépend de RIEN. C’est la couche la plus importante et la plus protégée.
Principe Fondamental
Le domain doit être totalement indépendant de toute technologie :
❌ Interdit
Dépendances interdites
- ❌ Frameworks (Micronaut, Spring, Jakarta)
- ❌ Base de données (JPA, Hibernate, JDBC)
- ❌ APIs externes (Jackson, HTTP clients)
- ❌ Infrastructure technique
- ❌ Couche application
Pourquoi ? Pour pouvoir tester, maintenir et faire évoluer le métier sans dépendre de la technique.
Structure du Domain
Organisation :
model/= Entités, Value Objects, règles métierport/output/= Interfaces définissant les besoins du domain
Entités (Entities)
Les Entités sont des objets métier avec une identité unique.
Caractéristiques
- Ont un identifiant (ID)
- Peuvent changer d’état dans le temps
- Identité persiste même si les attributs changent
Exemple : Entité User
@Value
public class User {
Long id; // ← Identité unique
String name;
Email email;
LocalDateTime createdAt;
public User withName(String newName) {
return new User(id, newName, email, createdAt);
}
}Deux User avec le même id sont la même personne, même si leur nom est différent.
Exemple : Entité TrendResult
@Value
@Builder
public class TrendResult {
String keyword;
String region;
Integer interestScore;
List<RelatedTopic> relatedTopics;
LocalDateTime queriedAt;
public boolean isPopular() {
return interestScore > 70;
}
public boolean isRecent() {
return queriedAt.isAfter(LocalDateTime.now().minusHours(24));
}
}Value Objects
Les Value Objects sont des objets immuables sans identité.
Caractéristiques
- Immuables (ne changent jamais)
- Pas d’ID (identité = valeur)
- Deux VO avec les mêmes valeurs sont identiques
Exemple : Email
@Value
public class Email {
String value;
public Email(String value) {
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("Email invalide: " + value);
}
if (value.length() > 255) {
throw new IllegalArgumentException("Email trop long");
}
this.value = value.toLowerCase().trim();
}
}Validation dans le constructeur : Un Value Object ne peut jamais être dans un état invalide !
Exemple : TrendQuery
@Value
public class TrendQuery {
String keyword;
String region;
LocalDateTime timestamp;
public TrendQuery(String keyword, String region) {
if (keyword == null || keyword.isBlank()) {
throw new IllegalArgumentException("Keyword cannot be empty");
}
if (!region.matches("^[A-Z]{2}$")) {
throw new IllegalArgumentException("Region must be 2 uppercase letters");
}
this.keyword = keyword.trim();
this.region = region.toUpperCase();
this.timestamp = LocalDateTime.now();
}
}Entités vs Value Objects
Entités
Entités
Identité unique :
User user1 = new User(1L, "Alice", email1, now);
User user2 = new User(1L, "Bob", email2, now);
// user1.equals(user2) → TRUE (même ID)Peut changer :
User updated = user1.withName("Alice Smith");
// updated.getId() == user1.getId() → TRUE
// C'est toujours la même personneRègles Métier dans le Domain
Le domain contient la logique métier pure.
Exemple : Validation Métier
@Value
public class Order {
Long id;
List<OrderLine> lines;
OrderStatus status;
BigDecimal totalAmount;
public Order validate() {
if (lines.isEmpty()) {
throw new BusinessException("Order must have at least one line");
}
if (totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException("Total amount must be positive");
}
return this;
}
public boolean canBeCancelled() {
return status == OrderStatus.PENDING
|| status == OrderStatus.CONFIRMED;
}
public Order cancel() {
if (!canBeCancelled()) {
throw new BusinessException(
"Cannot cancel order with status: " + status
);
}
return new Order(id, lines, OrderStatus.CANCELLED, totalAmount);
}
}Les règles métier sont encapsulées dans le domain, pas éparpillées dans les controllers ou services !
Ports de Sortie (Output Ports)
Les Output Ports sont des interfaces qui définissent les besoins du domain.
Principe
Le domain dit : “J’ai besoin de sauvegarder des utilisateurs” L’infrastructure répond : “Je sais comment le faire avec PostgreSQL / MongoDB / etc.”
Exemple : TrendRepository
// 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);
}Exemple : UserRepository
public interface UserRepository {
Optional<User> findById(Long id);
Optional<User> findByEmail(Email email);
void save(User user);
void delete(Long id);
}Le domain définit CE DONT il a besoin, pas COMMENT c’est implémenté.
Exemple Complet : Domain TrendResult
Voyons un exemple complet avec entité, value object, et règles métier.
Value Object : TrendQuery
@Value
public class TrendQuery {
String keyword;
String region;
LocalDateTime timestamp;
public TrendQuery(String keyword, String region) {
validateKeyword(keyword);
validateRegion(region);
this.keyword = keyword.trim();
this.region = region.toUpperCase();
this.timestamp = LocalDateTime.now();
}
private void validateKeyword(String keyword) {
if (keyword == null || keyword.isBlank()) {
throw new IllegalArgumentException("Keyword cannot be empty");
}
if (keyword.length() > 100) {
throw new IllegalArgumentException("Keyword too long");
}
}
private void validateRegion(String region) {
if (!region.matches("^[A-Z]{2}$")) {
throw new IllegalArgumentException(
"Region must be 2 uppercase letters"
);
}
}
}Entité : TrendResult
@Value
@Builder
public class TrendResult {
String keyword;
String region;
Integer interestScore;
List<RelatedTopic> relatedTopics;
LocalDateTime queriedAt;
// Règles métier
public boolean isPopular() {
return interestScore > 70;
}
public boolean isVeryPopular() {
return interestScore > 90;
}
public boolean isRecent() {
return queriedAt.isAfter(
LocalDateTime.now().minusHours(24)
);
}
public String getPopularityLevel() {
if (interestScore >= 90) return "VERY_HIGH";
if (interestScore >= 70) return "HIGH";
if (interestScore >= 40) return "MEDIUM";
return "LOW";
}
}Port de Sortie : TrendRepository
public interface TrendRepository {
Optional<TrendResult> getTrends(TrendQuery query);
}Bonnes Pratiques
1. Validation dans les Constructeurs
@Value
public class Price {
BigDecimal amount;
String currency;
public Price(BigDecimal amount, String currency) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
if (currency == null || currency.length() != 3) {
throw new IllegalArgumentException("Invalid currency code");
}
this.amount = amount;
this.currency = currency.toUpperCase();
}
}2. Méthodes Métier Expressives
@Value
public class ShoppingCart {
List<CartItem> items;
public BigDecimal calculateTotal() {
return items.stream()
.map(CartItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public boolean isEmpty() {
return items.isEmpty();
}
public boolean hasItem(Long productId) {
return items.stream()
.anyMatch(item -> item.getProductId().equals(productId));
}
}3. Immutabilité avec @Value
@Value // Génère : final class, private final fields, getters, equals, hashCode
public class Address {
String street;
String city;
String zipCode;
String country;
}Checklist Domain Layer
Pour vérifier que votre domain est bien conçu :
- Le domain n’importe AUCUNE classe d’infrastructure
- Pas d’annotations framework (@Entity, @Table, @JsonProperty)
- Les entités ont une identité (ID)
- Les value objects sont immuables (@Value)
- Les règles métier sont dans le domain
- La validation est dans les constructeurs
- Les ports de sortie sont des interfaces
- Le domain est testable sans infrastructure
Résumé
Le domain est le cœur protégé de votre application. Il contient la logique métier pure et ne dépend de rien.
| Concept | Rôle | Exemple |
|---|---|---|
| Entité | Objet avec identité | User, Order |
| Value Object | Objet immutable sans identité | Email, Money |
| Port Output | Interface vers l’extérieur | UserRepository |
| Règles métier | Logique business | order.canBeCancelled() |
Principe clé : Le domain définit CE QU’IL VEUT, l’infrastructure fournit COMMENT.
Prochaines étapes
Maintenant que le domain est clair, explorez comment l’orchestrer avec la couche Application.