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 DDD | Emplacement dans l’hexagone |
|---|---|
| Entités, objets valeur, agrégats | Le domaine, au centre |
| Services de domaine | Le domaine |
| Interfaces de dépôt, de notification | Ports de sortie, déclarés par le domaine |
| Services applicatifs (cas d’usage) | Ports d’entrée, à la frontière |
| Événements de domaine | Le domaine ; leur publication est un port |
| Implémentations de dépôt, ORM, SQL | Adaptateurs sortants |
| Contrôleurs HTTP, consommateurs de file | Adaptateurs entrants |
| Couches d’anticorruption | Adaptateurs, à 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.
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.
- 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.
- Identifier le cœur de métier. Un seul, celui qui justifie l’effort.
- Faire un event storming sur ce périmètre. Deux heures, et le langage commence à exister.
- 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.
- 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.
- Écrire le nouveau code du cœur dans ce module, avec les patrons tactiques — et laisser le reste tranquille.
- Étrangler progressivement : chaque nouvelle fonctionnalité du périmètre passe par le module ; l’ancien chemin se vide et meurt de lui-même.
- 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
| Terme | En une phrase | Page |
|---|---|---|
| Domaine | L’activité de l’entreprise | Sous-domaines |
| Sous-domaine | Une partie de cette activité — cœur, support ou génerique | Sous-domaines |
| Langage omniprésent | Un vocabulaire unique, du métier jusqu’au code | Langage omniprésent |
| Contexte délimité | La frontière où un mot a exactement un sens | Contextes délimités |
| Carte de contextes | Les relations entre contextes, techniques et politiques | Cartographie |
| Couche d’anticorruption | La traduction qui protège un modèle d’un voisin | Cartographie |
| Noyau partagé | Le peu de modèle que deux contextes co-détiennent | Cartographie |
| Objet valeur | Défini par ses attributs, immuable, sans identité | Entités et objets valeur |
| Entité | Définie par son identité, qui persiste à travers ses états | Entités et objets valeur |
| Agrégat | Une frontière de cohérence, donc de transaction | Agrégats |
| Racine d’agrégat | Le seul point d’entrée de l’agrégat | Agrégats |
| Invariant | Une règle vraie à chaque instant, sans délai | Agrégats |
| Service de domaine | Une règle qui croise plusieurs agrégats | Services et dépôts |
| Service applicatif | Un cas d’usage : orchestrer, sans règle | Services et dépôts |
| Dépôt | L’illusion d’une collection d’agrégats | Services et dépôts |
| Fabrique | Une construction complexe, ou une reconstitution | Services et dépôts |
| Événement de domaine | Un fait métier survenu, immuable, au passé | Événements |
| Événement d’intégration | Le même fait, traduit pour l’extérieur | Événements |
| Cohérence différée | Deux agrégats d’accord un peu plus tard | Agrégats |
| Event storming | L’atelier qui produit tout ce qui précède | Event storming |
Références
- https://martinfowler.com/bliki/AnemicDomainModel.html
- https://martinfowler.com/bliki/StranglerFigApplication.html
- https://github.com/ddd-crew/ddd-starter-modelling-process — un ordre d’adoption détaillé, en neuf étapes
- Vlad Khononov, Learning Domain-Driven Design, chapitres 10 à 12 — l’adoption dans l’existant