Principles of Aggregate design in DDD.
What Is an Aggregate
Aggregate = Consistency Boundary
- An aggregate is a graph of objects, and that graph is treated as a single unit
- Within the aggregate,true invariants(constraints that must always be satisfied) are maintained
- The aggregate root (root entity) is the sole entry point for the entire aggregate
Boundary Example: Car Aggregate
┌─────────────────────────────────────┐
│ Car集約 │
│ ┌──────────────────────────────┐ │
│ │ Car (集約ルート) │ │
│ │ - carId: CarId │ │
│ │ - tires: List[Tire] │ │
│ │ - engine: Engine │ │
│ └──────────────────────────────┘ │
│ ↓ 所有 ↓ 所有 │
│ ┌──────────┐ ┌─────────┐ │
│ │ Tire │ × 4 │ Engine │ │
│ │ (VoかEnt)│ │ (VoかEnt)│ │
│ └──────────┘ └─────────┘ │
└─────────────────────────────────────┘
- Tire and Engine are within the boundary of the Car aggregate
- Direct access to Tire or Engine from outside is not allowed
- They can be operated only through Car
Design by Contract (DbC)
Aggregates arecontractsdesigned based on.
| Contract | Description | Responsibility |
|---|---|---|
| Precondition | A condition that must be satisfied before a method call | Caller |
| Postcondition | A condition that is satisfied after method execution | Implementer |
| Invariant | A condition that must always be satisfied | Implementer |
For detailed language-specific implementation patterns, see references/typescript.md、references/scala.md、references/rust.md、references/python.md .
Basic Principles
1. Immutability Recommended
In modern development, immutability is recommended. Unless there is a particular reason, make it immutable. When updating state, carry over existing values and overwrite only the fields that change. This prevents omissions when adding fields and makes the intent of updates clear.
| Language | Immutable Update Pattern |
|---|---|
| TypeScript | Props type + ...this.props spread syntax |
| Scala | case class + copy() |
| Rust | struct + ..self struct update syntax |
| Python | dataclass(frozen=True) + replace() |
2. Strong Consistency Boundary
An aggregate is a single strong consistency boundary. All state inside the aggregate is placed under the management of that aggregate.
3. Indirect References to Other Aggregates
Do not hold references to other aggregates inside an aggregate. When relating to other aggregates, reference them indirectly by ID.
4. Complete Constructor
Initialize all state in the primary constructor. Even when overloading, always design auxiliary constructors so that they use the primary constructor.
5. Defensive Copy
When holding mutable objects, always return a copy or convert them to immutable objects when returning them externally.
6. Maintaining Invariants
No matter what operation is performed, it must not fall into an invalid state. For immutable aggregates, protect this with the primary constructor.
Structural Principles
7. Access via the Aggregate Root
Direct access to entities or value objects inside an aggregate must always go through the aggregate root.
8. 1 Transaction = 1 Aggregate
Do not update multiple aggregates in a single transaction. Consistency between aggregates is ensured by eventual consistency.
9. Keep Aggregates Small
An overly large aggregate causes concurrency problems (lock contention). Include only the scope that truly requires consistency.
Collaboration Principles
10. Collaboration via Domain Events
When an aggregate changes state, it publishes a domain event, and other aggregates or external systems subscribe to it and react.
11. Optimistic Locking (Only When Required)
Only when detection of concurrent update conflicts is required should a version number be provided. If there is no requirement, it is unnecessary.
12. Ignorance of Persistence
An aggregate focuses on domain logic and is unaware of how it is stored (the responsibility of the repository).
Evans Rules (Eric Evans)
Aggregate rules from the original DDD text.
Evans Rule 1: Responsibility of the Root Entity
Only the aggregate root can be obtained directly from the repository.
- External objects cannot hold references into the aggregate's internals
- The aggregate root can pass the identifiers of internal entities, but only for temporary use
- The aggregate root is responsible for maintaining the invariants of the entire aggregate
Evans Rule 2: Restrictions on Access to Internal Objects
Entities within the boundary have local identifiers. They need only be unique within the aggregate.
- Cannot be accessed directly from outside
- The identifiers of internal entities do not need to be globally unique
Vernon's 4 Rules (Vaughn Vernon)
Design rules from 'Implementing Domain-Driven Design'.
Rule 1: True Invariants at the Consistency Boundary
Design aggregates based on true invariants (Guarantee true invariants within the consistency boundary)
良い例:Order集約
- 注文合計 = 各明細の小計の合計(真の不変条件)
- この計算は即座に正しくなければならない
悪い例:結果整合性で十分な場合
- 在庫数の更新は別の集約で結果整合性
Rule 2: Small Aggregates
Keep aggregates small(Keep aggregates small)
Problems with large aggregates:
- Increased lock contention
- Decreased concurrency
- Increased transaction failure rate
❌ Bad: 大きな集約
Product集約
├── productId
├── name
├── backlogItems: List[BacklogItem] ← 数百件になる可能性
└── ...
✅ Good: 分割した集約
Product集約 BacklogItem集約
├── productId ├── backlogItemId
├── name ├── productId(IDで参照)
└── ... └── ...
Rule 3: Reference Other Aggregates by ID
Reference other aggregates by ID only(Reference other aggregates by ID)
The same principle as the aforementioned 'indirect reference to other aggregates'.
Rule 4: Eventual Consistency
Use eventual consistency outside the boundary(Outside the boundary is eventually consistent)
- Consistency between aggregates is ensured by domain events + eventual consistency
- Do not update multiple aggregates in a single transaction
Review Checklist
Used when reviewing existing aggregates.
Design by Contract
- Are preconditions clearly defined (require)
- Are postconditions satisfied (ensuring)
- Are invariants maintained in the constructor and after each operation
Basic Principles
- Is the aggregate immutable, or is there a clear reason why it cannot be made immutable?
- Does it avoid holding direct references to other aggregates (does it hold only IDs)?
- Does it provide a complete constructor?
- Do the auxiliary constructors use the primary constructor?
- When returning mutable objects, does it make defensive copies?
- Are the invariants validated in the constructor?
Evans Rules
- Is only the aggregate root retrieved directly from the repository?
- Are there no direct references to the inside of the aggregate from the outside?
- Are the identifiers of internal entities limited to temporary use?
Vernon's Rules
- Are only true invariants within the aggregate boundary (is there no excessive inclusion)?
- Is the aggregate sufficiently small (does it not contain collections of hundreds of items)?
- Are other aggregates referenced only by ID?
- Is consistency between aggregates ensured by eventual consistency?
Structural Principles
- Is external access only via the aggregate root?
- Are there no holes through which internal entities can be manipulated directly?
- Is the size of the aggregate appropriate (is it not too large)?
Collaboration Principles
- Does it publish domain events when state changes?
- If there is a requirement for optimistic locking, is a version number implemented?
- Even though there is no requirement for optimistic locking, does it not have an unnecessary version number?
- Is persistence logic not leaking into the aggregate?
Related Skills (Recommended to Read Together)
When using this skill, also refer to the following skills:
domain-building-blocks: Design of the elements that compose an aggregate (value objects, entities, domain services)aggregate-transaction-boundary: Relationship between aggregates and transaction boundaries (1 transaction = 1 aggregate rule)cross-aggregate-constraints: Constraint checking between aggregates and design of eventual consistencyrepository-placement: Where to place the repository interface for an aggregate