Agrégats
VoletTactique La notion la plus difficile de DDD, et celle qui rapporte le plus. C’est aussi celle qui est le plus souvent mal comprise — presque toujours de la même façon.
La question à laquelle un agrégat répond
Ce n’est pas « comment regrouper mes classes ». C’est :
Quel ensemble de données doit être cohérent à chaque instant, et donc modifié dans une seule transaction ?
Un agrégat est un groupe d’entités et d’objets valeur traité comme une unité de cohérence. Il possède une racine — une entité — qui est le seul point d’entrée : rien à l’intérieur n’est accessible directement de l’extérieur.
Reformulé de la manière la plus opératoire possible : un agrégat est une frontière de transaction. Tout ce qui doit être vrai en permanence est dedans ; tout ce qui peut être vrai « dans une seconde » est dehors.
Warning
L’erreur canonique consiste à construire les agrégats à partir des noms du domaine et des relations entre eux — « une commande a des lignes, un client a des commandes, donc… ». On obtient un agrégat qui grossit jusqu’à englober le modèle entier, puisque tout est relié à tout. Les agrégats se construisent à partir des invariants, jamais des relations.
L’invariant, seule justification
Un invariant est une règle qui doit être vraie à chaque instant, sans exception et sans délai. Deux exemples pour sentir la différence :
- « Le total d’une commande est la somme de ses lignes. » Invariant : il ne peut pas être faux une milliseconde. Les lignes sont donc dans l’agrégat commande.
- « Le chiffre d’affaires mensuel du client est à jour. » Pas un invariant : que ce total soit recalculé deux secondes plus tard ne dérange personne. Le client est donc hors de l’agrégat commande.
La seule raison légitime de placer deux choses dans le même agrégat est un invariant qui les lie. S’il n’y en a pas, ce sont deux agrégats.
Tip
La question qui tranche, et qu’il faut poser à l’expert du domaine et non à l’équipe technique : « si ces deux informations se contredisent pendant deux secondes, que se passe-t-il ? » Si la réponse est « rien, ça se rattrape », la cohérence peut être différée. Si c’est « on vend une place déjà vendue », c’est un invariant. La frontière d’un agrégat est une décision métier, pas un arbitrage technique.
Les quatre règles
Formulées par Vaughn Vernon dans Effective Aggregate Design, ce sont les règles les plus utiles de tout le DDD tactique.
1. Protéger les vrais invariants dans une frontière de cohérence
Ce qui est à l’intérieur est cohérent à chaque commit. Ce qui est à l’extérieur peut attendre. C’est la règle dont les trois autres découlent.
2. Concevoir de petits agrégats
Un gros agrégat coûte cher sur trois plans : il faut le charger entièrement pour modifier un détail, il verrouille beaucoup de données donc génère de la contention, et il finit par contenir des règles qui n’ont rien à voir entre elles.
Le contre-exemple classique est l’agrégat Client contenant ses commandes. Un
client fidèle a dix mille commandes ; ajouter une ligne à la dernière charge les
dix mille. Et deux commandes simultanées du même client entrent en conflit sans
aucune raison métier.
La règle pratique : viser un agrégat composé d’une racine et de quelques objets valeur. Une collection d’entités filles dans un agrégat doit être bornée par le métier — les lignes d’une commande, oui ; les commandes d’un client, non.
3. Référencer les autres agrégats par identité
Pas de référence d’objet vers une autre racine : un identifiant.
C’est ce qui garde les agrégats petits, empêche le chargement en cascade, rend la frontière de transaction explicite, et permet de déplacer un agrégat vers un autre contexte sans démonter le modèle.
4. Modifier les autres agrégats en cohérence différée
Corollaire direct : une transaction ne modifie qu’un agrégat. Si une action doit en affecter deux, le second est mis à jour après coup, via un événement de domaine.
C’est la règle qui surprend le plus, et c’est la plus importante en pratique. Elle n’est pas un compromis technique consenti à la performance : elle reflète le fait que la plupart des règles métier tolèrent un délai, et que celles qui ne le tolèrent pas définissent justement une frontière d’agrégat.
Un exemple travaillé
Une réservation de places de spectacle. Le métier énonce quatre règles :
- Une réservation ne peut pas contenir plus de dix places.
- Le montant total d’une réservation est la somme de ses lignes.
- Une séance ne peut pas être réservée au-delà de sa capacité.
- Le nombre de points de fidélité du client reflète ses réservations confirmées.
Classons-les. Les règles 1 et 2 portent sur une réservation seule : invariants
internes, donc les lignes sont dans l’agrégat Reservation. La règle 3 porte sur
une séance et doit être vraie à chaque instant — sans quoi on vend deux fois
la même place : c’est un invariant, mais d’un autre agrégat, Seance. La règle 4
tolère un délai : cohérence différée.
Trois agrégats, donc — Reservation, Seance, Client — et non un seul objet
relié par des références.
Quatre points méritent d’être relevés dans ce code.
LigneReservation n’est jamais exposée. Il n’existe aucun moyen d’obtenir une
ligne et de la modifier : toute évolution passe par ajouterPlaces, qui vérifie
l’invariant. Une collection retournée telle quelle — un getLignes() public —
suffirait à annuler la protection.
seanceId et clientId sont des identifiants, pas des objets. La réservation
ne peut pas modifier la séance, ce qui est exactement l’effet recherché.
L’invariant est vérifié avant la mutation, dans la même méthode. Il n’y a pas de moment où l’agrégat est incohérent.
La règle sur les points de fidélité n’est pas appliquée ici : un événement est enregistré, et c’est un autre morceau du système qui s’en chargera.
Note
La version Rust obtient gratuitement une garantie que les deux autres demandent en
discipline : LigneReservation étant privée au module, il est impossible de la
manipuler depuis l’extérieur — le compilateur refuse. En Python, le _ initial est
une convention ; en TypeScript, private disparaît à l’exécution. L’encapsulation
d’agrégat est le point où le système de visibilité de Rust s’aligne le mieux avec
DDD.
L’invariant qui porte sur une autre racine
Reste la règle 3 : la capacité de la séance. Elle doit être immédiate, mais elle
concerne Seance. Comment faire, puisqu’une transaction ne modifie qu’un agrégat ?
La réponse est de choisir où vit l’invariant, et il n’y a que trois options honnêtes.
| Option | Mécanisme | Quand |
|---|---|---|
Déplacer la décision dans Seance | seance.reserver(places) produit une allocation ; la réservation la référence | La capacité est un vrai invariant strict — le cas ici |
| Fusionner les deux agrégats | Un seul agrégat Seance contenant ses réservations | Petite jauge, forte contention acceptable |
| Différer et compenser | On accepte le dépassement, on détecte, on rembourse | Surréservation assumée (aérien, hôtellerie) |
Le choix ne se déduit pas du code : il se demande au métier. Et la troisième option, qui paraît la plus laide, est celle que les compagnies aériennes appliquent depuis cinquante ans — parce que le coût du verrou global dépasse celui de la compensation.
Warning
Si un invariant strict traverse deux agrégats de deux contextes différents, c’est un signal fort que la frontière de contexte est mal placée. La conséquence serait une transaction distribuée, et aucune sophistication technique ne rend cela souhaitable.
Concurrence et verrou optimiste
Puisque l’agrégat est l’unité de cohérence, c’est sur lui que porte le contrôle de concurrence — pas sur les lignes ni sur les tables.
Le mécanisme standard est le verrou optimiste : la racine porte un numéro de version, incrémenté à chaque enregistrement. Deux modifications concurrentes de la même réservation aboutissent au rejet de la seconde, qui doit relire et réessayer.
C’est aussi une raison supplémentaire de faire de petits agrégats : plus l’agrégat est gros, plus il attire de modifications concurrentes, et plus les conflits sont fréquents sur des données qui n’avaient rien à voir entre elles.
Comment les trouver
Une méthode qui fonctionne, dans cet ordre :
- Lister les règles du métier, avec leurs mots, sans se soucier des objets.
- Trier ces règles en deux tas : celles qui doivent être vraies à chaque instant, et celles qui tolèrent un délai. Le second tas est en général le plus gros — c’est une bonne nouvelle.
- Grouper les données mentionnées par chaque règle immédiate. Chaque groupe est un agrégat candidat.
- Choisir la racine : l’entité par laquelle le métier désigne naturellement le groupe.
- Vérifier la taille. Si un agrégat contient une collection non bornée, le découper.
- Relier le reste par identité, et traiter les règles différées par des événements.
Un event storming produit directement la matière des deux premières étapes, ce qui explique que les deux pratiques aillent presque toujours ensemble.
Ce qu’un agrégat n’est pas
- Un graphe d’objets ORM. Si votre agrégat est défini par les relations
@OneToManyde l’ORM, ce sont les tables qui décident du modèle. Le chargement paresseux à l’intérieur d’un agrégat est un symptôme : un agrégat correctement dimensionné se charge en entier. - Un module. Un agrégat est petit — une racine et quelques objets valeur. Un contexte délimité contient plusieurs agrégats.
- Une unité de lecture. Un agrégat est conçu pour écrire en respectant des invariants. Les besoins d’affichage — une liste, un tableau de bord, une recherche multicritère — n’ont pas à passer par lui ; c’est le point de départ de la séparation lecture/écriture, dont CQRS est la forme aboutie.
Références
- Vaughn Vernon, Effective Aggregate Design — https://kalele.io/blog-posts/, trois articles, la référence sur le sujet
- Eric Evans, Domain-Driven Design, chapitre 6 — Aggregates
- https://martinfowler.com/bliki/DDD_Aggregate.html