ediverse Explorer la plateforme

À la une PEPPOL BIS Billing 3.0 L’obligation européenne d’e-invoicing arrive : France sept 2026, Belgique janv 2026, Allemagne 2025.

Aggregate

L'unité de cohérence transactionnelle dans un bounded context — la frontière à l'intérieur de laquelle les invariants tiennent toujours.

Problème

Un hub EDI reçoit une commande PO-12345 avec 50 lignes. Deux requêtes simultanées modifient la commande : l'une retire la ligne 17, l'autre change la quantité de la ligne 32. Si chaque ligne est persistée indépendamment, on peut commit la ligne 32 modifiée mais pas la ligne 17 supprimée — l'invariant « totalAmount = somme des lignes » casse. Pire : un service externe interroge la ligne 17 alors qu'elle est en cours de suppression et croit que la commande est intacte. La cohérence métier dérive.

Forces

  • Les invariants métier traversent plusieurs objets. « Le montant total égale la somme des lignes » : invariant entre Order et OrderLine.
  • La concurrence est inévitable. Plusieurs requêtes modifient simultanément les mêmes données.
  • La cohérence forte coûte cher. Verrouiller toute la DB pour chaque update n'est pas réaliste.
  • La cohérence éventuelle est acceptable hors aggregate. Entre Order et Invoice, on peut tolérer un délai ; entre Order et ses Lines, non.

Solution

Délimiter chaque aggregate dans le contexte : un cluster d'objets avec une Aggregate Root unique. Les références externes (autres aggregates, services, événements) ne pointent que la Root, jamais ses enfants. Toute modification passe par la Root, qui valide les invariants avant commit. Une transaction = un seul aggregate. Entre aggregates, on accepte la consistance éventuelle (via Domain Events). Règles pratiques : aggregate petit (5-7 entités max), Root porteur d'identifiant global, optimistic locking sur la Root.

┌─────────────────────────────────────────┐
│  Aggregate : PurchaseOrder              │
│                                         │
│  ▶ Root : Order                         │
│      orderNumber : "PO-12345"           │
│      buyerId, supplierId                │
│      status : Open|Confirmed|Cancelled  │
│      totalAmount : Money                │
│                                         │
│   Children (uniquement via Root) :      │
│      OrderLine[ ]                       │
│        lineNumber, gtin, qty, price     │
│      DeliveryInstruction                │
│        dueDate, address                 │
│                                         │
│  Invariants imposés par Root :          │
│   - somme(lines.amount) == totalAmount  │
│   - statut Cancelled => lines lockées   │
│   - dueDate >= today + leadTime         │
└─────────────────────────────────────────┘
        ▲
        │ Référence externe : orderNumber (jamais OrderLine
        │ depuis l'extérieur)
        │
   Autres Aggregates : Invoice, Shipment, Payment
   → tous référencent Order par son ID seul.

Implémentation EDI

Cas concret : dans un service de gestion d'ORDERS, l'aggregate est PurchaseOrder : Root Order + enfants OrderLine[], DeliveryInstruction, PaymentTerms. Toute mutation passe par order.cancelLine(lineNumber) ou order.changeQuantity(lineNumber, qty) — la Root valide les invariants (statut autorise modification, totalAmount recalculé). L'Invoice est un autre aggregate : elle référence order.orderNumber mais pas les lignes. Quand InvoiceIssued est émis, le service de facturation crée son propre aggregate Invoice avec sa logique. Si demain une INVOIC est annulée, l'Order ne change pas immédiatement : un event InvoiceCancelled déclenche un processus de réconciliation (saga). Sur un hub Walmart à 50 000 PO/jour, cela permet de scaler horizontalement : chaque shard de Kafka traite un sous-ensemble d'aggregates indépendamment.

Anti-patterns

  • Aggregate trop gros. Mettre Order + ses 100 lignes + toutes les Invoices liées + les Payments dans un seul aggregate : contention, performance dégradée, transactions longues.
  • Référence interne d'aggregate. Un service externe qui fait order.lines[5].update() contourne la Root et casse les invariants. Toute modification passe par la Root.
  • Aggregate transactionnel inter-aggregate. Une transaction qui modifie Order et Invoice ensemble : refusé par le pattern. Utiliser des Domain Events et une Saga.
  • Aggregate sans invariant. Si la Root n'impose rien (« libre service »), pourquoi un aggregate ? Probablement un simple agrégat de données — clarifier la frontière.

Patterns liés

  • Bounded Context — l'aggregate vit dans un contexte.
  • Domain Event — l'événement émis par la Root après commit.
  • Saga Orchestration — la cohérence inter-aggregate via compensations.
  • Event Sourcing — un aggregate peut être hydraté depuis son flux d'événements.
  • Outbox — publier l'événement émis par la Root atomiquement avec le commit.

Sources

  • Evans E.Domain-Driven Design, Addison-Wesley 2003. Chap. 6 « The Life Cycle of a Domain Object » §Aggregates.
  • Vernon V.Effective Aggregate Design (DDD Community 2011, série de 3 articles). La référence pour les règles modernes (aggregate small, reference by ID). dddcommunity.org — Vernon 2011
  • Vernon V.Implementing Domain-Driven Design, Addison-Wesley 2013. Chap. 10 développe les aggregates avec exemples Java.
  • Microsoft Architecture Center — Designing a DDD-oriented microservice. learn.microsoft.com — DDD microservice