Implémenter le Domain-Driven Design (DDD) pour des applications métier Spring Boot robustes
Le Domain-Driven Design (DDD) vise à aligner la conception logicielle sur la compréhension du métier. Dans un contexte Spring Boot, DDD permet de construire des applications plus robustes, maintenables et testables, en réduisant la dépendance excessive à la structure technique au profit du modèle métier.
Pourquoi adopter le DDD dans des applications Spring Boot
Les applications métier échouent fréquemment lorsque les règles dominantes du domaine se retrouvent dispersées dans les contrôleurs, les services applicatifs ou les entités de persistence. DDD propose une stratégie claire : isoler le cœur métier, expliciter le langage commun, et structurer le code autour des bounded contexts.
Objectifs principaux
Renforcer la cohérence du modèle, faciliter l’évolution des règles métier, et améliorer la testabilité grâce à des unités de code proches du domaine.
Les briques conceptuelles essentielles du DDD
Ubiquitous Language (langage omniprésent)
Un langage commun entre experts métier et développeurs réduit les ambiguïtés. Les classes, méthodes et invariants doivent refléter ce vocabulaire : Commande, Facture, Contrat, Remboursement, etc.
Bounded Context
Un bounded context délimite un sous-domaine où le modèle est cohérent. Un même terme peut avoir des significations différentes selon les contextes ; DDD évite de fusionner ces règles.
Entités, Value Objects, Agrégats
- Entité : identité persistante (ex. Commande), suit un cycle de vie.
- Value Object : pas d’identité, valeur immuable (ex. Montant, Email).
- Agrégat : groupe d’objets avec un racine d’agrégat ; garantit les invariants.
Domain Events
Les événements de domaine permettent de découpler la réaction aux changements d’état. Exemple : CommandePayee déclenche la facturation dans un autre composant.
Stratégie d’architecture Spring Boot orientée DDD
Une approche courante consiste à appliquer une architecture hexagonale (ports & adapters) ou un découpage en couches DDD (application, domaine, infrastructure). Le domaine reste au centre : il ne dépend pas de Spring ni des technologies externes.
Découpage recommandé des packages
com.example
└── sales
├── domain
│ ├── model
│ │ ├── aggregate
│ │ ├── entity
│ │ └── valueobject
│ ├── service
│ └── event
├── application
│ ├── command
│ ├── usecase
│ └── handler
├── infrastructure
│ ├── persistence
│ ├── mapper
│ └── messaging
└── api
└── rest
Modéliser le domaine : entités, value objects et invariants
Value Object immuable avec validation
public final class Montant {
private final BigDecimal value;
public Montant(BigDecimal value) {
if (value == null || value.signum() < 0) {
throw new IllegalArgumentException("Montant invalide");
}
this.value = value;
}
public BigDecimal asBigDecimal() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Montant)) return false;
Montant other = (Montant) o;
return value.compareTo(other.value) == 0;
}
@Override
public int hashCode() {
return value.stripTrailingZeros().hashCode();
}
}
Entité et racine d’agrégat avec invariants
public class Commande {
private final CommandeId id;
private EtatCommande etat;
private Montant montant;
public Commande(CommandeId id, Montant montant) {
this.id = id;
this.montant = montant;
this.etat = EtatCommande.CREEE;
}
public void marquerCommePayee() {
if (etat != EtatCommande.CREEE) {
throw new IllegalStateException("La commande doit être créée pour être payée.");
}
this.etat = EtatCommande.PAYEE;
}
public CommandeId id() {
return id;
}
}
Les invariants sont centralisés dans le modèle. Les contrôleurs et services applicatifs orchestrent, mais ne décident pas des règles fines : cette responsabilité appartient à l’agrégat et aux objets du domaine.
Application layer : cas d’usage et orchestration
La couche application coordonne les flux de commande, garantit la transaction et invoque le modèle. Elle convertit les entrées (requêtes) en intentions métier (commands) et gère les sorties (réponses).
Command et handler (exemple simplifié)
public record MarquerCommandePayee(
String commandeId
) {}
@Service
public class MarquerCommandePayeeHandler {
private final CommandeRepository commandeRepository;
private final EventPublisher eventPublisher;
@Transactional
public void handle(MarquerCommandePayee command) {
CommandeId id = new CommandeId(command.commandeId());
Commande commande = commandeRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Commande introuvable"));
commande.marquerCommePayee();
commandeRepository.save(commande);
eventPublisher.publish(new CommandePayee(commande.id()));
}
}
Ici, la logique métier réside dans Commande, tandis que le handler assure la cohérence transactionnelle et la publication d’un événement.
Repository : ports et implémentations infrastructure
Port de persistance (interface domaine)
public interface CommandeRepository {
java.util.Optional<Commande> findById(CommandeId id);
void save(Commande commande);
}
Adapter JPA (infrastructure)
@Repository
public class JpaCommandeRepository implements CommandeRepository {
private final SpringDataCommandeJpaRepository springRepo;
private final CommandeMapper mapper;
public JpaCommandeRepository(SpringDataCommandeJpaRepository springRepo, CommandeMapper mapper) {
this.springRepo = springRepo;
this.mapper = mapper;
}
@Override
public Optional<Commande> findById(CommandeId id) {
return springRepo.findById(id.value())
.map(mapper::toDomain);
}
@Override
public void save(Commande commande) {
springRepo.save(mapper.toEntity(commande));
}
}
Les mappers évitent que la structure JPA impose sa forme au modèle. L’infrastructure devient remplaçable sans réécrire le domaine.
Gestion des Domain Events : cohérence et découplage
Les Domain Events facilitent l’évolution du système en découplant les réactions. Selon la criticité, une publication synchrone ou asynchrone peut être adoptée (ex. après commit transactionnel).
Exemple d’événement de domaine
public record CommandePayee(CommandeId commandeId) {}
Relais côté application (publisher abstrait)
public interface EventPublisher {
void publish(Object event);
}
L’adapter infrastructure peut intégrer une file (Kafka/RabbitMQ) ou un bus interne Spring. L’important est que le domaine n’en dépende pas.
Erreurs, exceptions et messages métier
Les invariants doivent provoquer des exceptions contrôlées au niveau domaine/application. La couche API traduit ces erreurs vers des codes HTTP et des messages stables.
Exception liée à l’invariant
public class InvariantViolationException extends RuntimeException {
public InvariantViolationException(String message) {
super(message);
}
}
Exemple de stratégie : les contrôleurs n’effectuent pas de validation métier profonde ; ils relaient les erreurs et structurent la réponse.
Contrats API : DTO et conversion explicite
Un DTO ne doit pas devenir l’objet du domaine. Les contrôleurs reçoivent des entrées typées, puis déclenchent des commands. Une conversion claire réduit l’effet domino lors des changements de modèle.
Contrôleur REST (exemple)
@RestController
@RequestMapping("/api/commandes")
public class CommandeController {
private final MarquerCommandePayeeHandler handler;
public CommandeController(MarquerCommandePayeeHandler handler) {
this.handler = handler;
}
@PostMapping("/{commandeId}/payer")
public ResponseEntity<Void> payer(@PathVariable String commandeId) {
handler.handle(new MarquerCommandePayee(commandeId));
return ResponseEntity.ok().build();
}
}
Pratiques de mise en œuvre pour un DDD robuste
- Commencer par un bounded context : limiter la portée pour valider la démarche sur un périmètre stable.
- Favoriser les tests domaine : vérifier les invariants via des tests unitaires orientés modèle.
- Documenter les décisions : règles, agrégats, limites de contextes, stratégie d’événements.
- Réduire les dépendances du domaine : aucune référence à Spring, JPA, ou frameworks.
- Utiliser des transactions avec soin : garantir la cohérence sans étendre excessivement les périmètres transactionnels.
Checklist de conformité DDD (Spring Boot)
- Le domaine ne dépend d’aucune infrastructure.
- Les agrégats contrôlent les invariants.
- L’application orchestre les cas d’usage via commands/handlers.
- Les repositories sont des ports, implémentés par des adapters.
- Les événements de domaine découplent les réactions.
- Les API utilisent des DTO et déclenchent des intents métier.
Conclusion
Implémenter le DDD pour Spring Boot revient à traiter le modèle métier comme un composant central. En isolant le domaine, en définissant clairement les agrégats et bounded contexts, et en structurant l’application autour des commands et événements, les applications deviennent plus résilientes aux changements.
À propos de l'auteur
Laty Gueye Samba est développeur Full Stack basé à Dakar, Sénégal. Spécialiste des écosystèmes Java / Spring Boot et Angular.
Contact : latygueyesamba@gmail.com | Dakar, Sénégal