Domain-Driven Design (DDD)

Comment appliquer le DDD pour modéliser des domaines complexes, définir des bounded contexts et construire un logiciel aligné sur le métier.

Quel problème cela résout

Dans les systèmes complexes, le plus grand défi n’est pas technique mais communicationnel. Les développeurs parlent une langue, les experts métier en parlent une autre, et le logiciel finit par être une traduction défaillante des deux. Le résultat : des modèles de données qui ne reflètent pas la réalité du métier, une logique dispersée dans des couches incorrectes et des changements qui exigent de modifier la moitié du système.

Le Domain-Driven Design attaque ce problème à la racine : il propose que le logiciel modélise directement le domaine métier, en utilisant le même langage que celui employé par les experts du domaine. Ce n’est ni un framework ni une bibliothèque, c’est une approche de conception qui place le domaine au centre de toutes les décisions architecturales.

Symptômes indiquant que vous avez besoin du DDD

  • L’équipe technique et l’équipe métier ne se comprennent pas
  • Les modèles de données sont des tables de base de données déguisées en objets
  • Un changement dans une règle métier exige de modifier plusieurs services
  • Il n’y a pas de frontières claires entre les responsabilités de chaque partie du système
  • La logique métier est dispersée entre contrôleurs, services et repositories
  • Les noms dans le code ne correspondent pas aux termes utilisés par le métier

Exemple concret du problème

Imaginez un système e-commerce où l’équipe métier parle de « commandes », « retours » et « remboursements », mais où le code comporte des classes comme DataProcessor, ItemHandler et TransactionManager. Lorsque le métier dit « nous devons changer la politique de retours », personne ne sait quels fichiers toucher parce qu’il n’existe ni classe Retour ni module Remboursements.

❌ Sans DDD — Code déconnecté du métier :
   src/
   ├── handlers/
   │   ├── DataProcessor.java
   │   ├── ItemHandler.java
   │   └── TransactionManager.java
   ├── models/
   │   ├── Record.java
   │   └── Entry.java
   └── utils/
       └── Helper.java

✅ Avec DDD — Code aligné sur le domaine :
   src/
   ├── commandes/
   │   ├── Commande.java
   │   ├── LigneDeCommande.java
   │   └── RepositoryCommandes.java
   ├── retours/
   │   ├── Retour.java
   │   ├── PolitiqueRetour.java
   │   └── ServiceRemboursement.java
   └── expeditions/
       ├── Expedition.java
       └── TransporteurAdapter.java

Comment cela fonctionne

Le DDD se divise en deux grands domaines : la conception stratégique (comment diviser le système) et la conception tactique (comment modéliser chaque partie).

Conception stratégique

Ubiquitous Language (Langage Omniprésent)

C’est le vocabulaire partagé entre les développeurs et les experts du domaine. Si le métier parle de « commandes », « expéditions » et « retours », le code doit employer exactement ces termes — pas « orders_table », « shipping_record » ou « return_entity ».

❌ Code sans langage omniprésent :
   class DataRecord { processItem() }

✅ Code avec langage omniprésent :
   class Commande { confirmerExpedition() }

Le langage omniprésent se documente, se maintient et s’utilise dans les conversations, le code, les tests et la documentation. Ce n’est pas qu’une simple convention de nommage — c’est un accord actif entre le métier et le développement qui évolue avec le domaine.

Comment construire le langage omniprésent :

  1. Organisez des sessions d’Event Storming avec des experts du domaine et des développeurs
  2. Identifiez les termes clés utilisés par le métier et documentez-les dans un glossaire partagé
  3. Utilisez ces termes exacts dans le code, les tests, la documentation et les conversations
  4. Lorsqu’une ambiguïté surgit (le même mot désigne des choses différentes), c’est le signe que vous avez besoin de bounded contexts séparés

Bounded Contexts (Contextes Délimités)

Un bounded context est une frontière explicite à l’intérieur de laquelle un modèle de domaine possède une signification cohérente. Le même mot peut désigner des choses différentes selon les contextes :

  • « Client » dans le contexte des Ventes : quelqu’un qui achète des produits
  • « Client » dans le contexte du Support : quelqu’un qui ouvre des tickets
  • « Client » dans le contexte de la Facturation : une entité dotée de données fiscales
┌─────────────────────┐  ┌─────────────────────┐
│  Contexte : Ventes  │  │  Contexte : Support │
│                     │  │                     │
│  Client             │  │  Client             │
│  ├─ nom             │  │  ├─ nom             │
│  ├─ historiqueAchat │  │  ├─ ticketsOuverts  │
│  └─ remise          │  │  └─ niveauPriorite  │
│                     │  │                     │
│  Commande           │  │  Ticket             │
│  ├─ lignes          │  │  ├─ description     │
│  └─ total           │  │  └─ etat            │
└─────────────────────┘  └─────────────────────┘

Chaque bounded context possède son propre modèle, sa propre base de données et sa propre équipe (idéalement). La communication entre contextes s’effectue à travers des interfaces bien définies.

Règles pratiques pour identifier les bounded contexts :

  • Si deux équipes ont besoin de modèles différents du même concept → contextes séparés
  • Si un changement dans une zone ne devrait pas affecter une autre → contextes séparés
  • Si le langage omniprésent change → vous franchissez une frontière de contexte
  • Un bounded context n’est pas nécessairement un microservice, mais il en est un candidat naturel

Context Mapping

Définit comment les bounded contexts se relient entre eux :

RelationDescriptionExemple
Shared KernelDeux contextes partagent un sous-ensemble du modèleTypes de devises partagés entre Ventes et Facturation
Customer-SupplierUn contexte fournit des données qu’un autre consommeL’Inventaire (supplier) fournit le stock aux Commandes (customer)
ConformistUn contexte s’adapte au modèle d’un autre sans négociationVotre système s’adapte au modèle de l’API d’un fournisseur externe
Anti-Corruption LayerUn contexte traduit le modèle externe dans son propre langageAdaptateur qui traduit les réponses d’un système legacy
Open Host ServiceUn contexte expose un protocole ouvert pour que d’autres le consommentAPI REST publique avec documentation OpenAPI
Published LanguageOn utilise un langage partagé documenté (JSON Schema, Protobuf)Contrats d’événements au format Avro

Conception tactique

Entities (Entités)

Objets dotés d’une identité unique qui persiste dans le temps. Deux entités ayant les mêmes attributs mais un ID différent sont différentes.

Commande #1234 ≠ Commande #5678
(même si toutes deux comportent les mêmes produits et le même total)

Les entités encapsulent du comportement, pas seulement des données. Une Commande n’est pas un DTO avec des getters et setters — elle possède des méthodes qui reflètent des opérations du domaine :

// ❌ Modèle anémique (anti-pattern)
commande.setEtat("confirmee");
commande.setDateConfirmation(new Date());

// ✅ Modèle riche avec comportement
commande.confirmer();  // valide les règles en interne et change l'état

Value Objects (Objets de Valeur)

Objets définis par leurs attributs, sans identité propre. Deux value objects ayant les mêmes attributs sont égaux. Ils sont immuables.

Adresse("Rue 1", "Ville A") == Adresse("Rue 1", "Ville A")
Argent(100, "USD") == Argent(100, "USD")

Les value objects sont plus courants qu’il n’y paraît. De nombreux concepts que nous modélisons sous forme de chaînes ou de nombres primitifs devraient être des value objects :

// ❌ Types primitifs
String email = "user@example.com";
double prix = 99.99;

// ✅ Value objects avec validation
Email email = new Email("user@example.com");  // valide le format
Argent prix = new Argent(99.99, "USD");       // évite de mélanger les devises

Aggregates (Agrégats)

Un cluster d’entités et de value objects traités comme une unité de cohérence. Chaque agrégat possède une racine (Aggregate Root) qui constitue l’unique point d’accès externe.

┌─────────────────────────────┐
│  Aggregate : Commande       │
│  ┌───────────────────────┐  │
│  │ Commande (Aggregate Root)│ │
│  │  ├─ id                │  │
│  │  ├─ etat              │  │
│  │  └─ dateCreation      │  │
│  └───────────────────────┘  │
│  ┌──────────┐ ┌──────────┐  │
│  │ LigneCmd │ │ LigneCmd │  │
│  │ produit  │ │ produit  │  │
│  │ quantite │ │ quantite │  │
│  │ prix     │ │ prix     │  │
│  └──────────┘ └──────────┘  │
│  ┌──────────────────────┐   │
│  │ AdresseExpedition(VO)│   │
│  └──────────────────────┘   │
└─────────────────────────────┘

Règles clés des agrégats :

  • Les transactions ne doivent modifier qu’un seul agrégat à la fois
  • Les références entre agrégats se font par ID, pas par référence directe
  • Si vous devez modifier plusieurs agrégats, utilisez des Domain Events
  • Gardez les agrégats petits — un agrégat volumineux est le signe de frontières incorrectes

Domain Events (Événements de Domaine)

Représentent quelque chose qui s’est produit dans le domaine. Ils s’expriment au passé et dans le langage du métier.

CommandeConfirmee { commandeId, date, total }
PaiementRecu { paiementId, commandeId, montant }
ExpeditionEnvoyee { expeditionId, commandeId, transporteur }

Les événements permettent aux bounded contexts de communiquer de façon découplée. Ils constituent la base de l’Event-Driven Architecture et du Saga Pattern pour coordonner les transactions distribuées.

Exemple de flux avec des événements :

Contexte Ventes :
  commande.confirmer()
  → publie CommandeConfirmee

Contexte Inventaire :
  écoute CommandeConfirmee
  → réserve le stock
  → publie StockReserve

Contexte Expéditions :
  écoute StockReserve
  → programme l'expédition
  → publie ExpeditionProgrammee

Repositories

Abstractions qui encapsulent l’accès aux données. Le domaine ne sait pas si les données proviennent d’une base de données, d’une API ou d’un fichier. Le repository parle le langage du domaine.

❌ commandeDAO.selectByStatusAndDate(status, date)
✅ repositoryCommandes.rechercherEnAttenteDepuis(date)

Domain Services (Services de Domaine)

Opérations qui n’appartiennent naturellement à aucune entité ni value object. Ils contiennent une logique métier qui implique plusieurs agrégats.

ServiceDePrix.calculerRemise(client, commande, promotion)
ServiceDExpedition.selectionnerTransporteur(commande, destination)

Exemple pratique : E-commerce avec DDD

Voyons comment appliquer le DDD à un système e-commerce réel :

Étape 1 : Identifier les bounded contexts

┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│    Catalogue     │  │    Commandes     │  │    Paiements     │
│                  │  │                  │  │                  │
│  Produit         │  │  Commande        │  │  Transaction     │
│  Categorie       │  │  LigneDeCommande │  │  MoyenDePaiement │
│  Prix            │  │  AdresseExpedit. │  │  Remboursement   │
│  Inventaire      │  │  EtatCommande    │  │  EtatPaiement    │
└────────┬────────┘  └────────┬────────┘  └────────┬────────┘
         │                    │                    │
         │    Evenements      │    Evenements      │
         └────────────────────┴────────────────────┘

Étape 2 : Définir les agrégats dans le contexte des Commandes

Aggregate Root: Commande
├── id: CommandeId (Value Object)
├── client: ClientId (référence par ID, pas par objet)
├── lignes: List<LigneDeCommande>
│   ├── produitId: ProduitId
│   ├── quantite: Quantite (Value Object, > 0)
│   └── prixUnitaire: Argent (Value Object)
├── adresse: AdresseExpedition (Value Object)
├── etat: EtatCommande (enum)
└── total(): Argent (calculé)

Invariants de l'agrégat :
- Une commande doit comporter au moins une ligne
- Le total ne peut jamais être négatif
- On ne peut confirmer qu'une commande à l'état « brouillon »
- On ne peut annuler qu'une commande qui n'a pas été expédiée

Étape 3 : Implémenter le comportement dans l’agrégat

class Commande {
  confirmer() {
    if (this.etat !== 'brouillon')
      throw new Error('On ne peut confirmer qu\'une commande en brouillon');
    if (this.lignes.length === 0)
      throw new Error('La commande doit comporter au moins une ligne');

    this.etat = 'confirmee';
    this.dateConfirmation = new Date();
    this.enregistrerEvenement(new CommandeConfirmee(this.id, this.total()));
  }

  annuler(motif) {
    if (this.etat === 'expediee' || this.etat === 'livree')
      throw new Error('Impossible d\'annuler une commande déjà expédiée');

    this.etat = 'annulee';
    this.motifAnnulation = motif;
    this.enregistrerEvenement(new CommandeAnnulee(this.id, motif));
  }
}

Avantages

  • Alignement métier-technologie : Le code reflète directement le domaine, réduisant l’écart de communication
  • Frontières claires : Les bounded contexts définissent des limites explicites qui facilitent l’évolution indépendante
  • Modèle riche : Les entités contiennent du comportement, pas seulement des données, évitant les modèles anémiques
  • Scalabilité organisationnelle : Chaque bounded context peut être développé par une équipe indépendante
  • Évolution maîtrisée : Les changements dans un contexte n’affectent pas les autres si les interfaces sont maintenues
  • Base pour les microservices : Les bounded contexts sont des candidats naturels pour devenir des microservices
  • Tests plus clairs : Les tests reflètent de vraies règles métier, pas des implémentations techniques

Compromis / Inconvénients

  • Courbe d’apprentissage élevée : Le DDD introduit de nombreux concepts qui demandent du temps à maîtriser
  • Surcharge dans les domaines simples : Pour des CRUD basiques, le DDD ajoute une complexité inutile
  • Nécessite l’accès aux experts du domaine : Sans collaboration constante avec le métier, le modèle dérive
  • Plus de code initial : Entités, value objects, repositories et événements exigent plus de code qu’une approche directe
  • Risque de sur-ingénierie : Il est facile d’appliquer tous les patterns tactiques quand vous n’en avez besoin que de quelques-uns
  • Duplication intentionnelle : Chaque bounded context peut avoir sa propre représentation de concepts similaires
  • Investissement en découverte : Les sessions d’Event Storming et de modélisation demandent du temps et une participation active du métier

Quand l’utiliser

  • Domaines complexes comportant de nombreuses règles métier
  • Systèmes qui évoluent fréquemment sous l’effet des changements du métier
  • Grandes équipes qui doivent travailler en parallèle avec des frontières claires
  • Projets où la communication entre le métier et le développement est critique
  • Systèmes en cours de migration d’un monolithe vers des microservices
  • Lorsque le coût d’un modèle incorrect est élevé (finance, santé, logistique)

Quand l’éviter

  • Applications CRUD simples sans logique métier significative
  • Prototypes ou MVP où la vitesse prime sur la structure
  • Petites équipes sans accès aux experts du domaine
  • Systèmes aux domaines bien compris et stables qui ne changent pas
  • Projets au budget ou au temps très limité pour la phase de conception

Technologies et implémentations courantes

CatégorieOptions
Frameworks DDDAxon Framework (Java), EventSourcing.NetCore (C#), Ecotone (PHP)
Event SourcingEventStoreDB, Axon Server, Marten (.NET)
CQRSMediatR (.NET), Axon (Java), implémentations sur mesure
MessagerieApache Kafka, RabbitMQ, Amazon EventBridge
ModélisationEvent Storming, Domain Storytelling, Context Mapping
PersistancePostgreSQL, MongoDB (par bounded context)

Relation avec d’autres patterns

Le DDD est le fondement conceptuel sur lequel se construisent de nombreux autres patterns d’architecture distribuée :

┌─────────────────────────────────────────────────┐
│              Domain-Driven Design                │
│         (Bounded Contexts + Ubiquitous Language) │
└──────────┬──────────┬──────────┬────────────────┘
           │          │          │
           ▼          ▼          ▼
    ┌──────────┐ ┌──────────┐ ┌──────────────┐
    │Microserv.│ │  Event   │ │    Shared    │
    │ Pattern  │ │  Driven  │ │    Kernel    │
    └──────────┘ └──────────┘ └──────────────┘
           │          │
           ▼          ▼
    ┌──────────┐ ┌──────────┐
    │   Saga   │ │  Outbox  │
    │ Pattern  │ │ Pattern  │
    └──────────┘ └──────────┘
  • Microservices Pattern : Les bounded contexts du DDD servent de guide pour définir les frontières de chaque microservice
  • Event-Driven Architecture : Les Domain Events du DDD sont la base de la communication asynchrone entre services
  • Saga Pattern : Coordonne les transactions qui traversent plusieurs bounded contexts à l’aide d’événements et de compensations
  • Outbox Pattern : Garantit que les Domain Events sont publiés de façon fiable en même temps que les changements de données
  • Anti-Corruption Layer (ACL) : Implémente la relation de Context Mapping qui protège votre domaine des modèles externes
  • Shared Kernel : Implémente la relation de Context Mapping où deux contextes partagent un sous-ensemble du modèle
  • API Gateway / BFF : Fournissent des points d’entrée capables de mapper les requêtes externes vers les bounded contexts internes

Erreurs courantes en appliquant le DDD

ErreurConséquenceSolution
Appliquer le DDD à tout le systèmeSur-ingénierie dans les zones simplesN’utilisez le DDD que dans le core domain, du CRUD pour le reste
Bounded contexts trop grandsMonolithe distribuéDivisez par sous-domaines réels, pas par couches techniques
Bounded contexts trop petitsServices trop bavardsRegroupez les fonctionnalités qui changent ensemble
Ignorer le langage omniprésentCode déconnecté du métierSessions régulières avec les experts du domaine
Modèle anémiqueLogique dispersée dans les servicesDéplacez le comportement vers les entités
Agrégats trop grandsProblèmes de concurrenceGardez les agrégats petits, utilisez des événements entre eux

Prochaines étapes

Avec le DDD comme base pour modéliser des domaines complexes, l’étape suivante naturelle consiste à comprendre comment gérer les transactions qui traversent plusieurs bounded contexts. C’est à cela que sert le Saga Pattern, qui coordonne les opérations distribuées avec des compensations. Explorez également le Microservices Pattern pour comprendre comment les bounded contexts deviennent des services indépendants.