Pourquoi le DDD n'est pas un terme à la mode, mais un outil de sauvetage de projet
Chaque développeur Java a déjà vécu cette situation : le code fonctionne, la logique métier est dispersée dans les services, et six mois plus tard, seul l'auteur (et encore, avec difficulté) peut s'y retrouver. Cela vous dit quelque chose ? C'est le classique anemic domain model — quand les entités ne sont que des POJO avec des getters et des setters, et que toute la logique vit dans des services « obèses ».
C'est là qu'intervient le Domain-Driven Design (DDD) — une approche qui place la logique métier au centre du développement, et non les détails techniques. Et non, ce n'est pas une énième méthodologie « théorique ». Le DDD dans les projets Java offre des patrons concrets qui transforment le chaos en une architecture propre et maintenable. Dans cet article, nous allons décortiquer le DDD en pratique : avec du code, des exemples et sans blabla.
DDD Stratégique vs Tactique : que choisir pour un projet Java ?
Le DDD se divise en deux niveaux : stratégique et tactique. Le premier aide à diviser un grand système en Bounded Contexts (contextes) et à structurer la communication entre eux. Le second fournit des blocs de construction concrets pour le code : Entity, Value Object, Aggregate, Repository, Domain Event, etc.
Pour un développeur Java, le DDD tactique est le point de départ. Il donne des outils applicables dès aujourd'hui. Mais sans une compréhension stratégique des contextes, vous risquez de créer un modèle « universel » qui ne convient à personne. Commençons donc par le commencement : analysons les patrons clés sur l'exemple d'un domaine simple mais réel — un système de gestion de commandes.
Les blocs de construction du DDD en Java : de l'Entity à l'Aggregate
1. Entity (Entité) — chacune a sa propre identité
En DDD, une Entity est un objet qui possède un identifiant unique et une ligne de vie continue. Vous pouvez modifier tous ses champs, mais ce sera toujours la même entité. En Java, ce n'est pas un simple POJO — c'est un objet avec un comportement.
Exemple : une commande dans une boutique en ligne. Même si le statut de la commande change, c'est toujours la même commande avec le même ID.
public class Order { private final OrderId id; // Value Object, pas un primitif private OrderStatus status; private List<OrderLineItem> items; private Money totalAmount;
public Order(OrderId id) { this.id = id; this.status = OrderStatus.PENDING; this.items = new ArrayList<>(); this.totalAmount = Money.ZERO; }
public void addItem(Product product, int quantity) { // Règle métier : impossible d'ajouter un article à une commande confirmée if (this.status != OrderStatus.PENDING) { throw new IllegalStateException("Cannot add items to a non-pending order"); } OrderLineItem item = new OrderLineItem(product, quantity); items.add(item); recalculateTotal(); }
public void confirm() { if (items.isEmpty()) { throw new IllegalStateException("Cannot confirm an empty order"); } this.status = OrderStatus.CONFIRMED; // On peut ici déclencher un événement domaine // registerEvent(new OrderConfirmedEvent(this.id)); }
private void recalculateTotal() { this.totalAmount = items.stream() .map(OrderLineItem::getSubtotal) .reduce(Money.ZERO, Money::add); }
// Getters — lecture seule, pas de setters ! public OrderId getId() { return id; } public OrderStatus getStatus() { return status; } public List<OrderLineItem> getItems() { return Collections.unmodifiableList(items); } public Money getTotalAmount() { return totalAmount; }}Remarquez : OrderId n'est pas un Long, mais un Value Object. Pourquoi ? Parce qu'un type primitif n'a pas de sémantique. OrderId peut contenir un préfixe, une date de création ou pr