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.
Pourquoi le Domain-Driven Design ?
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.
Les 6 briques de construction du domaine
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.
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.

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.
/** 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.
/** 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.
/** É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.
| Concept | Identité | Immuable | Exemple |
|---|---|---|---|
| Aggregate Root | Oui (globale) | Non | Order |
| Entity | Oui (locale) | Non | OrderLine |
| Value Object | Non | Oui | Money, Address |
| Identifier | Non (wrapper) | Oui | OrderId |
| Domain Event | Non | Oui | OrderPlaced |
| Domain Service | Non | Stateless | PricingService |
Organiser à grande échelle
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.
Les pièges à éviter
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.
De la théorie à la détection automatique
| Concept DDD | Ce que HexaGlue détecte | En savoir plus |
|---|---|---|
| Aggregate Root | Classe avec un champ d'identité typé | Génération JPA |
| Entity | Classe membre d'un agrégat | Génération JPA |
| Value Object | Record immuable, sans identité | Classification |
| Identifier | Record wrappant un type primitif | Classification |
| Domain Event | Record avec suffixe Event | Living Doc |
| Domain Service | Classe stateless dans le domaine | Audit |
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é.


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.