← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

DDD Aggregates & Aggregate Roots

architecture Advanced
debt(d7/e7/b7/t7)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). While phpstan and deptrac are listed in detection_hints, they cannot automatically detect poor aggregate boundaries — tools can catch some symptoms like layer violations, but identifying that an aggregate is too large, that transactions span multiple aggregates, or that internal collections are exposed requires careful architectural review or runtime observation of contention issues. The detection_hints explicitly note 'automated: no'.

e7 Effort Remediation debt — work required to fix once spotted

Closest to 'cross-cutting refactor across the codebase' (e7). Fixing poorly designed aggregates means redrawing consistency boundaries, which typically requires: splitting large aggregates into smaller ones, introducing domain events for cross-aggregate communication, changing how references work (ID-only instead of object references), and updating all code that interacts with these aggregates. The quick_fix mentions redesigning aggregates and potentially introducing domain events, indicating significant architectural work.

b7 Burden Structural debt — long-term weight of choosing wrong

Closest to 'strong gravitational pull' (b7). Aggregate boundaries shape how every feature touching the domain is implemented — they define transaction boundaries, what can be loaded together, how invariants are enforced, and how different parts of the system communicate. Once established, aggregate design influences all future domain work. The term applies across web/cli/queue contexts, and poor aggregate design creates persistent drag on the entire domain layer.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7). The misconception field directly states developers believe 'aggregates should be as large as possible to keep related data together' when the opposite is true — aggregates should be as small as possible. This contradicts intuitive thinking about grouping related data and differs from how ORMs encourage loading object graphs. Developers coming from CRUD/Active Record patterns will naturally design oversized aggregates, causing contention and complexity.

About DEBT scoring →

Also Known As

DDD aggregate aggregate root domain aggregate

TL;DR

A cluster of domain objects treated as a single unit with one root entity controlling access and enforcing invariants across the cluster.

Explanation

An Aggregate is a cluster of related domain objects (entities and value objects) with a single Aggregate Root — the only entity that external objects can hold a reference to. The root ensures all invariants are maintained: an Order (root) controls its OrderItems — you can't add an OrderItem without going through Order::addItem(), which validates business rules like maximum quantity. Aggregates define transactional boundaries: you load and save an aggregate atomically; two aggregates in the same transaction is a design smell. Keep aggregates small — an Order with thousands of items should be reconsidered. Reference other aggregates by ID (not direct object reference) to enforce boundaries. Events crossing aggregate boundaries use domain events.

Diagram

flowchart TD
    subgraph OrderAggregate
        ROOT[Order aggregate root]
        LINE[OrderLine]
        ADDR[ShippingAddress]
        ROOT --> LINE & ADDR
    end
    subgraph CustomerAggregate
        CROOT[Customer aggregate root]
        PROFILE[Profile]
        CROOT --> PROFILE
    end
    ROOT -->|reference by ID only| CROOT
    INFO[Aggregates communicate by ID<br/>Each has its own transaction boundary]
style ROOT fill:#6e40c9,color:#fff
style CROOT fill:#6e40c9,color:#fff
style INFO fill:#1f6feb,color:#fff

Common Misconception

Aggregates should be as large as possible to keep related data together. Large aggregates cause contention — every operation locks the whole aggregate. Aggregates should be as small as possible while still enforcing their invariants consistently.

Why It Matters

Aggregates define consistency boundaries — all changes within an aggregate are atomic, and external objects can only reference aggregates by ID, enforcing invariants and preventing inconsistent state.

Common Mistakes

  • Aggregates that are too large — every entity in a domain is not part of one aggregate; keep them small.
  • Referencing aggregate internals from outside the aggregate root — go through the root always.
  • Saving multiple aggregates in a single transaction — use eventual consistency via domain events instead.
  • Exposing internal collection references from the aggregate root — callers can modify state without invariant checks.

Code Examples

✗ Vulnerable
// Modifying aggregate internals directly — bypasses invariants:
$order->items[] = new OrderItem($product, $qty); // Direct collection mutation
// Should be: $order->addItem($product, $qty) — root enforces invariants
✓ Fixed
// Aggregate — cluster of domain objects treated as a single unit
// All changes go through the Aggregate Root

class Order { // Aggregate Root
    private array \$items = [];
    private OrderStatus \$status;

    // Only Order can add items — enforces invariants
    public function addItem(Product \$product, int \$qty): void {
        if (\$this->status !== OrderStatus::Draft) throw new OrderNotEditableException();
        if (\$qty < 1) throw new InvalidQuantityException();
        \$this->items[] = new OrderItem(\$product, \$qty);
    }

    public function getTotal(): Money {
        return array_reduce(\$this->items, fn(\$sum, \$i) => \$sum->add(\$i->getTotal()), Money::zero());
    }
}

// Never manipulate OrderItem directly from outside — always go through Order
// Repository saves/loads the entire aggregate atomically

Added 15 Mar 2026
Edited 22 Mar 2026
Views 33
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 1 ping S 0 pings S 1 ping M 0 pings T 1 ping W 2 pings T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 2 pings F 0 pings S
No pings yet today
Perplexity 1
Perplexity 9 Amazonbot 6 Google 2 Unknown AI 2 Ahrefs 2 SEMrush 2 ChatGPT 2 Majestic 1 Bing 1
crawler 25 crawler_json 2
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: High
⚡ Quick Fix
Design small aggregates — each should fit in a single DB transaction; if two aggregates always change together, they may be one; if one aggregate needs another to validate, consider domain events instead
📦 Applies To
any web cli queue-worker
🔗 Prerequisites
🔍 Detection Hints
Transaction modifying 3+ aggregate roots; aggregate loading full object graph; aggregate with 15+ entities making it a god aggregate
Auto-detectable: ✗ No phpstan deptrac
⚠ Related Problems
🤖 AI Agent
Confidence: Low False Positives: High ✗ Manual fix Fix: High Context: Class Tests: Update

✓ schema.org compliant