Retour aux articles

Implémenter le Domain-Driven Design (DDD) pour des applications métier Spring Boot robustes

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

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

© 2026 Laty Gueye Samba.