Context Scope

Aggregates : La Frontière de Cohérence Qu'On Vous Cache

📅 Publié le 16 January 2025 • ⏱️ 11 min de lecture

Aggregates : La Frontière de Cohérence Qu'On Vous Cache

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 :

  • "Ben... la commande, les lignes de commande, le client, les produits..."
  • "Juste la commande et ses lignes, le reste on référence par ID"
  • "Euh... ça dépend ?"

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.

Ce que disent les livres

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 :

  1. Un seul Aggregate Root - Point d'entrée unique pour modifier l'état
  2. Cohérence transactionnelle - Les invariants sont garantis dans une transaction
  3. Référencement par ID - Les Aggregates se référencent entre eux par identifiant, pas par objet
  4. Limite de modification - Une transaction ne modifie qu'un seul Aggregate

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.

Le problème avec le consensus

Erreur classique n°1 : L'Aggregate cathédrale

Symptômes :

  • Un Aggregate Order qui contient : la commande, les lignes, le client complet, les produits avec leur stock, les paiements, les expéditions
  • Plusieurs centaines de lignes dans l'Aggregate
  • Modification d'une commande qui charge client + produits + stock + paiements
  • Conflits de concurrence fréquents (tout le monde modifie Order)
  • Tests qui nécessitent de mocker une dizaine de dépendances

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.

Erreur classique n°2 : L'Aggregate anémique

Symptômes :

  • Un Aggregate Order qui ne contient que des identifiants et des getters/setters
  • Toute la logique métier dans les services applicatifs
  • Invariants vérifiés "ailleurs" (dans les services, pas l'Aggregate)
  • Services qui manipulent directement les collections internes de l'Aggregate
  • Violations régulières des règles métier

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.

Le vrai problème

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.

La vraie question à poser

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.

Le test de la désynchronisation temporaire

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

Exemples concrets

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

Les patterns de coordination

Vous avez séparé vos Aggregates. Bien. Mais vous devez quand même coordonner Order et Inventory. Comment faire sans les fusionner ?

Pattern 1 : Saga ou Process Manager

Le principe : Un orchestrateur coordonne plusieurs Aggregates sans les coupler. Chaque Aggregate reste autonome.

Flux typique pour créer une commande :

  1. Réserver le stock (action sur Aggregate Inventory)
  2. Créer la commande (action sur Aggregate Order)
  3. Si succès : confirmer la réservation
  4. Si échec : annuler la réservation (compensation)

Avantages :

  • Aggregates restent petits et focalisés
  • Chaque Aggregate protège uniquement ses propres invariants
  • Coordination explicite, testable séparément

Compromis :

  • Complexité de la compensation en cas d'erreur
  • Pas de cohérence transactionnelle ACID stricte
  • Fenêtre temporelle où les données sont en cours de synchronisation

Pattern 2 : Événements de domaine

Le principe : Les Aggregates communiquent par événements. Quand Order change, il émet un événement. Inventory réagit à cet événement.

Flux typique :

  1. Order émet "OrderCompleted" après validation
  2. Inventory écoute cet événement
  3. Inventory confirme définitivement la réservation de stock
  4. Si besoin, Inventory émet à son tour un événement

Avantages :

  • Découplage total entre Aggregates
  • Évolutivité (facile d'ajouter de nouveaux listeners)
  • Historique naturel (audit trail d'événements)

Compromis :

  • Cohérence éventuelle (délai de propagation)
  • Debugging plus complexe (flux asynchrone)
  • Gestion des événements en échec

Pattern 3 : Vérification optimiste

Le principe : On vérifie avant de créer, on accepte qu'une race condition puisse survenir, on compense si détecté.

Flux typique :

  1. Vérifier le stock disponible (lecture, pas de verrou)
  2. Si OK, créer la commande
  3. Si entre-temps le stock a changé, on le détecte plus tard
  4. Compensation asynchrone si nécessaire

Avantages :

  • Simple à implémenter
  • Performant (pas de verrou)
  • Acceptable si les collisions sont rares

Compromis :

  • Race conditions possibles
  • Nécessite un process de détection et compensation
  • Pas adapté si les collisions sont fréquentes

Signaux d'alarme

🚩 Votre Aggregate est trop gros si :

Symptômes techniques :

  • Plus de 300-400 lignes dans l'Aggregate
  • Plus de 5-6 dépendances injectées
  • Tests qui mockent plus de 3 collaborateurs
  • Temps de chargement devenu problématique

Symptômes métier :

  • Modifier une petite règle nécessite de charger tout l'Aggregate
  • Conflits de concurrence fréquents (plusieurs utilisateurs modifient "en même temps")
  • L'Aggregate connaît des concepts de plusieurs domaines métier différents

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.

🚩 Votre Aggregate est trop petit si :

Symptômes techniques :

  • Les services contiennent toute la logique métier
  • L'Aggregate n'a que des getters/setters publics
  • Pas de méthodes métier dans l'Aggregate
  • Collections internes modifiables de l'extérieur

Symptômes métier :

  • Les invariants sont vérifiés dans les services, pas l'Aggregate
  • Violations régulières des règles métier
  • Les développeurs contournent l'Aggregate "parce que c'est plus simple"

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.

🚩 Vos Aggregates communiquent mal si :

Symptômes :

  • Vous chargez plusieurs Aggregates dans une transaction "pour être sûr"
  • Vous avez des références d'objets entre Aggregates (pas juste des IDs)
  • Appels synchrones entre Aggregates dans le même processus
  • Les événements de domaine ne sont jamais utilisés

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.

Règles de décision

Voici une grille pour dimensionner vos Aggregates.

Critère 1 : Invariants fortement couplés

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 :

  • Order et OrderLine ont l'invariant "une commande complétée ne peut pas être vide" → Même Aggregate
  • Order et Customer n'ont pas d'invariant commun → Aggregates séparés

Critère 2 : Fréquence de modification

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 :

  • Customer et Address sont souvent modifiés ensemble → Même Aggregate
  • Customer et historique des commandes évoluent indépendamment → Aggregates séparés

Critère 3 : Tolérance à la désynchronisation

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

Critère 4 : Complexité vs performance

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.

Pièges fréquents

Piège 1 : Aggregate = table de base de données

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.

Piège 2 : Aggregate = écran utilisateur

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.

Piège 3 : "On verra plus tard"

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.

Piège 4 : Aggregates immuables systématiquement

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.

À retenir

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 :

  1. Ces données doivent-elles être cohérentes dans la même transaction ?
  2. Peut-on tolérer une désynchronisation temporaire ?
  3. Les invariants métier lient-ils vraiment ces concepts ?

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 :

  • "Implementing Domain-Driven Design" - Vaughn Vernon (chapitre 10 sur les Aggregates)
  • "Effective Aggregate Design" - Vaughn Vernon (série d'articles, très concrets)
  • "Domain-Driven Design Distilled" - Vaughn Vernon (version condensée, va à l'essentiel)

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.

📚 À lire aussi

Patterns Event Sourcing : Au-Delà des Bases

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.

#tactical #event-sourcing #patterns

Pattern Repository : Approche Moderne

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.

#tactical #patterns #infrastructure