Mise en œuvre

VoletPratique L’architecture qui va avec, les pièges qui font échouer les adoptions, le chemin dans un code existant — et les cas où il faut s’abstenir.

L’architecture qui va avec

DDD ne prescrit aucune architecture. Il impose une seule contrainte, mais elle est forte : le domaine ne doit dépendre de rien. Ni ORM, ni framework, ni client HTTP, ni annotation.

C’est exactement ce que fournit l’architecture hexagonale, et c’est pourquoi les deux sont presque toujours enseignées ensemble. La correspondance est directe :

Notion DDDEmplacement dans l’hexagone
Entités, objets valeur, agrégatsLe domaine, au centre
Services de domaineLe domaine
Interfaces de dépôt, de notificationPorts de sortie, déclarés par le domaine
Services applicatifs (cas d’usage)Ports d’entrée, à la frontière
Événements de domaineLe domaine ; leur publication est un port
Implémentations de dépôt, ORM, SQLAdaptateurs sortants
Contrôleurs HTTP, consommateurs de fileAdaptateurs entrants
Couches d’anticorruptionAdaptateurs, à la frontière de contexte

Un contexte délimité correspond alors à un hexagone, avec son domaine, ses ports et ses adaptateurs. Un système à quatre contextes est composé de quatre hexagones, qui peuvent parfaitement cohabiter dans un même déploiement.

crates/ ou packages/
  reservation/            ← un contexte délimité = un hexagone
    domaine/              agrégats, objets valeur, ports, événements
    application/          cas d'usage
    infrastructure/       adaptateurs SQL, HTTP, bus
  fidelite/               ← un autre contexte, un autre modèle
    domaine/
    application/
    infrastructure/
  partage/                ← noyau partagé : identifiants, objets valeur stables
  app/                    racine de composition, câblage
Tip

Le dossier partage/ est à surveiller comme le lait sur le feu. C’est un noyau partagé : légitime pour des identifiants et quelques objets valeur vraiment universels, catastrophique dès qu’il commence à contenir des règles métier. S’il grossit, le modèle unifié revient par la fenêtre.

Le modèle anémique

C’est l’échec numéro un, et il est d’autant plus dangereux qu’il ressemble à du DDD. Martin Fowler l’a nommé anemic domain model : un modèle qui a la forme d’un modèle objet — des classes, des dossiers entities/, repositories/ — mais dont les objets ne contiennent aucun comportement.

Les symptômes se reconnaissent en trente secondes :

  • des classes composées uniquement d’accesseurs et de mutateurs ;
  • des services qui font tout le travail, en lisant et écrivant les champs des entités depuis l’extérieur ;
  • la même règle métier vérifiée à trois endroits, parce qu’aucun endroit ne l’héberge ;
  • l’impossibilité de répondre à « où est la règle X ? » sans chercher.

Le test décisif : une entité peut-elle être placée dans un état invalide par du code extérieur ? Si oui, elle ne protège rien, et le modèle est anémique.

Trois causes récurrentes, dont aucune n’est de la négligence :

Les habitudes d’ORM. Un objet mappé exige souvent un constructeur sans argument, des champs mutables et des accesseurs publics — c’est-à-dire précisément l’inverse d’un agrégat encapsulé.

Les DTO partout. Quand un objet doit être sérialisable de bout en bout, il perd son comportement en route et devient une structure de données.

Le service comme réflexe. Voir Services, dépôts et fabriques : le service est si commode qu’il attire tout. La règle de priorité — objet valeur, puis entité, puis service — est le seul garde-fou.

Pour en sortir, la méthode qui fonctionne est incrémentale et se mesure : prendre une règle, la déplacer depuis le service vers l’objet qui détient les données concernées, supprimer le mutateur devenu inutile, recommencer. Le premier signe de progrès est la disparition des set.

Les autres pièges

Le tactique sans le stratégique. Le plus fréquent. On obtient une arborescence entities/ repositories/ services/, un vocabulaire inchangé, et un coût supplémentaire sans aucun bénéfice — parce que le bénéfice venait des frontières et du langage, pas des dossiers. C’est ce que l’ordre de lecture proposé dans cette section cherche à éviter.

Le DDD partout. Appliquer les patrons tactiques aux sous-domaines de support et génériques. Voir Sous-domaines : c’est une erreur de dosage, et elle discrédite l’approche auprès de l’équipe plus sûrement qu’un échec technique.

Contexte délimité = microservice, immédiatement. Le découpage réseau fige des frontières encore incertaines. Un contexte mal placé dans un monolithe est un refactoring d’après-midi ; entre deux services, c’est un projet trimestriel.

La base de données partagée. Deux contextes qui lisent les mêmes tables ne sont pas deux contextes. Le schéma devient un contrat implicite que plus personne ne peut faire évoluer.

Modéliser tout le métier. L’exhaustivité n’est pas l’objectif. Le but est un modèle utile pour les décisions du cœur, pas une représentation fidèle de l’entreprise.

La réécriture globale. « On va refaire proprement en DDD » a un taux d’échec proche de un. Le chemin praticable est celui de l’étranglement progressif.

L’absence d’experts. Rédhibitoire, et rappelé une dernière fois : sans accès régulier à quelqu’un qui connaît le domaine et peut arbitrer, il ne reste que les patrons.

Le chemin dans un code existant

L’ordre suivant est celui qui échoue le moins souvent.

  1. Cartographier ce qui existe, honnêtement — y compris les grandes boules de boue. On ne peut pas décider sans savoir où l’on est.
  2. Identifier le cœur de métier. Un seul, celui qui justifie l’effort.
  3. Faire un event storming sur ce périmètre. Deux heures, et le langage commence à exister.
  4. Isoler un contexte, dans le monolithe. Un module, un paquet, une crate — avec une frontière vérifiée par un test d’architecture, pas par une convention.
  5. Poser une couche d’anticorruption entre ce module et le reste du code existant. C’est elle qui permet de progresser sans attendre que le legacy change.
  6. Écrire le nouveau code du cœur dans ce module, avec les patrons tactiques — et laisser le reste tranquille.
  7. Étrangler progressivement : chaque nouvelle fonctionnalité du périmètre passe par le module ; l’ancien chemin se vide et meurt de lui-même.
  8. Ne distribuer que si un besoin le réclame — échelle, cadence de déploiement, autonomie d’équipe. Jamais parce que la frontière existe.
Note

Le point 4 mérite d’être outillé plutôt que promis. import-linter en Python, dependency-cruiser en TypeScript, ArchUnit en Java, et en Rust la simple séparation en crates — le domaine dont le Cargo.toml ne déclare aucune dépendance d’infrastructure. Une frontière qui n’est pas vérifiée par la CI est une frontière qui aura fui dans six semaines.

Quand ne pas faire de DDD

Cette liste est aussi importante que le reste, et l’honnêteté sur ce point est ce qui rend l’approche crédible.

  • Le domaine est mince. L’application déplace des données, elle n’en décide rien. Une architecture en couches assumée est meilleure : plus courte, plus lisible, moins chère.
  • Pas d’accès aux experts. Le prérequis bloquant.
  • Durée de vie courte. Un outil interne pour six mois n’amortira jamais l’investissement.
  • La complexité est ailleurs. Beaucoup de systèmes ont une complexité technique — débit, latence, volume, temps réel — et un métier trivial. DDD n’aide pas sur ce terrain, et ne prétend pas le faire.
  • L’équipe découvre tout en même temps. DDD plus hexagonal plus événements plus microservices, sur un projet à échéance : c’est le scénario d’échec le plus documenté. Un élément à la fois, en commençant par le langage — qui est gratuit.

Ce qui n’interdit pas d’emprunter des morceaux. Le langage omniprésent et les objets valeur sont rentables presque partout, et ne coûtent presque rien. C’est l’entrée la plus raisonnable dans l’approche.

Comment savoir que ça marche

Quatre signaux observables, préférables à toute auto-évaluation :

  • Le métier et l’équipe emploient les mêmes mots, et une méthode du code peut être lue à voix haute en réunion.
  • Une nouvelle règle atterrit à un seul endroit, et l’équipe sait lequel avant d’ouvrir l’éditeur.
  • Les tests du domaine tournent sans infrastructure, en quelques millisecondes.
  • Les frontières tiennent : un changement de règle dans un contexte ne provoque pas de compilation cassée dans un autre.

Si aucun de ces quatre points n’a bougé après trois mois, ce qui a été adopté est la forme, pas l’approche.

Le vocabulaire, en une page

TermeEn une phrasePage
DomaineL’activité de l’entrepriseSous-domaines
Sous-domaineUne partie de cette activité — cœur, support ou géneriqueSous-domaines
Langage omniprésentUn vocabulaire unique, du métier jusqu’au codeLangage omniprésent
Contexte délimitéLa frontière où un mot a exactement un sensContextes délimités
Carte de contextesLes relations entre contextes, techniques et politiquesCartographie
Couche d’anticorruptionLa traduction qui protège un modèle d’un voisinCartographie
Noyau partagéLe peu de modèle que deux contextes co-détiennentCartographie
Objet valeurDéfini par ses attributs, immuable, sans identitéEntités et objets valeur
EntitéDéfinie par son identité, qui persiste à travers ses étatsEntités et objets valeur
AgrégatUne frontière de cohérence, donc de transactionAgrégats
Racine d’agrégatLe seul point d’entrée de l’agrégatAgrégats
InvariantUne règle vraie à chaque instant, sans délaiAgrégats
Service de domaineUne règle qui croise plusieurs agrégatsServices et dépôts
Service applicatifUn cas d’usage : orchestrer, sans règleServices et dépôts
DépôtL’illusion d’une collection d’agrégatsServices et dépôts
FabriqueUne construction complexe, ou une reconstitutionServices et dépôts
Événement de domaineUn fait métier survenu, immuable, au passéÉvénements
Événement d’intégrationLe même fait, traduit pour l’extérieurÉvénements
Cohérence différéeDeux agrégats d’accord un peu plus tardAgrégats
Event stormingL’atelier qui produit tout ce qui précèdeEvent storming

Références