Guide DDD

Domain-Driven Design
Les principes essentiels du modèle de domaine.

Le Domain-Driven Design (DDD) propose une approche de modélisation logicielle centrée sur le domaine métier. Ce guide présente les building blocks tactiques, les patterns stratégiques et les anti-patterns courants, avec des exemples Java concrets.

Contexte

Pourquoi le Domain-Driven Design ?

Dans un projet logiciel, la complexité provient rarement de la technologie : elle vient du domaine métier. Un système de gestion de commandes, un workflow de paiement, un moteur de tarification : la difficulté réside dans les règles métier, leurs exceptions et leurs interactions.
Le Domain-Driven Design, introduit par Eric Evans en 2003 dans son livre Domain-Driven Design: Tackling Complexity in the Heart of Software, propose une réponse structurée : placer le modèle métier au centre de l'architecture logicielle. Plutôt que de laisser la technique dicter la structure du code, DDD aligne le code sur le langage et les concepts du métier.
En 2026, DDD reste plus pertinent que jamais. Avec la montée des architectures modulaires, du déploiement continu et des systèmes distribués, avoir un modèle de domaine explicite et bien délimité est devenu un prérequis pour la maintenabilité à long terme.

DDD n'est pas une technologie

Le Domain-Driven Design n'est ni un framework, ni une librairie. C'est une approche de modélisation : un ensemble de principes pour structurer le code autour du métier. Il s'applique indépendamment du langage, du framework ou de l'infrastructure.

Building Blocks

Les 6 briques de construction du domaine

DDD définit un vocabulaire précis pour modéliser le domaine métier. Chaque brique a un rôle clair et des règles de conception spécifiques. Les six building blocks tactiques constituent le socle de tout modèle DDD.

Aggregate Root

L'Aggregate Root est le gardien d'un groupe cohérent d'objets. Il constitue la frontière transactionnelle : toute modification des objets qu'il contient passe par lui. C'est la source de vérité pour les invariants métier. Un agrégat Order contient ses OrderLine : on ne crée jamais une ligne sans passer par la commande.

Order.java
public class Order {
private final OrderId id;
private final CustomerId customerId;
private OrderStatus status;
private final List<OrderLine> lines;
private Money totalAmount;
public Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
this.status = OrderStatus.DRAFT;
this.lines = new ArrayList<>();
this.totalAmount = Money.ZERO;
}
/** Ajoute une ligne en vérifiant les invariants métier. */
public void addLine(Product product, int quantity) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot modify a confirmed order");
}
var line = new OrderLine(product, quantity);
lines.add(line);
totalAmount = recalculateTotal();
}
/** Confirme la commande : vérifie qu'elle contient au moins une ligne. */
public OrderPlaced confirm() {
if (lines.isEmpty()) {
throw new IllegalStateException("Cannot confirm an empty order");
}
this.status = OrderStatus.CONFIRMED;
return new OrderPlaced(id, customerId, totalAmount);
}
}

Règle d'or de l'agrégat

Un agrégat protège ses invariants métier. Dans l'exemple ci-dessus, Order empêche l'ajout de lignes après confirmation et interdit la confirmation d'une commande vide. Ces règles sont garanties à chaque appel, quel que soit le code appelant.

classDiagram : agrégat Customer avec ses Value Objects (Email, Address) et son identifier (CustomerId)
Living documentation HexaGlue : diagramme de classes de l'agrégat Customer montrant les relations avec Email, Address et CustomerId

Entity

Une Entity est un objet avec une identité propre qui persiste dans le temps. Deux entités avec les mêmes attributs mais des identités différentes sont des objets différents. L'entité a un cycle de vie : elle est créée, modifiée et éventuellement supprimée. Dans un agrégat Order, chaque OrderLine est une entité : elle a un index ou un identifiant local qui la distingue des autres lignes.

Value Object

Un Value Object est un objet sans identité, défini uniquement par ses attributs. Deux Value Objects avec les mêmes valeurs sont interchangeables. Ils sont immuables : au lieu de modifier un Value Object, on en crée un nouveau. En Java, les records sont parfaitement adaptés aux Value Objects : immuabilité garantie, equals/hashCode basés sur les valeurs, syntaxe concise.

Money.java
/** Objet valeur représentant un montant monétaire. */
public record Money(BigDecimal amount, Currency currency) {
public static final Money ZERO = new Money(BigDecimal.ZERO, Currency.EUR);
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount must be non-negative");
}
Objects.requireNonNull(currency, "Currency is required");
}
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money(amount.add(other.amount), currency);
}
public Money multiply(int quantity) {
return new Money(amount.multiply(BigDecimal.valueOf(quantity)), currency);
}
}

Identifier

Un Identifier est un type wrapper qui encapsule une valeur primitive (UUID, Long) dans un type métier explicite. Au lieu de passer un UUID anonyme, on passe un OrderId : le compilateur empêche de confondre un identifiant de commande avec un identifiant de client.

OrderId.java
/** Identifiant typé pour les commandes. */
public record OrderId(UUID value) {
public OrderId {
Objects.requireNonNull(value, "OrderId value is required");
}
public static OrderId generate() {
return new OrderId(UUID.randomUUID());
}
public static OrderId of(String raw) {
return new OrderId(UUID.fromString(raw));
}
}

Domain Event

Un Domain Event représente un fait métier qui s'est produit dans le domaine. Il est nommé au passé composé (OrderPlaced, PaymentReceived) et porte les données nécessaires aux consommateurs. Les événements découplent les agrégats entre eux : un agrégat émet un événement, d'autres y réagissent.

OrderPlaced.java
/** Événement émis lorsqu'une commande est confirmée. */
public record OrderPlaced(
OrderId orderId,
CustomerId customerId,
Money totalAmount,
Instant occurredAt
) {
public OrderPlaced(OrderId orderId, CustomerId customerId, Money totalAmount) {
this(orderId, customerId, totalAmount, Instant.now());
}
}

Domain Service

Un Domain Service encapsule une logique métier qui n'appartient naturellement à aucune entité. Un PricingService qui calcule le prix d'une commande en fonction de règles de tarification, de remises et de taxes est un bon exemple : cette logique implique plusieurs agrégats et ne peut pas être rattachée à un seul. Le Domain Service est stateless : il reçoit ses données en paramètre et retourne un résultat.

ConceptIdentitéImmuableExemple
Aggregate RootOui (globale)NonOrder
EntityOui (locale)NonOrderLine
Value ObjectNonOuiMoney, Address
IdentifierNon (wrapper)OuiOrderId
Domain EventNonOuiOrderPlaced
Domain ServiceNonStatelessPricingService
Patterns stratégiques

Organiser à grande échelle

Les building blocks tactiques structurent le code à l'intérieur d'un module. Les patterns stratégiques organisent le système dans son ensemble : ils délimitent les contextes, définissent un langage commun et gèrent les relations entre équipes.

Bounded Context

Un Bounded Context est une frontière logique et linguistique autour d'un modèle. À l'intérieur d'un contexte, chaque terme a un sens précis et unique. Le mot "Commande" peut désigner une chose différente dans le contexte "Vente" et dans le contexte "Logistique" : chaque contexte a son propre modèle.

Ubiquitous Language

L'Ubiquitous Language est le vocabulaire partagé entre les développeurs et les experts métier, dans un Bounded Context donné. Chaque concept du code utilise les mêmes termes que le métier. Si l'expert dit "confirmer une commande", la méthode s'appelle confirm(), pas processOrder().

Context Mapping

Le Context Mapping décrit les relations entre Bounded Contexts. Chaque relation suit un pattern : Anti-Corruption Layer (ACL) pour isoler un contexte d'un autre, Shared Kernel pour partager un modèle commun, ou Open Host Service pour exposer une API publique. Ces patterns explicites évitent le couplage accidentel entre équipes.

Anti-patterns

Les pièges à éviter

Le DDD est souvent mal appliqué. Trois anti-patterns reviennent régulièrement dans les projets Java et minent la qualité du modèle de domaine.

Anemic Domain Model

Le modèle anémique est le piège le plus courant : les entités ne contiennent que des getters/setters, et toute la logique métier se retrouve dans des services. Résultat : le domaine n'est qu'un conteneur de données passif. Si votre classe Order ne contient aucune règle métier, c'est un modèle anémique.

God Aggregate

Le God Aggregate est un agrégat qui englobe trop d'entités et de responsabilités. Un agrégat Customer qui contient les commandes, les paiements, les adresses et les préférences est trop gros : il crée des conflits de concurrence et rend le système difficile à faire évoluer. Chaque agrégat doit avoir une frontière transactionnelle minimale.

Primitive Obsession

La Primitive Obsession consiste à utiliser des types primitifs (Long, String) là où un type métier serait plus expressif. Un Long orderId peut être confondu avec un Long customerId : le compilateur ne protège rien. Un OrderId typé élimine cette classe d'erreurs.

DDD et HexaGlue

De la théorie à la détection automatique

HexaGlue applique les principes DDD de manière concrète : il analyse votre code Java et détecte automatiquement chaque building block. Pas d'annotations à ajouter, pas de configuration obligatoire : les conventions de nommage et la structure du code suffisent.
Concept DDDCe que HexaGlue détecteEn savoir plus
Aggregate RootClasse avec un champ d'identité typéGénération JPA
EntityClasse membre d'un agrégatGénération JPA
Value ObjectRecord immuable, sans identitéClassification
IdentifierRecord wrappant un type primitifClassification
Domain EventRecord avec suffixe EventLiving Doc
Domain ServiceClasse stateless dans le domaineAudit

Classification automatique et traçable

HexaGlue classifie automatiquement chaque building block DDD à partir de la structure de votre code. Chaque décision est traçable : le rapport d'audit indique la règle qui a déclenché la classification et le niveau de confiance associé.

Inventaire architectural : les 11 rôles DDD détectés par HexaGlue
Rapport d'audit HexaGlue : grille d'inventaire montrant les 11 rôles DDD classifiés avec compteurs (Aggregates, Value Objects, Identifiers, Ports)
Living documentation : détail de l'agrégat Account avec identité et propriétés
Living documentation HexaGlue : agrégat Account avec identity details, properties (id, ownerName, balance, transactions) et niveau de confiance

Le DDD s'applique naturellement dans une architecture hexagonale : le domaine est au centre, protégé des détails d'infrastructure par les ports et les adapters. HexaGlue détecte les deux : les building blocks DDD et la structure hexagonale. Vous pouvez voir ces principes appliqués à un projet réel dans l'étude de cas e-commerce.

Le DDD structure votre domaine métier.
HexaGlue en prend soin.

Voyez la classification DDD en action sur un projet réel ou commencez avec le tutoriel.