Stratégies de Domain-Driven Design (DDD) pour des applications Spring Boot robustes
Les approches Domain-Driven Design (DDD) aident à structurer des applications complexes autour du domaine métier, en réduisant la dépendance à des modèles techniques instables. Pour des systèmes construits avec Spring Boot, DDD devient une méthode pragmatique pour améliorer la maintenabilité, la testabilité et la cohérence fonctionnelle.
1) Découper le domaine : Bounded Context, Ubiquitous Language et limites
Bounded Context et segmentation
La première stratégie consiste à identifier des Bounded Context (BC) où le langage et les règles métier sont cohérents. Chaque BC possède son modèle, ses invariants et son comportement. Cela évite le “modèle unique” qui se déforme avec la croissance.
Ubiquitous Language : aligner équipe et code
Le langage omniprésent doit être reflété dans les noms de classes, méthodes et événements. Lorsque le code diverge du vocabulaire métier, la dette s’accumule : ambiguïtés, mappings ad hoc et erreurs de compréhension.
Exemple de structure
Une structuration typique peut séparer les modules par BC :
src/
main/
java/
com/example/
ordering/ // Bounded Context: Commandes
domain/
application/
infrastructure/
billing/ // Bounded Context: Facturation
domain/
application/
infrastructure/
shared-kernel/ // si nécessaire : Concepts partagés stables
domain/
2) Modèle Domain : entités, value objects, agrégats et invariants
Entités et identités stables
Les entités portent une identité et évoluent au cours du temps. L’important n’est pas seulement l’ID technique, mais la capacité du modèle à exprimer un cycle de vie métier.
Value Objects : exactitude et immutabilité
Les Value Objects représentent des concepts dont l’identité dépend de leurs attributs. Leur immutabilité facilite l’égalité, la sérialisation et la sécurité des invariants.
Agrégats : cohérence transactionnelle
Les agrégats définissent un périmètre de cohérence : une seule racine d’agrégat garantit la validité des règles internes via des méthodes dédiées.
Exemple : Value Object et invariant
public record EmailAddress(String value) {
public EmailAddress {
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("Email invalide");
}
}
}
Exemple : Agrégat avec méthodes métier
public class Order {
private final OrderId id;
private final List lines = new ArrayList<>();
private OrderStatus status;
public Order(OrderId id) {
this.id = id;
this.status = OrderStatus.DRAFT;
}
public void addLine(ProductId productId, Money unitPrice, int quantity) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Ajout impossible après validation");
}
lines.add(new OrderLine(productId, unitPrice, quantity));
}
public void submit() {
if (lines.isEmpty()) {
throw new IllegalStateException("Une commande doit contenir au moins une ligne");
}
this.status = OrderStatus.SUBMITTED;
}
}
3) Application Layer : orchestration sans logique métier lourde
La couche application coordonne les cas d’usage. Elle appelle des services de domaine, orchestre les transactions et gère la communication inter-agrégats via des ports (interfaces).
Pourquoi garder le domaine “silencieux” ?
Le domaine doit rester indépendant des détails d’infrastructure (JPA, HTTP, files). Cette séparation réduit fortement le couplage et améliore les tests unitaires.
Exemple : Commande (Use Case) en Spring Boot
@Service
public class SubmitOrderUseCase {
private final OrderRepository orderRepository;
public SubmitOrderUseCase(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Transactional
public void execute(OrderId orderId) {
Order order = orderRepository.getById(orderId);
order.submit();
orderRepository.save(order);
}
}
4) Repositories : définir des ports orientés domaine
Repositories comme abstractions
Un Repository de domaine décrit des opérations nécessaires aux cas d’usage. L’implémentation concrète dépend de l’infrastructure (ex. JPA).
Exemple : interface domain repository
public interface OrderRepository {
Order getById(OrderId id);
void save(Order order);
}
Implémentation JPA isolée dans l’infrastructure
@Repository
class JpaOrderRepository implements OrderRepository {
private final SpringDataOrderJpaRepository repo;
@Override
public Order getById(OrderId id) {
// mapping Entity <--> Domain
return repo.findById(id.value())
.map(this::toDomain)
.orElseThrow(() -> new EntityNotFoundException("Order introuvable"));
}
@Override
public void save(Order order) {
// mapping Domain --> Entity
repo.save(toEntity(order));
}
}
5) Ports & Adapters : découpler l’infrastructure du modèle
Dans un cadre DDD, la communication avec l’extérieur se fait via des ports (interfaces) et des adapters (implémentations). Les ports rendent le domaine indépendant et permettent de simuler facilement les intégrations.
Exemple : port de notification
public interface NotificationService {
void notifyOrderSubmitted(OrderId orderId);
}
L’adapter peut ensuite utiliser un mécanisme concret (email, SMS, événement).
@Service
class EmailNotificationAdapter implements NotificationService {
@Override
public void notifyOrderSubmitted(OrderId orderId) {
// Appel SMTP / provider / etc.
}
}
6) Gestion des transactions : cohérence et limites d’agrégation
Une règle clé : les transactions doivent protéger la cohérence de l’agrégat. Les opérations inter-agrégats nécessitent souvent des stratégies événementielles ou des orchestrations de niveau application.
Stratégie recommandée
- Transactional côté cas d’usage, autour de la racine d’agrégat.
- Éviter les transactions qui traversent plusieurs BC.
- Utiliser des événements de domaine pour déclencher des traitements asynchrones.
7) Domain Events : découpler sans perdre l’expressivité
Les domain events permettent d’annoncer un fait métier (“commande soumise”) sans imposer la réaction immédiate. Les listeners peuvent appartenir au même BC ou à un BC différent (via messaging).
Exemple : événement de domaine
public record OrderSubmittedEvent(OrderId orderId, Instant occurredAt) {}
Publier depuis le domaine (option pragmatique)
Selon l’architecture retenue, l’événement peut être collecté par l’agrégat ou déclenché par l’application. L’objectif reste d’éviter l’infrastructure dans le domaine.
8) Stratégies de lecture : CQRS léger et modèles de projection
Pour les vues complexes, une séparation lecture/écriture (souvent CQRS) peut réduire la complexité du modèle. Les projections (read models) peuvent être nourries par événements, laissant le modèle d’écriture plus pur.
Exemple : projection pour le back-office
@Entity
class OrderSummaryView {
@Id String id;
String customerName;
String status;
BigDecimal total;
}
Les projections peuvent être mises à jour par un consumer d’événements, sans réutiliser les entités JPA côté domaine.
9) Éviter les pièges fréquents en DDD avec Spring Boot
Piège 1 : Anémie du modèle
Lorsque le domaine ne contient presque aucune règle et que toute la logique vit dans les services “utilitaires”, DDD devient un simple découpage en packages. La solution consiste à introduire des méthodes métier et des invariants au bon endroit.
Piège 2 : Exposer les entités JPA
Le retour direct d’entités JPA dans les API entraîne une fuite de détails d’infrastructure. Une couche de mapping vers des DTO ou des read models maintient la stabilité.
Piège 3 : Sur-découpage prématuré
Trop de Bounded Contexts ou trop de microservices peut ralentir l’itération. Une approche progressive (d’abord découper le domaine, puis évoluer vers l’asynchronisme) est souvent plus rentable.
10) Checklist de conception pour une application robuste
- Modèle d’abord : entités, value objects, agrégats, invariants.
- Application layer : orchestration des cas d’usage, pas de règles de domaine.
- Repositories comme ports : interfaces orientées domaine.
- Infrastructure isolée : adaptateurs JPA, clients externes, messaging.
- Événements : décorréler les réactions et autoriser l’asynchronisme.
- Lecture optimisée : projections CQRS légères quand nécessaire.
- Ubiquitous Language : alignement continu entre métier et code.
En appliquant ces stratégies, les applications Spring Boot gagnent en robustesse : le modèle devient plus stable, les changements fonctionnels deviennent plus sûrs, et la complexité se répartit mieux entre couches.
Conclusion
Les principes DDD (Bounded Context, agrégats, invariants, ports & adapters, events) forment un ensemble cohérent. Dans un projet Spring Boot, le succès dépend surtout de la discipline de séparation : le domaine exprime la vérité métier, l’application orchestre, l’infrastructure exécute. Cette séparation réduit le couplage, facilite l’évolution et améliore la fiabilité à long terme.
À 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