Java · Conception métier

DDD en Java : faire coïncider le modèle et le langage métier

Délimiter le langage métier et choisir entités, value objects, agrégats et événements sans réduire le DDD aux packages.

Une même « commande » peut désigner, dans une équipe, un formulaire en attente; dans une autre, une demande déjà acceptée; dans la base, un enregistrement avec un statut. Si ces sens se confondent, le code paraît cohérent tout en laissant les règles importantes dans des conversations, des contrôleurs ou des scripts. Domain-Driven Design (DDD) propose de construire et d’affiner un modèle avec les personnes qui connaissent le domaine puis de rendre ce modèle explicite dans le langage et le logiciel.

Pour commencer, prenez un scénario métier à discuter avec une personne qui le connaît. Sur une feuille ou dans une note, écrivez ce que « commande » signifie dans chaque contexte, puis une règle de transition. Ce premier exercice convient à un développeur et à son interlocuteur métier; les exemples Java qui suivent demandent de savoir lire des classes et des records.

Dans le scénario fictif Checkout, proposez : « une commande brouillon contient des lignes SKU/quantité; la placer exige au moins une ligne ». Pour Catalogue, décrivez plutôt le produit référencé par le SKU et demandez si ce contexte a besoin de connaître une commande. S’il n’en a pas besoin, évitez d’y introduire ce concept seulement pour partager une classe Java. Confrontez ces définitions et la règle au spécialiste métier : qui peut placer la commande, à quel moment, et que doit-il se passer pour une liste vide ?

Le résultat recherché est une définition par contexte et un invariant que chacun peut reformuler. Si les réponses divergent, clarifiez le vocabulaire avant de choisir les classes. Les patterns tactiques expriment ensuite les règles du modèle.

Un modèle s’applique dans un contexte précis

Eric Evans décrit le DDD comme une collaboration entre praticiens du domaine et praticiens du logiciel, avec un langage partagé à l’intérieur d’un contexte explicitement délimité. Sa synthèse sépare aussi les dimensions stratégique et tactique : elles se complètent et doivent être pensées ensemble (Domain-Driven Design Reference).

Le langage ubiquitaire est le vocabulaire commun utilisé pour discuter du modèle et pour nommer les éléments du code. « Placer une commande », « réserver un article » ou « autoriser un paiement » doivent désigner des comportements compréhensibles par les personnes concernées, pas seulement des noms choisis par les développeurs. Quand une conversation révèle qu’un même terme recouvre deux règles, le modèle doit rendre cette distinction visible.

Ce langage est ubiquitaire dans un contexte délimité (bounded context) qui fixe où le modèle et ses définitions s’appliquent. Dans notre application fictive de passage de commande, le contexte Catalogue peut parler d’un produit et de son libellé; le contexte Checkout représente une ligne avec son SKU et sa quantité. Le terme « produit » n’a donc pas nécessairement le même rôle dans les deux modèles.

Un bounded context peut partager un déploiement avec d’autres contextes. Il peut aussi être représenté par des contrats d’API, des événements ou une traduction explicite entre modèles. Les équipes décident alors quels termes traversent la frontière et qui fait évoluer le contrat. Le bounded context suit le sens du modèle; un package Java ou un microservice ne le définit pas à lui seul.

Stratégie et tactique s’informent mutuellement

La dimension stratégique aide à identifier les modèles utiles, leurs frontières et les relations entre eux. Elle demande de travailler avec les spécialistes du domaine, de clarifier les termes ambigus et de décider comment les contextes échangent leurs informations. Elle évite, par exemple, d’imposer une représentation unique de Product ou Order à des besoins qui divergent.

La dimension tactique exprime les règles à l’intérieur d’un bounded context au moyen d’entités, de value objects, d’agrégats, de repositories, de services de domaine et d’événements. La stratégie oriente ces choix, tandis que le travail tactique peut révéler qu’une frontière ou un terme métier doit être revu. Les deux dimensions progressent ensemble.

Dans l’exemple fictif Checkout, PlaceOrder accepte des lignes décrites par un SKU et une quantité. Les extraits présentent les types et comportements du modèle.

Le port reçoit un PlaceOrderCommand composé de OrderLineInput(sku, quantity). Le cas d’usage convertit ces entrées en OrderLine, crée l’agrégat avec un identifiant injecté, le fait passer à PLACED puis demande au repository de le sauvegarder. Cette partie se concentre sur le modèle métier; le contrat applicatif sera relié au repository plus bas. Les imports et types auxiliaires des extraits sont omis.

Entité et value object : identité ou valeur ?

Une entité se reconnaît à son identité et à sa continuité dans le temps. Order reste la même commande lorsque son statut évolue; deux commandes distinctes peuvent avoir les mêmes lignes.

record OrderId(String value) {
    OrderId {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("L’identifiant est obligatoire");
        }
    }
}

OrderId est un value object plausible : son égalité découle de sa valeur et il n’a pas de cycle de vie métier indépendant. Un value object représente ainsi une valeur définie par ses propriétés, sans identité propre dans le modèle. Il est généralement immuable et validé à sa création.

Pour garder la forme du contrat de commande utilisé dans ce corpus, OrderLine reçoit un int et protège lui-même sa validité :

record OrderLine(String sku, int quantity) {
    OrderLine {
        if (sku == null || sku.isBlank()) {
            throw new IllegalArgumentException("Le SKU est obligatoire");
        }
        if (quantity <= 0) {
            throw new IllegalArgumentException("La quantité doit être positive");
        }
    }
}

Dans ce modèle, OrderLine est un value object : son égalité est définie par le SKU et la quantité. Le métier ne suit pas chaque ligne séparément. Un record Java fournit une égalité fondée sur ses composants; ses composants restent à choisir selon la sémantique du domaine. Les records ne garantissent ni immutabilité profonde ni validation métier automatique. La documentation Oracle décrit les record classes de Java 21.

L’agrégat dessine une frontière de cohérence

Un agrégat regroupe des objets soumis à un ensemble d’invariants; sa racine est le point d’entrée pour les modifications qui concernent l’ensemble. Ici, Order est à la fois une entité et la racine de l’agrégat. Elle garde ses lignes à l’intérieur.

enum OrderStatus { DRAFT, PLACED }

record OrderPlaced(OrderId orderId) {}

final class Order {
    private final OrderId id;
    private final List<OrderLine> lines;
    private final List<OrderPlaced> events = new ArrayList<>();
    private OrderStatus status;

    private Order(OrderId id, List<OrderLine> lines) {
        this.id = id;
        this.lines = List.copyOf(lines);
        this.status = OrderStatus.DRAFT;
    }

    static Order create(OrderId id, List<OrderLine> lines) {
        return new Order(id, lines);
    }

    void place() {
        if (status != OrderStatus.DRAFT) {
            throw new IllegalStateException("Seule une commande brouillon peut être placée");
        }
        if (lines.isEmpty()) {
            throw new IllegalStateException("Une commande vide ne peut pas être placée");
        }
        status = OrderStatus.PLACED;
        events.add(new OrderPlaced(id));
    }

    OrderId id() { return id; }
    OrderStatus status() { return status; }
    List<OrderPlaced> events() { return List.copyOf(events); }
}

Order.create construit une commande en état DRAFT, même si sa liste est vide. place() vérifie d’abord l’état et la présence d’au moins une ligne puis passe à PLACED et enregistre OrderPlaced. Le cas d’usage peut sauvegarder l’agrégat après cette transition. La règle est ainsi protégée quel que soit le chemin d’entrée.

La frontière d’agrégat ne signifie ni « une table SQL » ni « un objet contenant tout ce qui a un lien ». Evans recommande d’appliquer les règles de cohérence dans l’agrégat même et de traiter les mises à jour entre agrégats de façon asynchrone. Cela implique une cohérence parfois différée : deux agrégats liés peuvent temporairement refléter des états différents. Le cas d’usage doit prévoir comment rendre cette situation compréhensible et traiter un échec. La taille de l’agrégat se décide selon les invariants qui doivent être garantis ensemble.

Repository : charger et sauvegarder une racine

Un repository fournit une abstraction pour retrouver ou sauvegarder des objets du modèle, souvent des racines d’agrégat. Dans cet exemple, OrderRepository.save(order) suffit au scénario de création. Son adaptateur traduit le modèle en JPA, SQL ou une autre représentation.

L’article Clean Architecture en Java : placer les règles métier face à Spring développe la direction de dépendance entre ce contrat et son adaptateur.

Événement de domaine : nommer un fait métier

Un événement de domaine nomme un fait qui s’est produit dans le langage du modèle. OrderPlaced décrit la transition de l’exemple; son nom indique un événement passé. Quand ce fait métier est transmis à d’autres composants, le contrat d’intégration définit le format et les garanties de cohérence et de livraison.

Java et Spring dans l’architecture métier

Une application Spring Boot peut organiser ses packages par contexte (checkout, catalogue, payment) et laisser Spring câbler les composants. Cette structure aide l’équipe à repérer les modèles et leurs échanges. Le langage métier fixe le sens des termes dans chaque contexte.

Les annotations JPA dans les objets du domaine sont un choix de couplage. Les garder à l’extérieur préserve l’indépendance vis-à-vis de l’ORM, au prix de code de conversion. Utiliser directement des entités JPA réduit les conversions mais les contraintes de persistance peuvent alors influencer le modèle.

Un pattern tactique est utile lorsqu’il clarifie ou protège une règle identifiée. Dans un domaine simple, une structure plus directe peut rester plus lisible. La décision pratique consiste à délimiter les modèles avec les spécialistes puis à choisir les patterns qui expriment leurs règles.

Sources techniques

  1. Domain-Driven Design Reference: Definitions and Pattern SummariesEric Evans / Domain Language
  2. Domain LanguageDomain Language
  3. Java SE 21 — Record ClassesOracle