ediverse Explore the platform

Spotlight PEPPOL BIS Billing 3.0 The EU e-invoicing mandate is here — France Sept 2026, Belgium Jan 2026, Germany 2025.

Aggregate

The transactional consistency unit inside a bounded context — the boundary inside which invariants always hold.

Problem

An EDI hub receives order PO-12345 with 50 lines. Two simultaneous requests modify it: one removes line 17, the other changes the quantity of line 32. If each line is persisted independently, line 32 can commit while line 17 deletion does not — the invariant "totalAmount = sum of lines" breaks. Worse: an external service queries line 17 while it is being deleted and believes the order is intact. Business consistency drifts.

Forces

  • Business invariants span multiple objects. "Total amount equals sum of lines": invariant between Order and OrderLine.
  • Concurrency is unavoidable. Multiple requests modify the same data simultaneously.
  • Strong consistency is expensive. Locking the whole DB for every update is not realistic.
  • Eventual consistency is acceptable outside aggregate. Between Order and Invoice we can tolerate a delay; between Order and its Lines, no.

Solution

Delimit each aggregate in the context: a cluster of objects with a single Aggregate Root. External references (other aggregates, services, events) point only to the Root, never its children. Every modification passes through the Root, which validates invariants before commit. One transaction = one aggregate. Between aggregates, accept eventual consistency (via Domain Events). Practical rules: small aggregate (5-7 entities max), Root carrying global identifier, optimistic locking on the Root.

┌─────────────────────────────────────────┐
│  Aggregate: PurchaseOrder               │
│                                         │
│  ▶ Root: Order                          │
│      orderNumber: "PO-12345"            │
│      buyerId, supplierId                │
│      status: Open|Confirmed|Cancelled   │
│      totalAmount: Money                 │
│                                         │
│   Children (only through Root):         │
│      OrderLine[ ]                       │
│        lineNumber, gtin, qty, price     │
│      DeliveryInstruction                │
│        dueDate, address                 │
│                                         │
│  Invariants enforced by Root:           │
│   - sum(lines.amount) == totalAmount    │
│   - Cancelled status => lines locked    │
│   - dueDate >= today + leadTime         │
└─────────────────────────────────────────┘
        ▲
        │ External reference: orderNumber (never OrderLine
        │ from outside)
        │
   Other Aggregates: Invoice, Shipment, Payment
   → all reference Order by ID only.

EDI implementation

Concrete case: in an ORDERS management service, the aggregate is PurchaseOrder: Root Order + children OrderLine[], DeliveryInstruction, PaymentTerms. Every mutation goes through order.cancelLine(lineNumber) or order.changeQuantity(lineNumber, qty) — the Root validates invariants (status allows modification, totalAmount recomputed). The Invoice is another aggregate: it references order.orderNumber but not the lines. When InvoiceIssued is emitted, the invoicing service creates its own Invoice aggregate with its logic. If tomorrow an INVOIC is cancelled, the Order does not change immediately: a InvoiceCancelled event triggers a reconciliation process (saga). On a Walmart hub at 50,000 PO/day, this allows horizontal scaling: each Kafka shard handles a subset of aggregates independently.

Anti-patterns

  • Aggregate too big. Putting Order + its 100 lines + all linked Invoices + the Payments in one aggregate: contention, degraded performance, long transactions.
  • Internal aggregate reference. An external service doing order.lines[5].update() bypasses the Root and breaks invariants. Every modification through the Root.
  • Cross-aggregate transaction. A transaction modifying Order and Invoice together: rejected by the pattern. Use Domain Events and a Saga.
  • Aggregate without invariant. If the Root enforces nothing ("self-service"), why an aggregate? Probably a data bundle — clarify the boundary.
  • Bounded Context — the aggregate lives in a context.
  • Domain Event — the event emitted by the Root after commit.
  • Saga Orchestration — inter-aggregate consistency via compensations.
  • Event Sourcing — an aggregate can be hydrated from its event stream.
  • Outbox — publish the Root-emitted event atomically with the 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, 3-part series). The reference for modern rules (aggregate small, reference by ID). dddcommunity.org — Vernon 2011
  • Vernon V.Implementing Domain-Driven Design, Addison-Wesley 2013. Chap. 10 expands aggregates with Java examples.
  • Microsoft Architecture Center — Designing a DDD-oriented microservice. learn.microsoft.com — DDD microservice