Conception d'Aggregates : Pièges Courants et Solutions
Comment concevoir des aggregates robustes qui maintiennent les invariants métier sans sacrifier la performance. Exemples de dimensionnement, composition et gestion du cycle de vie.
Les Aggregates. Tout le monde en parle. Personne ne sait vraiment où tracer la frontière.
Vous connaissez ce moment, en conception, où quelqu'un demande : "On met quoi dans l'Aggregate Order ?"
Réponses typiques :
Spoiler : la troisième réponse est la seule honnête.
Les livres DDD vous donnent la règle : "L'Aggregate protège ses invariants métier dans une frontière transactionnelle." Très bien. Mais quels invariants ? Quelle frontière ?
Ce qu'on observe : soit des Aggregates obèses de 800 lignes qui font tout, soit des Aggregates anémiques qui ne protègent rien. Rarement le bon équilibre.
Explorons comment décider. Sans dogme.
Le consensus DDD classique :
Eric Evans dans le Blue Book : "Un Aggregate est un cluster d'objets associés que l'on traite comme une unité pour les changements de données."
Vaughn Vernon dans le Red Book : "Modélisez de vrais invariants dans la cohérence. Modélisez de petits Aggregates."
Les règles établies :
Ces règles sont valides. Le problème ?
Elles ne vous disent pas comment identifier la frontière.
C'est comme dire "construisez une maison solide" sans expliquer où placer les murs porteurs.
Symptômes :
Pourquoi ça arrive :
On raisonne en "entités qui vont ensemble fonctionnellement". Une commande "a besoin" du client, des produits, du stock pour être complète. On fourre tout dedans.
Résultat : un Aggregate qui connaît tout le système. Un couplage total. Une modification de la logique de paiement impacte l'Aggregate Order. Une modification du calcul de stock impacte l'Aggregate Order.
C'est l'Aggregate Dieu Object déguisé en DDD.
Symptômes :
Pourquoi ça arrive :
On a peur de l'Aggregate trop gros après avoir lu qu'il faut "de petits Aggregates". Alors on va à l'extrême inverse : tout vide, tout dans les services.
Le service vérifie que la commande n'est pas vide. Le service vérifie que le statut est correct. Le service calcule le total. L'Aggregate ? Juste un conteneur de données.
C'est le modèle anémique déguisé en DDD.
On ne sait pas identifier les invariants métier.
Les livres disent "protégez vos invariants dans l'Aggregate". D'accord. Mais lesquels ?
Parce que tout n'est pas un invariant. Tout ne mérite pas d'être dans la même frontière transactionnelle.
On se demande : "Qu'est-ce qui va dans mon Aggregate ?"
Reformulons : "Quelles données doivent être absolument cohérentes ensemble, dans la même transaction ?"
Pas "qui va avec qui fonctionnellement". Pas "qui est lié à qui dans le modèle de données".
Cohérence transactionnelle obligatoire.
C'est la seule question qui compte.
Voici un test simple pour identifier la frontière.
Question : "Si ces deux données sont désynchronisées pendant 500 millisecondes, quel est l'impact métier ?"
Réponse A : Catastrophique Corruption de données, invariant violé, état métier impossible ou illégal. → Même Aggregate, même transaction
Réponse B : Aucun impact Juste une vue temporairement obsolète, aucune conséquence métier. → Aggregates séparés, cohérence éventuelle acceptable
Réponse C : Impact négatif mais gérable Par exemple : affichage d'un stock légèrement approximatif pendant une seconde. → Zone grise, arbitrage selon le contexte métier
Exemple 1 : Order et OrderLine
Question : "Une Order peut-elle exister sans OrderLine pendant 500ms ?"
Analyse métier : Une commande vide n'a aucun sens. Si on persiste une Order sans lignes, même temporairement, on a un état métier incohérent. Impossible de calculer un total, impossible de savoir ce qui est commandé.
Conclusion : Order et OrderLine doivent être dans le même Aggregate. L'invariant "une commande a au moins une ligne" doit être protégé transactionnellement.
Exemple 2 : Order et Customer
Question : "Si le nom du Customer change pendant qu'on crée une Order, c'est grave ?"
Analyse métier : Non. La commande référence un client à un instant T. Si son nom change après, ça n'invalide pas la commande. On peut même avoir une commande avec un ID client qui n'existe plus (client supprimé après coup).
Conclusion : Order et Customer ne doivent pas être dans le même Aggregate. Order garde juste l'identifiant du Customer. Pas de cohérence transactionnelle nécessaire.
Exemple 3 : Order et Inventory (stock)
Question : "Si on crée une Order et que le stock diminue juste après, c'est grave ?"
Analyse métier : Ça dépend du contexte.
Contexte A - E-commerce grand public : Le stock affiché est indicatif. On peut accepter de vendre en sur-réservation et gérer la rupture après. La cohérence éventuelle suffit. → Aggregates séparés, coordination asynchrone
Contexte B - Vente de billets de concert : Impossible de vendre plus de places que disponible. La cohérence doit être stricte. MAIS ça ne signifie pas fusionner Order et Inventory en un seul Aggregate (voir coordination ci-dessous). → Aggregates séparés, coordination par Saga ou verrou optimiste
Vous avez séparé vos Aggregates. Bien. Mais vous devez quand même coordonner Order et Inventory. Comment faire sans les fusionner ?
Le principe : Un orchestrateur coordonne plusieurs Aggregates sans les coupler. Chaque Aggregate reste autonome.
Flux typique pour créer une commande :
Avantages :
Compromis :
Le principe : Les Aggregates communiquent par événements. Quand Order change, il émet un événement. Inventory réagit à cet événement.
Flux typique :
Avantages :
Compromis :
Le principe : On vérifie avant de créer, on accepte qu'une race condition puisse survenir, on compense si détecté.
Flux typique :
Avantages :
Compromis :
Symptômes techniques :
Symptômes métier :
Ce qui se passe : Vous avez probablement fusionné plusieurs responsabilités métier distinctes. L'Aggregate est devenu un point de couplage.
Solution : Identifier les sous-ensembles qui ont leurs propres invariants indépendants. Les extraire en Aggregates séparés. Coordonner par événements ou Saga.
Symptômes techniques :
Symptômes métier :
Ce qui se passe : Vous avez un modèle anémique déguisé. L'Aggregate ne protège rien. C'est juste un DTO glorifié.
Solution : Rapatrier la logique métier dans l'Aggregate. Transformer les setters publics en méthodes métier qui protègent les invariants. Rendre les collections immutables de l'extérieur.
Symptômes :
Ce qui se passe : Soit vos frontières sont mal placées (certains Aggregates devraient peut-être être fusionnés), soit vous contournez l'isolation.
Solution : Revoir les frontières. Si deux Aggregates sont toujours modifiés ensemble, c'est probablement un seul Aggregate. Sinon, introduire des événements de domaine pour la communication.
Voici une grille pour dimensionner vos Aggregates.
Question : "Ces deux concepts ont-ils des règles métier qui doivent être vérifiées ensemble ?"
Si oui → Même Aggregate Si non → Aggregates séparés
Exemple :
Question : "Est-ce qu'on modifie souvent l'un sans l'autre ?"
Si oui → Envisager la séparation Si non → Probablement même Aggregate
Exemple :
Question : "Peut-on tolérer 100ms de désynchronisation entre ces données ?"
Si oui → Aggregates séparés avec événements Si non → Même Aggregate ou Saga avec compensation stricte
Question : "Le coût de coordination dépasse-t-il le coût du couplage ?"
Si coordination trop complexe → Fusionner les Aggregates Si couplage crée des goulots → Séparer et coordonner
C'est un arbitrage. Il n'y a pas de réponse universelle.
On découpe les Aggregates selon le schéma de base de données. Une table = un Aggregate.
Pourquoi c'est faux : Les Aggregates sont des frontières métier, pas techniques. Plusieurs tables peuvent représenter un seul Aggregate. Un Aggregate peut s'étaler sur plusieurs tables.
On découpe les Aggregates selon ce que l'utilisateur voit à l'écran. Un formulaire = un Aggregate.
Pourquoi c'est faux : L'interface est une représentation, pas le modèle métier. Un écran peut afficher des données de plusieurs Aggregates. Un Aggregate peut alimenter plusieurs écrans.
On crée des Aggregates "au feeling", en se disant qu'on ajustera si problème.
Pourquoi c'est risqué : Déplacer une frontière d'Aggregate une fois le code en production est coûteux. Ça impacte la persistence, les événements, les tests, potentiellement les migrations de données.
Mieux vaut : Prendre le temps de modéliser avec Event Storming ou Domain Storytelling. Identifier les vrais invariants métier. Puis découper.
On lit qu'il faut des "petits Aggregates immuables" et on applique systématiquement.
Pourquoi c'est dogmatique : L'immutabilité a un coût (création d'objets, garbage collection). C'est une optimisation de conception, pas une règle absolue. Parfois, un Aggregate mutable bien protégé est plus simple.
L'arbitrage clé : La frontière d'un Aggregate n'est pas technique, c'est métier. Où sont vos invariants ? Quelle cohérence est obligatoire ?
Les 3 questions qui comptent :
Prochaine étape :
Prenez un de vos Aggregates actuels. Listez ses attributs et entités internes.
Pour chaque élément, posez la question : "Si cet élément est obsolète pendant 500ms, quel est l'impact métier ?"
Si l'impact est nul ou tolérable → Cet élément peut probablement sortir de l'Aggregate.
Si l'impact est catastrophique → Il doit rester.
Pour aller plus loin :
Contexte de ces observations :
Patterns observés sur projets variés (B2B SaaS, Fintech, E-commerce), équipes de tailles diverses. Les erreurs décrites sont récurrentes indépendamment du domaine ou de la stack.
Limites :
Domaines spécifiques (temps-réel critique, très haute volumétrie distribuée) peuvent nécessiter des arbitrages différents. Ces règles sont des points de départ, pas des vérités absolues.
Comment concevoir des aggregates robustes qui maintiennent les invariants métier sans sacrifier la performance. Exemples de dimensionnement, composition et gestion du cycle de vie.
Patterns avancés pour implémenter Event Sourcing en production. Stratégies de snapshotting, patterns de projection et gestion de l'évolution des schémas avec exemples pratiques.
Repenser le pattern repository pour les applications contemporaines. Quand l'utiliser, comment l'implémenter efficacement, et les alternatives à considérer à l'ère des ORMs et architectures cloud-native.