Modéliser le logiciel à l’image du métier. Le code parle le même langage que l’expert, les frontières sont explicites, et la logique métier vit au cœur de l’architecture — indépendante de toute technologie.
Imagine que tu construis une grande ville. Chaque quartier a ses propres règles, son propre langage, ses propres habitants. Le quartier financier parle de transactions et d’actifs. Le quartier médical parle de patients et d’ordonnances. Ils ne se comprennent pas directement — il faut des traducteurs aux frontières.
C’est exactement le DDD : modéliser le logiciel à l’image du métier, avec des frontières claires entre les domaines, un langage commun à chaque quartier, et une logique métier qui dicte l’architecture — jamais l’inverse.
Le Domain-Driven Design, introduit par Eric Evans en 2003, repose sur une idée simple mais radicale : c’est le domaine métier qui doit guider chaque décision architecturale. Avant de choisir une base de données, un framework ou un pattern, on modélise la réalité du métier avec les experts — et ce modèle devient le code.
Le DDD se divise en deux niveaux : la conception stratégique, qui organise les grandes frontières du système, et la conception tactique, qui fournit les briques de construction à l’intérieur de ces frontières.
02
Vue d’ensemble — Les deux niveaux
Conception Stratégique — Comment découper le système en domaines cohérents et faire coexister plusieurs modèles.
🗺️
Bounded Context
Contexte Delimité
Frontière explicite dans laquelle un modèle de domaine a une signification précise et cohérente. Un même mot peut signifier des choses différentes dans deux Bounded Contexts distincts.
FrontièreAutonomieCohérence
« Client » dans le contexte Commande ≠ « Client » dans le contexte CRM.
🗣️
Ubiquitous Language
Langage Ubiquitaire
Vocabulaire commun entre développeurs et experts métier, utilisé partout : dans les réunions, le code, les tests, la documentation. Si le code dit passer_commande(), le métier dit aussi « passer commande ».
CommunicationAlignementCohérence
Le code est la documentation. Le vocabulaire métier EST le code.
🧭
Context Map
Carte des Contextes
Représentation des relations entre Bounded Contexts. Définit comment ils communiquent : Shared Kernel, Customer/Supplier, Anti-Corruption Layer, Published Language…
ACLShared KernelRelations
Conception Tactique — Les briques de construction à l’intérieur d’un Bounded Context.
🏰
Aggregate
Frontière de Cohérence
Grappe d’Entities et de Value Objects traitée comme une unité atomique. Un seul point d’entrée (la Racine), une seule frontière transactionnelle. Les invariants métier sont garantis à l’intérieur.
RootInvariantsTransaction
Le gardien de la cohérence métier.
🪪
Entity
Objet avec Identité
Objet défini par son identité unique, pas par ses attributs. Deux Entities avec les mêmes données sont distinctes si leurs IDs diffèrent. Mutable, avec un cycle de vie propre.
ID uniqueMutableCycle de vie
💎
Value Object
Objet sans Identité
Objet défini entièrement par ses attributs. Immuable, sans identité propre. Deux Value Objects avec les mêmes données sont égaux et interchangeables.
ImmutableÉgalité par valeurSans ID
📢
Domain Event
Événement Domaine
Quelque chose qui s’est passé dans le domaine. Nommé au passé, immuable, daté. Déclenche des réactions dans d’autres Aggregates ou Bounded Contexts sans couplage direct.
CommandeConfirméeDécouplagePassé
🔧
Domain Service
Service Domaine
Opération métier sans état qui ne se rattache naturellement à aucune Entity ou Value Object. Contient de la logique métier qui implique plusieurs Aggregates ou des calculs transverses.
Sans étatInter-entités
🗄️
Repository
Abstraction de Persistance
Interface définie dans le Domain qui donne l’illusion d’une collection en mémoire. L’implémentation SQL/NoSQL vit dans l’Infrastructure — le Domain ne la connaît jamais.
InterfaceDIPPersistance
« La DB est un détail. » — Uncle Bob
03
Architecture en couches DDD
Le DDD s’organise en quatre couches dont la règle d’or est absolue : les dépendances ne pointent que vers l’intérieur. Le Domain ne dépend de rien. L’Infrastructure dépend du Domain — jamais l’inverse.
Présentation
Presentation Layer
Reçoit les requêtes, délègue à l’Application Layer. Ne contient aucune logique métier.
La Règle d’Or des dépendances : Présentation → Application → Domain ← Infrastructure Le Domain est le seul à ne dépendre de rien. Si votre Domain importe quoi que ce soit de NestJS, TypeORM ou Express — la règle est violée.
Un Aggregate, c’est un château fort médiéval. Il y a une porte principale (la Racine — Aggregate Root), des remparts qui définissent la frontière de cohérence, et à l’intérieur vivent des Entities et des Value Objects. Pour parler au seigneur, aux chevaliers ou aux serviteurs — tu passes obligatoirement par la porte principale. Jamais par les remparts. Et une seule règle absolue : une transaction ne franchit pas les murs d’un seul château.
Pour modifier quoi que ce soit dans l’Aggregate, on passe obligatoirement par l’Aggregate Root. Aucun accès direct aux Entities ou Value Objects internes depuis l’extérieur.
Règle 2
1 transaction = 1 seul Aggregate
Une transaction ne modifie qu’un seul Aggregate à la fois. Si deux Aggregates doivent changer, on utilise des Domain Events et la cohérence éventuelle.
Règle 3
Référence externe = ID uniquement
Un Aggregate ne référence jamais un autre Aggregate par objet. Seulement par son ID (Value Object typé). Cela garantit l’indépendance et la performance.
Règle 4
Les invariants sont garantis par la Racine
C’est la Racine qui vérifie toutes les règles métier avant chaque mutation. Si un invariant est violé, la Racine lève une exception — l’état reste cohérent.
Règle 5
Préférer les petits Aggregates
Plus l’Aggregate est petit, plus il est performant (moins de données chargées) et moins il génère de conflits de concurrence. Un Aggregate idéal a 1 à 3 Entities.
Règle 6
Communication inter-Aggregates via Events
Deux Aggregates ne s’appellent jamais directement. L’Aggregate A émet un Domain Event, un handler réagit et modifie l’Aggregate B dans une transaction séparée.
Entity → Pense à une personne. Si deux jumeaux ont exactement le même nom, la même apparence, la même date de naissance — ce sont quand même deux personnes distinctes. Ce qui compte, c’est leur identité, pas leurs attributs.
Value Object → Pense à un billet de 50€. Si tu échanges ton billet contre un autre billet de 50€ — tu t’en fous. Ils sont interchangeables. Ce qui compte, c’est la valeur, pas quel billet physique tu tiens.
💎 Value Object
Égalité par valeur — deux VO avec les mêmes données sont égaux
Immuable — on ne modifie pas un VO, on en crée un nouveau
Pas d’identité propre — aucun ID
Pas de cycle de vie — il naît et meurt avec son Entity
Partage sécurisé — on peut le passer partout sans risque
🪪 Entity
Égalité par identité — seul l’ID compte pour la comparaison
Mutable — ses attributs changent au cours du temps
ID unique obligatoire — c’est sa raison d’être
Cycle de vie propre — création, modification, archivage
Partage avec précaution — risque de mutation extérieure
domain/Montant.ts — Value Object
// Immuable. Égalité par valeur. Pas d'identité.classMontant {
private constructor(
private readonly _valeur: number,
private readonly _devise: string = 'EUR',
) {
if (_valeur < 0) throw newError('Montant négatif interdit');
}
// Factory method — seul point de créationstaticde(valeur: number, devise = 'EUR'): Montant {
return newMontant(valeur, devise);
}
// Opérations immuables — retournent un NOUVEAU Montantajouter(autre: Montant): Montant {
if (this._devise !== autre._devise)
throw newError('Devises différentes');
returnMontant.de(this._valeur + autre._valeur, this._devise);
}
// Égalité par VALEUR — pas par référenceestEgalA(autre: Montant): boolean {
return this._valeur === autre._valeur
&& this._devise === autre._devise;
}
}
// Test : deux Montant identiques sont égauxconst a = Montant.de(50, 'EUR');
const b = Montant.de(50, 'EUR');
a.estEgalA(b); // true → même VALEUR = égaux
a === b; // false → références différentes, mais on s'en fiche
domain/Client.ts — Entity
// Identité unique. Mutable. Cycle de vie propre.classClient {
private _email: Email; // ← Value Objectprivate _adresse: Adresse; // ← Value Objectconstructor(
readonly id: ClientId, // ← Value Object (ID typé)
email: Email,
adresse: Adresse,
) {
this._email = email;
this._adresse = adresse;
}
// Comportement métier : on change l'emailchangerEmail(nouvelEmail: Email): void {
if (this._email.estEgalA(nouvelEmail))
throw newError('Même email');
this._email = nouvelEmail; // ← nouveau VO, pas de mutation du VO
}
// Égalité par IDENTITÉ — seul l'ID compteestEgalA(autre: Client): boolean {
return this.id.estEgalA(autre.id);
}
}
// Test : deux Clients avec le même ID sont le même Clientconst c1 = newClient(id, email1, adresse1);
const c2 = newClient(id, email2, adresse2);
c1.estEgalA(c2); // true → même ID = même Entity
Le cas piège — L’Adresse : VO ou Entity ? Un même concept peut être Value Object dans un contexte et Entity dans un autre. L’Adresse de livraison d’une commande est un VO : elle est capturée au moment de la commande et ne doit pas changer si le client déménage. L’Adresse d’un entrepôt logistique est une Entity : on veut tracer ses modifications au fil du temps et la retrouver par son identité. C’est le contexte métier qui décide, jamais la technique.
1Se soucie-t-on de QUELLE instance c’est ?Non → VOOui → Entity
2Si je remplace cet objet par un autre avec les mêmes données, est-ce que ça change quelque chose ?Non → VO (billet)Oui → Entity (passeport)
3Cet objet a-t-il une vie propre — on le crée, on le suit, on l’archive ?Non → VOOui → Entity
4L’objet change-t-il au cours du temps tout en restant le même objet ?Non → VOOui → Entity
5A-t-on besoin de le retrouver par son identité en base de données ?Non → VOOui → Entity
La règle pratique : La grande majorité des attributs d’une Entity devraient être des Value Objects. Client est une Entity — mais son email, son adresse, son telephone sont des Value Objects. L’Entity est le squelette — les Value Objects sont la chair.
07
Domain Events — Le découplage par les faits
Un Domain Event représente quelque chose qui s’est passé dans le domaine. Nommé au passé, immuable, daté. Il permet à un Aggregate de déclencher des réactions dans d’autres Aggregates — ou d’autres Bounded Contexts — sans couplage direct.
❌ Sans Domain Events — Couplage fort
Le Use Case appelle directement le service de stock, le mailer, le CRM…
Ajouter un effet de bord = modifier le Use Case existant
Impossible de tester la logique métier isolément
Un échec dans le mailer fait échouer toute la commande
✓ Avec Domain Events — Découplage
L’Aggregate émet un event — il ne sait pas qui écoute
Ajouter un effet de bord = ajouter un handler, sans toucher au code existant
Chaque handler est testable indépendamment
Un échec dans le mailer ne bloque pas la confirmation
domain/events/CommandeConfirmeeEvent.ts
// Interface commune à tous les Domain EventsinterfaceDomainEvent {
readonly occurredOn: Date;
readonly eventType: string;
}
// Event concret — immuable, nommé au passéclassCommandeConfirmeeEventimplementsDomainEvent {
readonly occurredOn = newDate();
readonly eventType = 'COMMANDE_CONFIRMEE';
constructor(
readonly commandeId: CommandeId,
readonly clientId: string,
readonly montantTotal: Montant,
) {}
}
// Handlers — chacun réagit indépendammentclassReduireStockHandler {
asynchandle(event: CommandeConfirmeeEvent): Promise<void> {
// Réduire le stock pour chaque produit de la commandeawait this.stockService.reduire(event.commandeId);
}
}
classEnvoyerConfirmationHandler {
asynchandle(event: CommandeConfirmeeEvent): Promise<void> {
// Envoyer un email au client — si ça échoue, la commande reste confirméeawait this.mailer.envoyer(event.clientId, 'Votre commande est confirmée');
}
}
08
Repository & Application Layer
Le Repository est une abstraction qui donne au domaine l’illusion d’une collection en mémoire. L’interface vit dans le Domain, l’implémentation SQL dans l’Infrastructure. L’Application Layer orchestre — sans jamais contenir de logique métier.
// Application Layer — orchestre, ne décide RIEN du métierclassPasserCommandeUseCase {
constructor(
private readonly commandeRepo: CommandeRepository, // ← interfaceprivate readonly eventBus: EventBus, // ← interface
) {}
asyncexecuter(dto: PasserCommandeDTO): Promise<void> {
// 1. Créer l'Aggregate via sa Factoryconst commande = Commande.creer(dto.clientId);
// 2. Déléguer le comportement au Domainfor (const ligne of dto.lignes) {
commande.ajouterProduit(
newProduitId(ligne.produitId),
ligne.quantite,
Montant.de(ligne.prix),
);
}
commande.confirmer();
// 3. Persister via le Repository (interface)await this.commandeRepo.save(commande);
// 4. Dispatcher les Domain Events APRÈS persistancefor (const event of commande.collecterEvenements()) {
await this.eventBus.publish(event);
}
}
}
09
Les pièges classiques à éviter
🐘
Piège 1
L’Aggregate trop gros
Un Aggregate « Commande » qui contient le Client, les Produits, l’Historique, les Factures… Trop d’Entities = trop de conflits de concurrence, trop de données chargées, trop de couplage.
PerformanceConcurrenceDécoupage
🔗
Piège 2
Référence directe entre Aggregates
Stocker une référence objet vers un autre Aggregate au lieu de son ID. Résultat : couplage fort, problèmes de lazy loading, transactions qui débordent.
ID uniquementDécouplagePerformance
🧠
Piège 3
Logique métier dans le Use Case
Le Use Case contient des if métier, des calculs de prix, des validations complexes. Il devrait seulement orchestrer : créer, déléguer, persister, dispatcher.
OrchestrationDélégationSéparation
🚪
Piège 4
Exposer les collections internes
Retourner un tableau mutable depuis l’Aggregate. L’extérieur peut alors modifier la collection sans passer par la Racine — les invariants sont contournés.
EncapsulationInvariantsReadonly
📭
Piège 5
Oublier les Domain Events
Appeler directement les services depuis le Use Case au lieu d’émettre des events. Résultat : couplage temporel, impossible d’ajouter un handler sans modifier le code existant.
Open/ClosedDécouplageExtensibilité
🏗️
Piège 6
Confondre Entity TypeORM et Entity Domain
L’Entity TypeORM est un modèle de persistance avec décorateurs @Entity@Column. L’Entity Domain est un objet pur sans dépendance technique. Ce sont deux choses complètement différentes.
MappingSéparationDomain pur
Signal d’alerte universelle : Si votre test d’un use case nécessite de lancer une base de données, de démarrer NestJS ou d’appeler Stripe — la Dependency Rule est violée quelque part. Le domaine doit être testable sans aucune infrastructure.
10
Récapitulatif — Les 7 piliers
ConceptRôleRègle clé
Ubiquitous LanguageLangage commun dévs ↔ expertsLe code utilise les mêmes termes que le métier
Bounded ContextFrontière explicite du modèleUn mot = une définition dans un contexte
EntityObjet avec identité uniqueÉgalité par ID, cycle de vie propre
Value ObjectObjet sans identité, immuableÉgalité par valeur, interchangeable
AggregateFrontière de cohérence transactionnelleAccès par la Racine, 1 tx = 1 Aggregate
Domain EventFait passé dans le domaineImmuable, daté, déclenche des réactions
RepositoryAbstraction de persistanceInterface dans Domain, impl dans Infra
📖
La synthèse de Robert C. Martin
Le Domain, c’est le cœur de la maison — il ne dépend pas des murs, de la plomberie ou de l’électricité. Si vous changez de plombier (base de données), le cœur ne bouge pas. C’est le principe de Clean Architecture appliqué au DDD : l’infrastructure est un détail remplaçable. Le métier, lui, est éternel.