Aggregate

Comprendre le rôle des aggregates pour gérer la cohérence métier, protéger les invariants et définir une frontière de modification claire.

Le problème

Quand un modèle métier commence à grossir, une question revient vite :

où est-ce qu’on fait respecter les règles importantes ?

Sans réponse claire, on obtient souvent un système où :

  • plusieurs objets peuvent être modifiés séparément alors qu’ils devraient rester cohérents
  • les règles métier sont dupliquées dans plusieurs services
  • n’importe quelle partie du code peut changer un état sensible
  • les transactions deviennent floues
  • il devient difficile de savoir quel objet est réellement responsable d’une décision

Exemple classique : une commande.

On veut garantir des choses simples :

  • une commande vide ne peut pas être validée
  • une commande déjà payée ne peut pas repasser à l’état brouillon
  • un article supprimé ne doit plus compter dans le total
  • une remise ne doit pas dépasser une certaine limite

Si ces règles sont réparties partout, le modèle devient fragile.

L’aggregate sert à résoudre ce problème.

L’idée simple

Un aggregate est une frontière de cohérence métier.

À l’intérieur de cette frontière :

  • un ensemble d’objets travaille ensemble
  • les invariants importants sont protégés
  • les modifications passent par un point d’entrée clair

En pratique, l’aggregate permet de dire :

  • quels objets appartiennent vraiment à la même décision métier
  • où une modification doit passer
  • ce qui doit rester cohérent dans une même opération

Le point clé est simple :

on ne modifie pas librement tous les objets du modèle. On passe par une frontière qui protège les règles importantes.

Comment ça fonctionne

Pour qu’un aggregate reste utile, il faut le voir comme une frontière métier, pas comme un conteneur technique.

1. Un aggregate protège des invariants

Un invariant est une règle qui doit rester vraie.

Exemples :

  • une commande confirmée doit avoir au moins une ligne
  • un panier ne peut pas contenir deux fois le même coupon exclusif
  • un compte ne peut pas descendre sous une certaine limite
  • une réservation confirmée ne peut pas dépasser la capacité disponible

L’aggregate sert à faire respecter ces règles au bon endroit.

2. L’accès passe par une racine

Un aggregate est généralement manipulé via une aggregate root.

C’est l’objet qui expose les opérations utiles et protège le reste.

Exemple :

  • Order peut être la racine
  • OrderLine peut exister à l’intérieur de l’aggregate
  • on ne modifie pas OrderLine directement depuis n’importe où
  • on demande à Order d’ajouter, retirer ou valider

La racine devient le point d’entrée naturel pour les changements importants.

3. Tout ne doit pas être dans le même aggregate

C’est une erreur fréquente.

Le but n’est pas de regrouper tous les objets liés par le métier. Le but est de regrouper ceux qui doivent rester cohérents dans la même frontière de modification.

La bonne question n’est pas :

“Quels objets se connaissent ?”

La bonne question est :

“Quels objets doivent changer ensemble pour garder une règle métier vraie ?”

Schéma

Structure d'un agrégat

Lecture utile :

  • Order est le point d’entrée
  • les objets internes existent, mais ne sont pas manipulés librement depuis l’extérieur
  • les règles métier sont protégées à travers la racine

Exemple concret

Prenons une commande.

Les règles métier sont :

  • une commande doit contenir au moins une ligne pour être confirmée
  • on ne peut pas ajouter de ligne après confirmation
  • le total doit être recalculé quand une ligne change
  • la remise ne peut pas dépasser le total

Mauvais design

export class Order {
  constructor(
    public id: string,
    public status: "draft" | "confirmed",
    public total: number
  ) {}
}

export class OrderLine {
  constructor(
    public orderId: string,
    public productId: string,
    public quantity: number,
    public unitPrice: number
  ) {}
}

export class Discount {
  constructor(
    public orderId: string,
    public amount: number
  ) {}
}

Avec ce design, n’importe quel code peut :

  • modifier total
  • ajouter une ligne
  • appliquer une remise
  • changer le statut

Le modèle n’empêche rien. Les règles finissent ailleurs, souvent dans des services procéduraux.

Design avec aggregate

export class Order {
  private lines: OrderLine[] = [];
  private discountAmount = 0;
  private status: "draft" | "confirmed" = "draft";

  constructor(private readonly id: string) {}

  addLine(productId: string, quantity: number, unitPrice: number): void {
    if (this.status !== "draft") {
      throw new Error("Cannot add a line to a confirmed order");
    }

    if (quantity <= 0) {
      throw new Error("Quantity must be greater than zero");
    }

    this.lines.push(new OrderLine(productId, quantity, unitPrice));
  }

  applyDiscount(amount: number): void {
    if (amount < 0) {
      throw new Error("Discount cannot be negative");
    }

    if (amount > this.getSubtotal()) {
      throw new Error("Discount cannot exceed subtotal");
    }

    this.discountAmount = amount;
  }

  confirm(): void {
    if (this.lines.length === 0) {
      throw new Error("Cannot confirm an empty order");
    }

    if (this.status !== "draft") {
      throw new Error("Order is already confirmed");
    }

    this.status = "confirmed";
  }

  getTotal(): number {
    return this.getSubtotal() - this.discountAmount;
  }

  private getSubtotal(): number {
    return this.lines.reduce(
      (sum, line) => sum + line.getLineTotal(),
      0
    );
  }
}

class OrderLine {
  constructor(
    private readonly productId: string,
    private readonly quantity: number,
    private readonly unitPrice: number
  ) {}

  getLineTotal(): number {
    return this.quantity * this.unitPrice;
  }
}

Ici :

  • Order est la racine de l’aggregate
  • OrderLine reste interne
  • les invariants passent par Order
  • les modifications sensibles sont contrôlées

C’est là que le modèle commence à protéger réellement le métier.

Aggregate et transaction

Un aggregate sert aussi à clarifier la frontière de cohérence.

En pratique, ça veut dire :

  • ce qui doit rester cohérent immédiatement vit souvent dans le même aggregate
  • ce qui peut être cohérent plus tard peut vivre ailleurs

Exemple :

  • le total et les lignes d’une commande doivent rester cohérents tout de suite
  • la génération d’une facture peut arriver dans une autre étape
  • l’envoi d’un email de confirmation peut être asynchrone

Tout mettre dans le même aggregate rend souvent le design trop gros et trop couplé.

Comment reconnaître un aggregate utile

Quelques questions aident beaucoup.

Où vivent les invariants importants ?

Si plusieurs règles métier concernent toujours les mêmes objets ensemble, il y a peut-être un aggregate.

Quel est le point d’entrée naturel pour modifier l’état ?

Si le code devrait toujours passer par un objet central pour modifier un ensemble cohérent, cet objet est souvent la racine.

Qu’est-ce qui doit rester cohérent immédiatement ?

C’est souvent le meilleur signal. Un aggregate protège surtout la cohérence locale.

Est-ce que cet aggregate devient énorme juste parce que “tout est lié” ?

Si oui, il est probablement trop large.

Exemple de repository

En général, on charge et on sauvegarde un aggregate via sa racine.

export interface OrderRepository {
  findById(orderId: string): Promise<Order | null>;
  save(order: Order): Promise<void>;
}

Le repository ne sert pas à manipuler chaque morceau interne séparément. Il sert à récupérer et persister l’aggregate comme frontière métier.

Points importants

  • Un aggregate est une frontière de cohérence métier.
  • Il protège des invariants importants.
  • L’accès passe généralement par une racine.
  • Tous les objets liés ne doivent pas forcément vivre dans le même aggregate.
  • Le bon critère est la cohérence immédiate, pas la proximité sémantique.
  • Un aggregate trop gros devient vite difficile à charger, tester et faire évoluer.

Erreurs fréquentes

Faire un aggregate géant

C’est l’erreur la plus classique.

Parce que plusieurs concepts sont liés métier, on les met tous ensemble. Résultat :

  • trop de responsabilités
  • trop de chargement inutile
  • trop de couplage
  • trop de conflits de modification

Confondre relation objet et frontière d’aggregate

Deux objets peuvent être liés sans devoir vivre dans le même aggregate.

Le critère principal reste : doivent-ils rester cohérents dans la même opération métier ?

Modifier directement les objets internes

Si n’importe quel code peut changer les lignes, les remises ou les statuts sans passer par la racine, l’aggregate ne protège plus rien.

Déplacer tous les invariants dans des services

Un service peut orchestrer. Mais si les règles essentielles quittent le modèle, celui-ci devient anémique.

Penser base de données avant cohérence métier

Un aggregate n’est pas d’abord une décision de table ou de jointure. C’est d’abord une décision de cohérence.

Conclusion

L’aggregate sert à protéger les règles métier qui doivent rester vraies ensemble.

Il donne une frontière claire à la cohérence locale et évite que le modèle soit modifié dans tous les sens.

Le réflexe utile à retenir est simple :

regroupe ce qui doit rester cohérent maintenant, pas tout ce qui est lié dans le métier.