← Back to list

aggregate-root
by dairectiv
⭐ 0🍴 0📅 Jan 3, 2026
SKILL.md
name: aggregate-root description: Guide for implementing DDD Aggregate Roots with rich domain models, validation, domain events, and exhaustive tests. Use when creating or modifying aggregates. allowed-tools: Read, Write, Edit, Glob, Grep
Aggregate Root Implementation Guide
This Skill provides best practices for implementing DDD Aggregate Roots with rich domain models.
When to Use
- Creating a new Aggregate Root
- Adding behavior to an existing aggregate
- Implementing state transitions
- Writing exhaustive unit tests for domain logic
Core Principles
1. Rich Domain Model (Not Anemic)
Aggregates must encapsulate business logic, not just hold data:
// BAD: Anemic model with public setters
$rule->setContent($content);
$rule->setState(DirectiveState::Published);
// GOOD: Rich model with behavior
$rule->updateContent($content);
$rule->publish();
2. Business Intent Through Method Names
Methods should express what happens in business terms:
| Bad | Good |
|---|---|
setState() | publish(), archive(), delete() |
setContent() | updateContent() |
setExamples() | addExample(), removeExample() |
3. Protect Invariants
Validate state transitions and business rules:
public function publish(): void
{
Assert::eq($this->state, DirectiveState::Draft, 'Only draft directives can be published.');
$this->state = DirectiveState::Published;
$this->recordEvent(new DirectivePublished($this->id));
}
4. Domain Events for State Changes
Record events for significant business actions:
protected function initialize(DirectiveId $id, string $name): void
{
$this->id = $id;
$this->name = $name;
$this->state = DirectiveState::Draft;
$this->recordEvent(new DirectiveDrafted($this->id));
}
public function archive(): void
{
Assert::notEq($this->state, DirectiveState::Archived, 'Already archived.');
$this->state = DirectiveState::Archived;
$this->recordEvent(new DirectiveArchived($this->id));
}
Directory Structure
src/{BoundedContext}/Domain/Object/{Aggregate}/
├── {Aggregate}.php # Aggregate root
├── {Aggregate}Id.php # Identity value object (optional)
├── Event/
│ ├── {Aggregate}Created.php
│ ├── {Aggregate}Updated.php
│ └── {Aggregate}Archived.php
└── Exception/
└── {Aggregate}NotFoundException.php
Implementation Patterns
Factory Method (Named Constructor)
public static function draft(DirectiveId $id, string $name, string $description): static
{
$entity = new self();
$entity->initialize($id, $name, $description);
return $entity;
}
State Guard Methods
final protected function assertNotArchived(): void
{
Assert::notEq($this->state, DirectiveState::Archived, 'Cannot modify archived entity.');
Assert::notEq($this->state, DirectiveState::Deleted, 'Cannot modify deleted entity.');
}
Mark Updated Pattern
final public function markAsUpdated(): void
{
$this->assertNotArchived();
$this->updatedAt = Chronos::now();
$this->recordEvent(new DirectiveUpdated($this->id));
}
Testing Best Practices
Use UnitTestCase with AggregateRootAssertions
#[Group('unit')]
#[Group('{bounded-context}')]
final class {Aggregate}Test extends UnitTestCase
Test Categories (Be Exhaustive)
-
Creation/Lifecycle
- Happy path creation
- All state transitions (draft → published → archived)
-
Invalid State Transitions
- Cannot publish already published
- Cannot archive already archived
- Cannot modify deleted entity
-
Behavior Methods
- Each method with valid inputs
- Each method with invalid inputs
- Edge cases (null, empty, boundary values)
-
Domain Events
- Assert every event is recorded
- Use
resetDomainEvents()between actions
Test Pattern
public function testItShouldPublishRule(): void
{
// Arrange
$rule = Rule::draft(DirectiveId::fromString('my-rule'), 'Name', 'Desc');
$this->resetDomainEvents();
// Act
$rule->publish();
// Assert state
self::assertSame(DirectiveState::Published, $rule->state);
// Assert event
$this->assertDomainEventRecorded(DirectivePublished::class);
}
public function testItShouldNotPublishAlreadyPublished(): void
{
$rule = Rule::draft(DirectiveId::fromString('my-rule'), 'Name', 'Desc');
$rule->publish();
$this->resetDomainEvents();
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Only draft directives can be published.');
$rule->publish();
}
Domain Event Assertions
// Assert specific event was recorded
$this->assertDomainEventRecorded(DirectiveCreated::class);
// Assert event recorded N times
$this->assertDomainEventRecorded(DirectiveUpdated::class, 3);
// Reset events mid-test
$this->resetDomainEvents();
// Assert no events (automatic in tearDown)
$this->assertNoDomainEvents();
Checklist
When implementing an Aggregate Root:
- Uses factory method (
draft(),create()) not public constructor - Method names reflect business intent
- State transitions are guarded with Assert
- Domain events recorded for significant changes
- No public setters, only behavior methods
-
markAsUpdated()called for modifications - Extends
AggregateRootbase class
When testing:
- Test all creation paths
- Test all valid state transitions
- Test all invalid state transitions
- Test each behavior method
- Assert all domain events
- Use
resetDomainEvents()to isolate assertions - Use
#[Group('unit')]and bounded context group
Reference Files
api/src/Authoring/Domain/Object/Directive/Directive.php- Base aggregateapi/src/Authoring/Domain/Object/Rule/Rule.php- Concrete aggregateapi/tests/Unit/Authoring/Domain/Object/Rule/RuleTest.php- Exhaustive testsapi/tests/Framework/UnitTestCase.php- Base test classapi/tests/Framework/Assertions/AggregateRootAssertions.php- Event assertions
Score
Total Score
50/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon