aggregate-transaction-boundary
SKILL.md
集約とトランザクション境界
集約は強い整合性境界である。ユースケースで複数集約を更新する場合は結果整合性を使う。
核心原則
1トランザクション = 1集約。これを逸脱してはならない。
集約の定義そのものが「強い整合性の境界」である。複数集約を単一トランザクションに含めることは、集約の定義からの逸脱であり、モジュラリティとスケーラビリティを破壊する。
権威ある定義
エリック・エヴァンス(DDD原典)
複数の集約にまたがるルールはどれも、常に最新の状態にあるということが期待できない。イベント処理やバッチ処理、その他の更新の仕組みを通じて、他の依存関係は一定の時間内に解消できる。
ヴァーン・ヴァーノン(実践ドメイン駆動設計)
ひとつの集約上でコマンドを実行するときに、他の集約のコマンドも実行するようなビジネスルールが求められるのなら、その場合は結果整合性を使うこと。
Lightbend Academy
トランザクションは複数の集約ルートを費やすべきではありません。
トランザクションは複数のエンティティにまたがりますか? この質問の答えがイエスならば、間違った集約ルートを持っていると言えるでしょう。
アンチパターン:ユースケースをトランザクション境界にする
問題のあるコード
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)
}
}
なぜ問題か:
- 集約の定義違反: 集約は強い整合性境界。複数集約を1トランザクションにすると、実質的に1つの巨大な整合性境界を作っている
- スケーラビリティの阻害: 集約Aと集約Bの更新が常にセットになり、後のスケーリングやサーバ分散が困難になる
- 異種DB環境で不可能: 集約ごとに異なるデータストアを使う場合、同一トランザクションは実装不可能
- マイクロサービスへの発展を阻害: 集約を別サービスに分離できなくなる
修正後のコード
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)
}
}
そもそもモデリングの問題ではないか
複数集約を同一トランザクションで更新したくなる場合、まずモデリングを見直すべきである。
判断フロー
2つの集約を常に一緒に更新する必要がある
↓
Task : TaskReport の関係は?
├─ 1:1 → 同一集約に統合を検討
│ 例: Task(taskReport: TaskReport)
│ → 1トランザクションで問題なし
│
├─ 1:N(少量) → 要件調整で小規模化できないか検討
│ → 可能なら同一集約に統合
│
├─ 1:N(大量) → 別集約 + 結果整合性
│ → ドメインイベントで連携
│
└─ TaskReportは純粋なクエリ要件か?
├─ YES → CQRS: リードモデルとして構築
│ → 集約ではなくプロジェクションで対応
└─ NO → ドメイン知識を持つ → 独立集約 + 結果整合性
同一集約への統合(1:1の場合)
// 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))
}
}
}
結果整合性の実現方法
集約が独立している場合、結果整合性で連携する。
ドメインイベント + 非同期処理
// 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)
}
}
ダブルコミット問題とSaga
独立トランザクションでは、集約Bの更新失敗時に集約Aはコミット済みという状態が発生する。
対処方法:
| 方法 | 適用場面 |
|---|---|
| リトライ | 一時的な障害の場合 |
| Saga(補償トランザクション) | 複雑な複数集約の協調が必要な場合 |
| Outboxパターン | イベント発行の確実性が必要な場合 |
Sagaは並行性問題を防止・軽減する設計テクニックであり、マイクロサービス環境での分散トランザクション管理に必須となる。
なぜこの規律が必要か
モジュラリティの確保
同一トランザクションに複数集約を含めると、暗黙的な結合が生まれる。
同一トランザクション:
Task ←──強結合──→ TaskReport
(分離不可能、独立スケーリング不可能)
結果整合性:
Task ──イベント──→ TaskReport
(独立デプロイ可能、独立スケーリング可能)
スケーラビリティへの道
モノリス(結果整合性を採用)
→ 集約ごとに独立したDB/テーブルへの分離が容易
→ マイクロサービス化が容易
→ 集約ごとの独立スケーリングが可能
モノリス(同一トランザクション)
→ 集約間の暗黙的結合で分離困難
→ マイクロサービス化に大規模リファクタリング必要
→ スケーリングはシステム全体でしかできない
aggregate-design スキルとの関係
| 観点 | 本スキル | aggregate-design スキル |
|---|---|---|
| 焦点 | トランザクション境界と結果整合性 | 集約の内部設計と構造 |
| 対象 | 集約間の連携パターン | 集約単体の設計原則 |
| 用途 | ユースケース設計・レビュー | 集約のモデリング・レビュー |
aggregate-design のルール8「1トランザクション = 1集約」とVernon Rule 4「結果整合性」を深掘りしたものが本スキルである。
レビューチェックリスト
トランザクション境界
-
@Transactional(またはトランザクション制御)の範囲内で、複数のリポジトリを呼び出していないか - 1つのユースケースで複数集約を更新する場合、結果整合性を採用しているか
- 「便利だから」という理由で複数集約を同一トランザクションに含めていないか
モデリング
- 常に一緒に更新される2つの「集約」は、本来1つの集約ではないか
- 片方がクエリ要件のみなら、CQRSのリードモデルで対応できないか
- 1:Nの関係でNが大量になる場合、別集約 + 結果整合性に分離しているか
結果整合性の実装
- ドメインイベントによる集約間連携が実装されているか
- イベント発行の確実性が担保されているか(Outboxパターン等)
- 失敗時のリカバリ戦略(リトライ、Saga)が定義されているか
- ダブルコミット問題を認識し、対処方法が明確か
関連スキル(併読推奨)
このスキルを使用する際は、以下のスキルも併せて参照すること:
aggregate-design: 集約の設計ルール(トランザクション境界の根拠)cross-aggregate-constraints: 集約間の制約と結果整合性の設計cqrs-aggregate-modeling: CQRS/ESによる集約軽量化とトランザクション問題の解消
Weekly Installs
18
Repository
j5ik2o/okite-aiGitHub Stars
72
First Seen
10 days ago
Security Audits
Installed on
opencode18
gemini-cli18
github-copilot18
codex18
amp18
cline18