Skip to Content
02 ArchitectureCouche Domain

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 :

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étier
  • port/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

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 personne

Rè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.

ConceptRôleExemple
EntitéObjet avec identitéUser, Order
Value ObjectObjet immutable sans identitéEmail, Money
Port OutputInterface vers l’extérieurUserRepository
Règles métierLogique businessorder.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.

Last updated on