Domain Driven Design : Guide Pratique
Introduction​
Ce document résume les concepts fondamentaux du Domain Driven Design (DDD) appliqués à un contexte .NET, illustrés par un exemple concret de gestion de dossiers, timesheets et RH.
Langage Ubiquitaire (Ubiquitous Language)​
Le code doit parler le même langage que le métier. Chaque terme a une définition précise, partagée entre développeurs et experts métier.
| Terme métier | Signification |
|---|---|
| Dossier | Un projet pour un client |
| Intervention | Une tâche réalisée sur un dossier |
| Timesheet | Le relevé journalier des heures d'un employé |
| Compteur | Un solde (congés, maladie, heures supplémentaires) |
| Contrat | Définit les jours et durées de travail d'un employé |
| Demande de Congé | Une réservation d'absence sur un compteur |
Règle d'or : si les utilisateurs parlent de "dossier", le code contient une classe Dossier, pas Project ou Folder.
Bounded Context​
Un Bounded Context est une frontière où les termes ont une signification précise et où le modèle est cohérent.
Le même mot peut avoir des significations différentes selon le contexte. Par exemple, "Employé" dans le contexte RH (contrat, compteurs, absences) n'a pas les mêmes attributs que "Employé" dans le contexte Gestion des Dossiers (juste un identifiant et un nom).
Exemple de découpage​
CONTEXTE : Gestion des Dossiers
- Dossier
- Intervention
- Client
- Employé (référence simplifiée)
CONTEXTE : Timesheet
- TimesheetJournalière
- LigneDeTemps
CONTEXTE : RH
- Employé (détaillé)
- Contrat
- Compteur
- DemandeDeCongé
Communication entre contextes​
Les contextes communiquent via des événements (Integration Events), jamais par accès direct aux données d'un autre contexte.
Aggregates​
Un Aggregate est un groupe d'objets formant une unité cohérente avec une porte d'entrée unique appelée Aggregate Root.
Principes​
- Toute modification passe par l'Aggregate Root
- L'Aggregate Root protège les règles métier (invariants)
- Un Aggregate = une transaction = un repository
- Les entités enfants ne sont jamais accessibles directement de l'extérieur
Question clé pour définir les frontières​
"Ces données doivent-elles être cohérentes immédiatement, dans la même transaction ?"
- Si oui → même Aggregate
- Si non (ça peut attendre quelques secondes) → Aggregates séparés
Exemple​
AGGREGATE Dossier
├── Dossier (Root)
│ - Connaît son statut (ouvert/clôturé)
│ - Contrôle l'ajout d'interventions
│ - Fait respecter les règles métier
└── Intervention (entités enfants)
Règle protégée : "On ne peut pas ajouter d'intervention sur un dossier clôturé"
Référencer un autre Aggregate​
Un Aggregate référence un autre Aggregate uniquement par son identifiant, jamais par référence directe.
TimesheetJournalière
└── LigneDeTemps
- dossierId (référence par ID, pas l'objet Dossier)
- durée
Entity vs Value Object​
Entity​
Un objet défini par son identité. Deux entities avec les mêmes valeurs mais des identités différentes sont différentes.
Exemples : Employé, Dossier, Intervention, DemandeDeCongé, Timesheet, Contrat, Compteur
Value Object​
Un objet défini uniquement par ses valeurs. Deux Value Objects avec les mêmes valeurs sont identiques et interchangeables.
Exemples : Durée, Période (date + tranche horaire), LieuDeTravail, Montant
Avantages des Value Objects​
- Encapsulent des règles de validation (une Durée ne peut pas être négative)
- Encapsulent des comportements (une Période sait si elle chevauche une autre)
- Rendent le code plus expressif et typé
Domain Services​
Un Domain Service contient de la logique métier qui n'appartient naturellement à aucun Aggregate.
Caractéristiques​
- Sans état (pas de données internes)
- Logique métier pure
- Opère sur des valeurs, pas directement sur des Aggregates
- Son nom fait partie du langage ubiquitaire
Exemple : CalculateurHeuresSupplementaires​
Entrées :
- tempsPresté (Durée)
- tempsContractuel (Durée)
Sortie :
- différence (Durée) : positive = heures sup, négative = déficit
Règle métier :
différence = tempsPresté - tempsContractuel
Domain Service vs Application Service​
| Domain Service | Application Service |
|---|---|
| Logique métier pure | Orchestration |
| Aucune dépendance technique | Utilise repositories, bus de messages |
| Dans la couche domaine | Dans la couche application |
Événements​
Domain Event​
Représente un fait métier qui s'est produit. Toujours nommé au passé.
Caractéristiques :
- Immutable
- Contient les informations nécessaires pour comprendre ce qui s'est passé
- Émis par l'Aggregate après une action réussie
- Traité au sein du même Bounded Context
- Généralement dans la même transaction
Exemples :
- InterventionAjoutée
- TimesheetJournalièreSoumise
- DemandeDeCongéCréée
Integration Event​
Notifie les autres contextes de ce qui s'est passé.
Caractéristiques :
- Publié vers l'extérieur du Bounded Context
- Transporté via un bus de messages (RabbitMQ, Azure Service Bus, etc.)
- Traité de manière asynchrone
- Cohérence à terme (eventual consistency)
Différences clés​
| Aspect | Domain Event | Integration Event |
|---|---|---|
| Portée | Même contexte | Entre contextes |
| Transport | En mémoire | Bus de messages |
| Transaction | Même transaction | Transactions séparées |
| Cohérence | Immédiate | À terme |
Contenu d'un événement​
Un événement ne contient que ce qui est pertinent, pas tout l'Aggregate.
TimesheetJournalièreValidée
├── timesheetId
├── employéId
├── date
├── tempsTotalPresté
└── occurredAt
(Pas besoin de : lieuDeTravail, pauseMidi, détail des lignes, etc.)
Quand Domain et Integration Event sont identiques​
Si le contenu est le même, la différence réside dans le transport et le moment du traitement. Une même classe peut servir aux deux usages.
Flux complet : exemple​
Scénario : Validation d'une timesheet et mise à jour des heures sup​
1. Manager valide la timesheet
└─► TimesheetJournalière.valider()
└─► Domain Event : TimesheetJournalièreValidée
2. Event publié comme Integration Event
└─► Bus de messages
3. Contexte RH reçoit l'événement
└─► Application Service
├─► Récupère l'Employé (avec son Contrat)
├─► Appelle CalculateurHeuresSupplementaires (Domain Service)
└─► Met à jour le Compteur
└─► Domain Event : CompteurMisÀJour
Capabilities vs Bounded Contexts​
Business Capability Map (vue stratégique)​
Répond à "Que fait l'entreprise ?"
- L0 : L'entreprise
- L1 : Grands domaines métier (RH, Opérations)
- L2 : Capacités (Suivi des Dossiers, Gestion des Absences)
- L3 : Sous-capacités (Création dossier, Demande congé)
Bounded Context (vue conception)​
Répond à "Comment modélise-t-on le logiciel ?"
Relation​
Souvent un L2 correspond à un Bounded Context, mais pas toujours. Plusieurs L2 peuvent être regroupés dans un même contexte s'ils partagent le même langage et les mêmes règles.
| Concept | Niveau | Question |
|---|---|---|
| L1 Capability | Stratégique | Quels grands domaines métier ? |
| L2 Capability | Tactique | Quelles capacités dans ce domaine ? |
| L3 Capability | Opérationnel | Quelles fonctions précises ? |
| Bounded Context | Conception | Où le modèle est-il cohérent ? |
| Aggregate | Implémentation | Quelle unité de cohérence ? |
Modèle complet de l'exemple​
CONTEXTE : Gestion des Dossiers
─────────────────────────────────────────
AGGREGATE Dossier
├── Dossier (Root)
└── Intervention
AGGREGATE Client
└── Client (Root)
Événements : InterventionAjoutée, InterventionModifiée,
DossierOuvert, DossierClôturé
CONTEXTE : Timesheet
─────────────────────────────────────────
AGGREGATE TimesheetJournalière
├── TimesheetJournalière (Root)
│ - employéId
│ - date
│ - statut (brouillon / soumise / validée / rejetée)
│ - lieuDeTravail (Value Object)
│ - pauseMidi (Value Object)
└── LigneDeTemps
- dossierId (référence)
- durée (Value Object)
- description
Règles métier :
- Soumettre uniquement si lieuDeTravail et pauseMidi renseignés
- Non modifiable après validation
Événements : TimesheetJournalièreSoumise, TimesheetJournalièreValidée,
TimesheetJournalièreRejetée
CONTEXTE : RH
─────────────────────────────────────────
AGGREGATE Employé
├── Employé (Root)
├── Contrat
│ - jours de prestation avec durées
└── Compteur (plusieurs : congés, maladie, heures sup)
AGGREGATE DemandeDeCongé
└── DemandeDeCongé (Root)
- employéId
- compteurType
- période (Value Object)
- statut (en attente / validée / refusée)
Règles métier :
- Pas deux demandes sur la mĂŞme tranche horaire
- Compteur décrémenté à la création, recédité si refus
- Heures sup calculées jour par jour
Événements : DemandeDeCongéCréée, DemandeDeCongéValidée,
DemandeDeCongéRefusée, CompteurMisÀJour
Domain Service : CalculateurHeuresSupplementaires
Pour aller plus loin​
- Value Objects : implémentation et patterns
- Context Mapping : patterns de relation entre contextes (ACL, Shared Kernel, Customer/Supplier)
- CQRS : séparation lecture/écriture
- Event Sourcing : stocker les événements plutôt que l'état
- Structure projet .NET : organisation des couches et dossiers