Retour aux articles

Stratégies de Domain-Driven Design (DDD) pour des applications Spring Boot robustes

Stratégies de Domain-Driven Design (DDD) pour des applications Spring Boot robustes | Laty Gueye Samba - Développeur Full Stack Dakar Sénégal, Expert Java Spring Boot Angular
```html

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