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 :
- Organisez des sessions d’Event Storming avec des experts du domaine et des développeurs
- Identifiez les termes clés utilisés par le métier et documentez-les dans un glossaire partagé
- Utilisez ces termes exacts dans le code, les tests, la documentation et les conversations
- 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 :
| Relation | Description | Exemple |
|---|---|---|
| Shared Kernel | Deux contextes partagent un sous-ensemble du modèle | Types de devises partagés entre Ventes et Facturation |
| Customer-Supplier | Un contexte fournit des données qu’un autre consomme | L’Inventaire (supplier) fournit le stock aux Commandes (customer) |
| Conformist | Un contexte s’adapte au modèle d’un autre sans négociation | Votre système s’adapte au modèle de l’API d’un fournisseur externe |
| Anti-Corruption Layer | Un contexte traduit le modèle externe dans son propre langage | Adaptateur qui traduit les réponses d’un système legacy |
| Open Host Service | Un contexte expose un protocole ouvert pour que d’autres le consomment | API REST publique avec documentation OpenAPI |
| Published Language | On 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égorie | Options |
|---|---|
| Frameworks DDD | Axon Framework (Java), EventSourcing.NetCore (C#), Ecotone (PHP) |
| Event Sourcing | EventStoreDB, Axon Server, Marten (.NET) |
| CQRS | MediatR (.NET), Axon (Java), implémentations sur mesure |
| Messagerie | Apache Kafka, RabbitMQ, Amazon EventBridge |
| Modélisation | Event Storming, Domain Storytelling, Context Mapping |
| Persistance | PostgreSQL, 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
| Erreur | Conséquence | Solution |
|---|---|---|
| Appliquer le DDD à tout le système | Sur-ingénierie dans les zones simples | N’utilisez le DDD que dans le core domain, du CRUD pour le reste |
| Bounded contexts trop grands | Monolithe distribué | Divisez par sous-domaines réels, pas par couches techniques |
| Bounded contexts trop petits | Services trop bavards | Regroupez les fonctionnalités qui changent ensemble |
| Ignorer le langage omniprésent | Code déconnecté du métier | Sessions régulières avec les experts du domaine |
| Modèle anémique | Logique dispersée dans les services | Déplacez le comportement vers les entités |
| Agrégats trop grands | Problèmes de concurrence | Gardez 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.