Domain-Driven Design : modéliser le métier, pas la base de données
Le DDD, ce n'est pas d'abord une histoire de repositories et d'agrégats, c'est parler aux experts métier jusqu'à ce que le code parle leur langage. Voici comment le DDD stratégique et tactique s'articulent, avec un exemple concret.
Le Domain-Driven Design, formalisé par Eric Evans, a deux volets constamment confondus. Le DDD stratégique concerne la façon de découper un grand domaine en morceaux gérables. Le DDD tactique, ce sont les patterns, entities, value objects, aggregates, utilisés pour modéliser à l'intérieur d'un de ces morceaux. La plupart des équipes sautent directement aux patterns tactiques et zappent le travail stratégique qui les rend utiles. Le vrai sujet n'est ni l'un ni l'autre : c'est faire parler au code le langage du métier qu'il sert.
Qu'est-ce que le Domain-Driven Design, en un paragraphe ?
Le Domain-Driven Design est une approche de la conception logicielle où la structure et le vocabulaire du code collent au domaine métier qu'il sert. Vous modélisez les concepts qu'un expert métier reconnaîtrait, commandes, factures, abonnements, expéditions, et les règles qui les gouvernent, directement dans le code, plutôt que de modéliser des tables de base de données ou des classes de framework. Il a un versant stratégique, comment découper un grand domaine en parties, et un versant tactique, des patterns pour modéliser à l'intérieur d'une partie, mais le coeur est un langage partagé entre ceux qui écrivent le code et ceux qui comprennent le métier.
DDD stratégique : les bounded contexts
Un bounded context est une frontière à l'intérieur de laquelle un modèle et son ubiquitous language s'appliquent de façon cohérente. Le mot Order ne signifie pas la même chose dans le contexte Ventes, un contrat signé, que dans le contexte Livraison, un colis à router. Vouloir construire un modèle Order unifié pour toute l'entreprise est là où les projets DDD dérapent généralement. Une context map montre quels contextes existent et comment ils se relient, et un anticorruption layer protège votre modèle quand vous intégrez un contexte que vous ne contrôlez pas.
- ✓Sous-domaine core : votre avantage concurrentiel. C'est là que l'effort de modélisation doit aller.
- ✓Sous-domaine supporting : nécessaire mais pas différenciant. Construisez-le simplement.
- ✓Sous-domaine generic : un problème déjà résolu, authentification, facturation, notifications. Achetez-le ou prenez quelque chose sur étagère.
L'ubiquitous language
Le vocabulaire utilisé par les experts métier en conversation doit être exactement le vocabulaire du code : noms de classes, de méthodes, même de variables. Si l'équipe facturation dit qu'un abonnement arrive à échéance, la méthode est lapse(), pas setStatusInactive(). Là où le code a besoin d'une couche de traduction entre ce que le métier appelle une chose et ce que le code l'appelle, cet écart est là où bugs et malentendus s'accumulent. Quand code et conversation divergent, c'est un signal que le modèle dérive, et c'est en général le code qui a tort.
DDD tactique : les briques
| Brique | C'est quoi | Exemple | Règle clé |
|---|---|---|---|
| Value object | Défini uniquement par ses attributs, pas d'identité | Money, DateRange, Address | Immuable, comparé par valeur |
| Entity | A une identité qui persiste à travers le changement | Order, Customer, Subscription | Comparée par ID, pas par attributs |
| Aggregate | Un groupe gardé cohérent comme une unité | Une commande avec ses lignes | Un point d'entrée, l'aggregate root |
| Aggregate root | Le seul membre que le code extérieur peut référencer | Order, pas OrderLine | Fait respecter les invariants de l'aggregate |
| Domain event | Quelque chose de significatif qui s'est produit | OrderPlaced, PaymentFailed | Nommé au passé, immuable |
| Repository | Accès aux aggregates façon collection | OrderRepository | Un par aggregate root |
| Domain service | Une logique qui n'appartient à aucune entity | PricingPolicy, FundsTransfer | Sans état, nommé en termes du domaine |
Ce sont des moyens, pas des fins. Sortez-les quand les règles du domaine justifient la structure, pas avant.
Un exemple travaillé : un domaine d'abonnement
Prenez une petite tranche. Un abonnement a un plan, une période de facturation et un statut : actif, en retard de paiement, ou annulé. Modélisez-le comme l'équipe facturation en parle, avec les règles qui vivent dans l'objet plutôt que dans un service qui le manipule de l'extérieur.
// Value object : pas d'identité, défini entièrement par ses attributs, immuable
final readonly class Money
{
public function __construct(
public int $amountInCents,
public string $currency,
) {}
public function equals(Money $other): bool
{
return $this->amountInCents === $other->amountInCents
&& $this->currency === $other->currency;
}
}// Aggregate root : l'état est privé, les règles vivent dans les méthodes
final class Subscription
{
private SubscriptionStatus $status = SubscriptionStatus::Active;
private array $raisedEvents = [];
public function __construct(
private readonly SubscriptionId $id,
private Plan $plan,
private BillingPeriod $currentPeriod,
) {}
public function renew(BillingPeriod $next): void
{
if ($this->status === SubscriptionStatus::Cancelled) {
throw new DomainException('Un abonnement annulé ne peut pas être renouvelé.');
}
$this->currentPeriod = $next;
$this->status = SubscriptionStatus::Active;
}
public function markPastDue(): void
{
if ($this->status !== SubscriptionStatus::Active) {
return;
}
$this->status = SubscriptionStatus::PastDue;
}
public function cancel(): void
{
$this->status = SubscriptionStatus::Cancelled;
$this->raisedEvents[] = new SubscriptionCancelled($this->id, now());
}
}// Domain event : au passé, immuable, porte ce dont les listeners ont besoin
final readonly class SubscriptionCancelled
{
public function __construct(
public SubscriptionId $subscriptionId,
public DateTimeImmutable $occurredAt,
) {}
}Les règles sont impossibles à contourner. Il n'y a pas de setStatus(), donc un abonnement ne peut pas repasser d'annulé à actif par accident. L'état est privé, le vocabulaire colle à l'équipe facturation, et l'annulation lève un événement auquel d'autres parties du système, relance de paiement, analytics, email client, peuvent réagir sans que l'abonnement ne les connaisse.
Les aggregates sont des frontières de cohérence, pas des graphes d'objets
Un aggregate regroupe ce qui doit rester cohérent ensemble et expose exactement un point d'entrée, l'aggregate root. Gardez les aggregates petits : référencez les autres aggregates par ID, pas par objet, et prenez un aggregate par transaction comme règle par défaut. Un gros aggregate qui embarque la moitié de la base devient un coût de chargement et une source de contention de verrous, et cela veut en général dire que la frontière de cohérence a été tracée trop large.
L'erreur DDD la plus courante est le modèle de domaine anémique : des classes qui ne sont que des sacs de données avec getters et setters, pendant que toute la logique vit dans une couche service séparée. C'est du code procédural avec des objets utilisés comme des structs, et cela abandonne presque tout ce que le DDD apporte. Si une règle peut être violée en appelant un setter, le modèle ne fait pas son travail.
DDD et Clean Architecture
Les deux s'articulent. La Clean Architecture donne le découpage en couches qui garde le domaine libre du framework et de la base de données. Le DDD dit quoi mettre dans cette couche domaine : les entities, value objects et aggregates qui portent les règles. Vous pouvez faire l'un sans l'autre, mais un domaine complexe veut en général les deux, le découpage pour protéger le modèle et la modélisation pour rendre la couche protégée digne d'être protégée.
Quand le DDD vaut l'investissement
- ✓Les règles métier sont complexes et changent souvent, pas du simple CRUD.
- ✓Plusieurs équipes travaillent sur différents sous-domaines et ont besoin de frontières de responsabilité claires.
- ✓Un malentendu avec le métier coûte cher, dans un domaine régulé ou financier par exemple.
- ✓Le système est censé vivre et évoluer pendant des années.
Quand s'en passer
- ✓Une appli CRUD qui est surtout des formulaires au-dessus d'une base.
- ✓Un prototype ou un projet à courte durée de vie.
- ✓Une petite équipe où tout le monde partage déjà le vocabulaire du domaine.
- ✓L'adoption partielle est légitime : gardez l'ubiquitous language, sautez les patterns tactiques tant que les règles ne les justifient pas.
FAQ
Le DDD en termes simples ?
C'est une façon de construire un logiciel pour que le code parle le langage du métier. Vous modélisez de vrais concepts du domaine et leurs règles, pas des tables de base de données, et vous gardez les mots des développeurs identiques aux mots des experts métier.
Qu'est-ce qu'un bounded context ?
Le périmètre à l'intérieur duquel un modèle et ses termes veulent dire exactement une chose. Deux contextes peuvent porter des modèles différents pour ce qui ressemble au même concept : un Client en Ventes n'est pas le même objet qu'un Client en Support, et c'est voulu.
DDD et Clean Architecture, est-ce pareil ?
Non, ils sont complémentaires. La Clean Architecture parle du sens des dépendances entre couches. Le DDD parle de la façon de modéliser le métier dans la couche domaine. Beaucoup d'équipes utilisent les deux.
Faut-il des microservices pour faire du DDD ?
Non. Les bounded contexts peuvent être des modules dans une seule base de code. Les microservices sont une façon de faire respecter une frontière de contexte avec le déploiement, pas une exigence du DDD.
Par où commencer avec le DDD ?
Par une conversation. Asseyez-vous avec un expert métier, notez les termes qu'il utilise et ce que chacun veut dire, et faites correspondre le code. Les patterns tactiques, aggregates, repositories, domain events, viennent après, et seulement là où les règles les justifient.
Le plus gros retour du DDD, ce ne sont pas les patterns. C'est le vocabulaire partagé qu'il impose entre ingénieurs et métier. Commencez par la conversation, modélisez ce que vous entendez, et n'ajoutez de la structure que là où les règles la méritent.
Besoin d'aide sur ce sujet ? Développement Full Stack
Découvrir ce service →