Que fait Domain-Driven Design — Patrons et Pratiques ?
Quand utiliser le DDD
À utiliser lorsque :
- La logique métier complexe dépasse le simple CRUD
- Plusieurs experts du domaine utilisent des vocabulaires différents
- Le modèle métier est l'avantage concurrentiel
- Applications de niveau entreprise
Ne pas utiliser lorsque :
- Applications CRUD simples
- Hackathon / MVP (trop lourd)
- Aucun expert du domaine disponible
Design Stratégique
Contextes Bornés
Le concept le plus important du DDD. Un Contexte Borné est une frontière à l'intérieur de laquelle un modèle particulier est défini et applicable.
Exemple — E-Commerce :
[Contexte Commande] [Contexte Paiement] [Contexte Expédition]
- Commande - Paiement - Expédition
- ArticleDeCommande - Transaction - NuméroDeSuivi
- Client (ref) - Remboursement - Adresse
- Adresse (valeur) - Facture - Transporteur
Client signifie des choses différentes dans chaque contexte :
- Contexte Commande : nom, email, préférence de livraison
- Contexte Paiement : informations de facturation, moyens de paiement
- Contexte Support : historique des tickets, score de satisfaction
Carte de Contexte
[Commande] ←→ [Paiement] # Partenariat
[Commande] → [Expédition] # Client-Fournisseur
[Commande] → [CRM Hérité] # Couche Anti-Corruption
Design Tactique
Entités
Objets avec identité. Deux entités ayant les mêmes attributs mais des ID différents sont différentes.
class User {
readonly id: UserId;
name: string;
email: Email; // Objet-Valeur
}
Objets-Valeurs
Objets définis par leurs attributs, pas par leur identité. Immuables.
class Email {
constructor(readonly value: string) {
if (!isValidEmail(value)) throw new InvalidEmailError(value);
}
equals(other: Email): boolean {
return this.value === other.value;
}
}
Agrégats
Un regroupement d'entités et d'objets-valeurs avec une seule entité racine (Racine d'Agrégat). Tout accès passe par la racine.
class Order { // Racine d'Agrégat
private items: OrderItem[] = [];
addItem(product: ProductRef, quantity: number): void {
// Logique métier ICI, pas dans un service
if (quantity <= 0) throw new InvalidQuantityError();
this.items.push(new OrderItem(product, quantity));
}
get total(): Money {
return this.items.reduce((sum, item) => sum.add(item.subtotal), Money.zero());
}
}
Règles des Agrégats :
- Référencer les autres agrégats uniquement par leur ID
- Un agrégat par transaction
- Cohérence éventuelle entre les agrégats
Événements de Domaine
Quelque chose qui s'est produit dans le domaine et qui intéresse les experts du domaine.
class OrderPlaced implements DomainEvent {
constructor(
readonly orderId: OrderId,
readonly customerId: CustomerId,
readonly total: Money,
readonly occurredAt: Date
) {}
}
Services de Domaine
Logique métier qui n'appartient pas naturellement à une entité ou à un objet-valeur.
class PricingService {
calculatePrice(order: Order, customer: Customer, promotions: Promotion[]): Money {
// Logique de tarification complexe impliquant plusieurs agrégats
}
}
Dépôts
Abstraction sur l'accès aux données. Un dépôt par racine d'agrégat.
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
delete(id: OrderId): Promise<void>;
}
Structure de Répertoire (DDD)
src/
├── modules/
│ └── orders/ # Contexte Borné
│ ├── domain/
│ │ ├── entities/
│ │ │ └── order.ts # Racine d'Agrégat
│ │ ├── value-objects/
│ │ │ └── money.ts
│ │ ├── events/
│ │ │ └── order-placed.ts
│ │ ├── services/
│ │ │ └── pricing.ts
│ │ └── repositories/
│ │ └── order.repository.ts # Interface
│ ├── application/
│ │ ├── commands/
│ │ │ └── place-order.ts
│ │ ├── queries/
│ │ │ └── get-order.ts
│ │ └── handlers/
│ │ └── place-order.handler.ts
│ └── infrastructure/
│ ├── persistence/
│ │ └── order.repository.impl.ts # Implémentation
│ └── api/
│ └── orders.controller.ts
La Règle de Dépendance de l'Architecture Propre
En DDD, les dépendances DOIVENT strictement pointer vers l'intérieur :
[ Frameworks & Drivers (Web, DB, UI) ]
└──▶ [ Adaptateurs d'Interface (Contrôleurs, Passerelles) ]
└──▶ [ Application (Cas d'Utilisation, Handlers CQRS) ]
└──▶ [ Domaine (Entités, Objets-Valeurs) ]
- La couche Domaine n'a AUCUNE dépendance vers les ORM (Prisma, TypeORM), les frameworks HTTP (Express, NestJS) ou les SDK externes.
- Les dépôts sont définis comme des interfaces dans la couche domaine/application et implémentés dans la couche infrastructure.
Événements de Domaine vs Événements d'Intégration
- Événements de Domaine : Représentent des changements d'état à l'intérieur d'un seul Contexte Borné.
- Levés directement à l'intérieur de la Racine d'Agrégat (
order.addItem(...)lèveOrderItemAdded). - Distribués in-process avant le commit de la transaction.
- Levés directement à l'intérieur de la Racine d'Agrégat (
- Événements d'Intégration : Publiés entre les frontières des Contextes Bornés pour communiquer avec d'autres services.
- Distribués via le modèle Transactional Outbox vers les courtiers de messages.
- Doivent utiliser des schémas rétrocompatibles avec versionnage.
Couche Anti-Corruption (ACL)
Lors de la consommation de données provenant d'un contexte borné externe ou d'une API tierce (ex. Stripe, Salesforce) :
- N'importez JAMAIS les modèles de domaine externes directement dans votre domaine.
- Créez un Traducteur / Adaptateur ACL dans la couche infrastructure pour convertir les DTO externes en vos propres Objets-Valeurs et Entités.
Anti-Patrons
- [FAIL] Modèle de domaine anémique — entités avec seulement getters/setters, toute la logique dans les services
- [FAIL] Agrégat trop gros — les agrégats doivent être petits, concentrés sur les invariants
- [FAIL] Transactions cross-agrégats — utiliser la cohérence éventuelle
- [FAIL] DDD partout — n'utiliser le DDD que là où la complexité le justifie
- [FAIL] Entités ORM qui fuient dans le Domaine — les entités du domaine ne doivent pas dépendre de
@Entity()ou de décorateurs ORM
Exemples de Code
Voir EXAMPLES.md pour des exemples de code détaillés.
Liste de Vérification
Ce qu'il faut vérifier pendant la phase de relecture avant de terminer la tâche.
Erreurs Courantes
Anti-patrons et choses à éviter explicitement. Voir TROUBLESHOOTING.md.
Notes d'Intégration
Comment cette compétence interagit avec d'autres compétences.
Exemples DDD — Anti-patrons vs Standard ContextOS
Exemple 1 : Entités du Domaine vs Modèles Anémiques
Anti-patron : Modèle de Domaine Anémique avec Setters qui Fuient
// MAUVAIS : Aucun invariant métier ; n'importe quel appelant peut corrompre l'état
class BankAccount {
public balance: number = 0;
public isFrozen: boolean = false;
}
// Logique qui fuit dans le contrôleur ou le service
account.balance -= 500; // Découvert non vérifié !
Bonne pratique : Standard ContextOS (Modèle de Domaine Riche avec Invariants Protégés)
// BON : Les invariants sont strictement appliqués à l'intérieur de la Racine d'Agrégat
class BankAccount {
private _balance: number;
private _isFrozen: boolean;
constructor(id: string, initialDeposit: Money) {
this._balance = initialDeposit.amount;
this._isFrozen = false;
}
public withdraw(amount: Amount): void {
if (this._isFrozen) {
throw new AccountFrozenException('Impossible de retirer d\'un compte gelé');
}
if (this._balance < amount.amount) {
throw new InsufficientFundsException('Fonds insuffisants pour le retrait');
}
this._balance -= amount.amount;
this.addDomainEvent(new MoneyWithdrawnEvent(this.id, amount));
}
}
Dépannage & Erreurs Courantes du DDD
1. Agrégats Dieu
- Symptôme : La Racine d'Agrégat contient 20 entités enfants et son chargement nécessite des jointures sur des dizaines de tables.
- Cause Racine : Traiter les tables d'un diagramme ERD comme des frontières d'agrégats plutôt que comme des unités de cohérence transactionnelle.
- Correctif : Concevoir des agrégats de petite taille. Référencer les autres agrégats uniquement par leur ID, pas par référence d'objet.
2. Fuite de l'Infrastructure dans la Couche Domaine
- Symptôme : Les entités du domaine importent Prisma, des décorateurs TypeORM, ou des objets Request Express.
- Cause Racine : Inversion des frontières de l'Architecture Propre.
- Correctif : La couche Domaine doit être du TypeScript pur avec zéro dépendance externe de framework.
3. Transaction couvrant plusieurs Agrégats
- Symptôme : Conflits de verrouillage élevés et blocages sous des transactions concurrentes.
- Cause Racine : Modification de plusieurs racines d'agrégats dans la même transaction de base de données.
- Correctif : Règle empirique : exactement une Racine d'Agrégat modifiée par transaction. Utiliser les Événements de Domaine pour la cohérence éventuelle entre les autres agrégats.