An aggregate is a strong consistency boundary. When a use case updates multiple aggregates, use eventual consistency.
Core Principle
1 transaction = 1 aggregate. This must not be deviated from.
The very definition of an aggregate is a "strong consistency boundary." Including multiple aggregates in a single transaction is a deviation from the definition of an aggregate and destroys modularity and scalability.
Authoritative Definitions
Eric Evans (the original DDD book)
Any rule that spans multiple aggregates cannot be expected to be always up to date. Through event processing, batch processing, or other update mechanisms, other dependencies can be resolved within a certain amount of time.
Vaughn Vernon (Implementing Domain-Driven Design)
If a business rule requires that when a command is executed on one aggregate, commands on other aggregates are also executed, then use eventual consistency.
Lightbend Academy
A transaction should not span multiple aggregate roots.
Does the transaction span multiple entities? If the answer to this question is yes, then you can say you have the wrong aggregate root.
Anti-Pattern: Making the Use Case the Transaction Boundary
Problematic Code
class CreateTaskUseCase(
private val taskRepository: TaskRepository,
private val taskReportRepository: TaskReportRepository,
) {
@Transactional // ← 複数集約を1トランザクションに閉じ込めている
fun execute(taskName: String) {
val task = Task(taskName)
taskRepository.insert(task)
val taskReport = TaskReport(task)
taskReportRepository.insert(taskReport)
}
}
Why It Is a Problem:
- Violation of the aggregate definition: An aggregate is a strong consistency boundary. Putting multiple aggregates in one transaction effectively creates one huge consistency boundary
- Hinders scalability: Updates to aggregate A and aggregate B always come as a set, making later scaling and server distribution difficult
- Impossible in heterogeneous DB environments: If a different data store is used for each aggregate, the same transaction cannot be implemented
- Hinders evolution into microservices: Aggregates can no longer be separated into different services
Corrected Code
class CreateTaskUseCase(
private val taskRepository: TaskRepository,
private val taskReportRepository: TaskReportRepository,
) {
fun execute(taskName: String) {
// 集約ごとに独立したトランザクション
val task = Task(taskName)
taskRepository.insert(task)
val taskReport = TaskReport(task)
taskReportRepository.insert(taskReport)
}
}
Is It Not a Modeling Problem in the First Place?
If you want to update multiple aggregates in the same transaction, you should first reconsider the modeling.
Decision Flow
2つの集約を常に一緒に更新する必要がある
↓
Task : TaskReport の関係は?
├─ 1:1 → 同一集約に統合を検討
│ 例: Task(taskReport: TaskReport)
│ → 1トランザクションで問題なし
│
├─ 1:N(少量) → 要件調整で小規模化できないか検討
│ → 可能なら同一集約に統合
│
├─ 1:N(大量) → 別集約 + 結果整合性
│ → ドメインイベントで連携
│
└─ TaskReportは純粋なクエリ要件か?
├─ YES → CQRS: リードモデルとして構築
│ → 集約ではなくプロジェクションで対応
└─ NO → ドメイン知識を持つ → 独立集約 + 結果整合性
Consolidation into the same aggregate (in the 1:1 case)
// TaskReportが常にTaskと1:1なら同一集約に統合
class Task private constructor(
val id: TaskId,
val name: TaskName,
val report: TaskReport // 集約内に含める
) {
companion object {
fun create(name: TaskName): Task {
val id = TaskId.generate()
return Task(id, name, TaskReport.create(id))
}
}
}
How to Achieve Eventual Consistency
If the aggregates are independent, coordinate them using eventual consistency.
Domain events + asynchronous processing
// 1. Task集約がドメインイベントを発行
class Task private constructor(
val id: TaskId,
val name: TaskName,
private val events: List<DomainEvent>
) {
companion object {
fun create(name: TaskName): Task {
val id = TaskId.generate()
return Task(
id, name,
listOf(TaskCreated(id, name)) // イベント発行
)
}
}
fun domainEvents(): List<DomainEvent> = events.toList()
}
// 2. ユースケースでTask保存後、イベントを発行
class CreateTaskUseCase(
private val taskRepository: TaskRepository,
private val eventPublisher: DomainEventPublisher,
) {
fun execute(taskName: String) {
val task = Task.create(TaskName(taskName))
taskRepository.store(task)
eventPublisher.publishAll(task.domainEvents())
}
}
// 3. イベントハンドラでTaskReport作成(別トランザクション)
class TaskCreatedEventHandler(
private val taskReportRepository: TaskReportRepository,
) {
fun handle(event: TaskCreated) {
val taskReport = TaskReport.create(event.taskId)
taskReportRepository.store(taskReport)
}
}
The Double Commit Problem and Saga
With independent transactions, a state arises where aggregate A has already committed when the update of aggregate B fails.
Countermeasures:
| Method | Applicable Situation |
|---|---|
| Retry | In the case of a temporary failure |
| Saga (Compensating Transaction) | When complex coordination of multiple aggregates is required |
| Outbox Pattern | When certainty of event publishing is required |
Saga is a design technique that prevents and mitigates concurrency problems, and is essential for distributed transaction management in microservice environments.
Why This Discipline Is Necessary
Ensuring Modularity
Including multiple aggregates in the same transaction creates implicit coupling.
同一トランザクション:
Task ←──強結合──→ TaskReport
(分離不可能、独立スケーリング不可能)
結果整合性:
Task ──イベント──→ TaskReport
(独立デプロイ可能、独立スケーリング可能)
The Path to Scalability
モノリス(結果整合性を採用)
→ 集約ごとに独立したDB/テーブルへの分離が容易
→ マイクロサービス化が容易
→ 集約ごとの独立スケーリングが可能
モノリス(同一トランザクション)
→ 集約間の暗黙的結合で分離困難
→ マイクロサービス化に大規模リファクタリング必要
→ スケーリングはシステム全体でしかできない
aggregate-design Relationship with the Skill
| Perspective | This Skill | aggregate-design Skill |
|---|---|---|
| Focus | Transaction boundaries and eventual consistency | Internal design and structure of aggregates |
| Target | Coordination patterns between aggregates | Design principles for individual aggregates |
| Use | Use case design and review | Aggregate modeling and review |
aggregate-design This skill is a deep dive into Rule 8, "1 transaction = 1 aggregate," and Vernon Rule 4, "eventual consistency."
Review Checklist
Transaction Boundaries
-
@Transactional(or transaction control), are multiple repositories being called? - When a single use case updates multiple aggregates, is eventual consistency adopted?
- Are multiple aggregates being included in the same transaction for the reason that "it's convenient"?
Modeling
- Are two "aggregates" that are always updated together actually one aggregate?
- If one side is only a query requirement, can it be handled by a CQRS read model?
- In a 1:N relationship where N becomes large, is it separated into a separate aggregate + eventual consistency?
Implementing Eventual Consistency
- Is inter-aggregate coordination via domain events implemented?
- Is the certainty of event publishing guaranteed (e.g., Outbox pattern)?
- Is a recovery strategy for failures (retry, Saga) defined?
- Is the double commit problem recognized and is the countermeasure clear?
Related Skills (Recommended Reading)
When using this skill, also refer to the following skills:
aggregate-design: Aggregate design rules (basis for transaction boundaries)cross-aggregate-constraints: Constraints between aggregates and the design of eventual consistencycqrs-aggregate-modeling: Aggregate weight reduction and resolution of transaction issues through CQRS/ES