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

Aggregate Design Heuristics

architecture PHP 7.0+ Advanced
debt(d7/e8/b8/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 as detection tools, the term itself notes automated detection is 'no'. Deptrac can enforce layer boundaries but cannot judge whether an aggregate is correctly sized or whether invariants are properly scoped. Detecting oversized aggregates or cross-aggregate transactions requires careful code review, domain expertise, and sometimes runtime observation of lock contention. Not quite d9 because experienced reviewers and some static patterns (e.g., multiple repository calls in one service method) can flag issues.

e8 Effort Remediation debt — work required to fix once spotted

Closest to 'architectural rework' (e9), scored down to e8. The quick_fix sounds simple ('reference other aggregates by ID') but in practice, resizing aggregates after they're established means restructuring entity relationships, rewriting repositories, introducing domain events for eventual consistency between formerly co-transactional entities, updating persistence mappings, and changing how data is queried. Common mistakes like 'Customer as part of Order aggregate' require splitting entities across bounded contexts. This is cross-cutting refactoring that approaches architectural rework, especially in a mature codebase.

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

Closest to 'defines the system's shape' (b9), scored down to b8. Aggregate boundaries are a foundational architectural decision in DDD systems. They determine transaction boundaries, consistency guarantees, repository structure, event flows, and API shapes. The term applies across all PHP contexts (web, cli, queue-worker), and once aggregates are defined, every new feature, query, and integration is shaped by those boundaries. Changing them later has cascading effects, but it doesn't quite reach b9 because in systems that aren't fully DDD, aggregate design may only govern part of the codebase.

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

Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception field is clear: developers intuitively believe 'larger aggregates are safer because they enforce more invariants in one transaction,' when in fact large aggregates cause write contention, lock timeouts, and performance problems. This directly contradicts the OOP instinct to group related objects together for safety. The common mistakes reinforce this — using object references instead of IDs, putting two aggregates in one transaction, and sizing for convenience rather than invariants are all natural-seeming choices that are wrong. A competent developer from an ORM/ActiveRecord background will almost certainly guess wrong initially.

About DEBT scoring →

Also Known As

aggregate sizing aggregate boundaries DDD aggregates Vaughn Vernon

TL;DR

Rules for sizing aggregates correctly — small aggregates with single-entity transactions, referencing other aggregates by ID, and designing boundaries around invariants not convenience.

Explanation

DDD aggregates enforce consistency boundaries. Key heuristics (Vaughn Vernon): (1) Design small aggregates — large aggregates cause contention; (2) Reference other aggregates by ID, not reference — prevents loading unnecessary objects; (3) One transaction per aggregate — if a use case modifies two aggregates, it probably needs eventual consistency; (4) Design around business invariants — what rules must always be true within this boundary?; (5) Accept eventual consistency between aggregates — use domain events to propagate changes.

Diagram

flowchart TD
    subgraph Correct - Small Aggregates
        ORD[Order<br/>aggregate root]
        LINE[OrderLine]
        ORD --> LINE
        ORD -->|customerId only - ID ref| CID[CustomerId]
    end
    subgraph Wrong - Oversized Aggregate
        ORD2[Order] --> CUST[Full Customer object]
        ORD2 --> PROD[Full Product catalogue]
        ORD2 --> INV[Inventory - all stock]
    end
    style CID fill:#238636,color:#fff
    style CUST fill:#f85149,color:#fff
    style PROD fill:#f85149,color:#fff
    style INV fill:#f85149,color:#fff

Watch Out

Developers often design aggregates by technical convenience (e.g., "User owns Orders owns OrderItems") rather than by invariant, then discover they need to modify multiple aggregates in a single transaction and retrofit eventual consistency—a painful redesign. The aggregate boundary should be drawn around what *must* stay consistent, not what's hierarchically related in the database.

Common Misconception

Larger aggregates are safer because they enforce more invariants in one transaction — large aggregates cause write contention, lock timeouts, and performance problems; most aggregates should be 2-5 entities.

Why It Matters

An Order aggregate that includes the Customer, all LineItems, the Product catalogue, and Inventory will lock all these records on every order placement — a correctly sized aggregate locks only the Order and its lines.

Common Mistakes

  • Aggregates sized for convenience (everything in one place) not invariants.
  • Object references between aggregates — use IDs; cross-aggregate references cause loading chains.
  • Two aggregates in one transaction — design for eventual consistency between them.
  • Customer as part of Order aggregate — Customer has its own life cycle and invariants; reference by ID.

Avoid When

  • The aggregate is too large — loading an entire order with all line items and history to change one field is wasteful.
  • Aggregates reference other aggregates by object rather than by ID — this creates tight coupling across boundaries.
  • Business rules span multiple aggregates — this is a sign the boundary is wrong, not that the rule should be in both.

When To Use

  • Enforcing invariants that span multiple objects — an Order and its OrderLines must be consistent together.
  • Transactional consistency boundaries in a domain model — one aggregate per transaction.
  • Domain-driven design contexts where you need a clear root that controls access to child entities.
  • Preventing direct manipulation of child entities from outside the aggregate boundary.

Code Examples

✗ Vulnerable
// Oversized aggregate — locks everything:
class Order {
    private Customer $customer;    // Full Customer object
    private Product[] $catalogue;  // Full product catalogue
    private Inventory $inventory;  // All inventory
    // Placing one order locks: customer table, product table, inventory
    // All concurrent orders for any product contend on inventory
}
✓ Fixed
// Small aggregate — reference by ID:
class Order {
    private OrderId $id;
    private CustomerId $customerId;  // ID reference only
    private OrderLine[] $lines;      // Only own entities
    private Money $total;
    // Invariant: total must equal sum of line amounts
    // Only locks: this order's rows — no customer/product contention
}
// When Order is placed, domain event notifies Inventory aggregate separately

Added 16 Mar 2026
Edited 10 Jun 2026
Views 74
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 1 ping W 0 pings T 0 pings F 0 pings S 1 ping S 0 pings M 0 pings T 0 pings W 1 ping T 0 pings F 0 pings S 0 pings S 1 ping M 4 pings T 0 pings W 1 ping T 1 ping F 3 pings S 2 pings S 0 pings M 3 pings T 4 pings W 2 pings T
Google 2
Scrapy 3 SEMrush 1
Scrapy 11 Amazonbot 9 Perplexity 8 ChatGPT 7 Google 6 Ahrefs 6 Unknown AI 3 SEMrush 3 Meta AI 1 Sogou 1
crawler 54 crawler_json 1
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: High
⚡ Quick Fix
Define transaction boundaries at the aggregate root — only the root entity should be modified and persisted in a single transaction; reference other aggregates by ID not by object
📦 Applies To
PHP 7.0+ web cli queue-worker
🔗 Prerequisites
🔍 Detection Hints
Transaction modifying multiple aggregate roots; aggregate root not protecting invariants of child entities
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