DDDの集約(Aggregate)設計ルールに基づくコードレビュー・設計支援・リファクタリングを行う。 Evans Rules、Vernon's 4 Rules、Design by Contractに基づき、集約の境界定義、不変条件の検証、 不変(Immutable)設計、ID参照、結果整合性、ドメインイベント連携を包括的にガイドする。 以下のいずれかに該当する場合は必ずこのスキルを使用すること: - 集約(Aggregate)の新規設計・実装・リファクタリング(どの言語でも) - 既存の集約やエンティティクラスのDDD観点でのコードレビュー - 集約の境界決定(「AとBは同じ集約にすべきか?」「この集約は大きすぎるか?」) - 集約内の不変条件・整合性境界の設計 - 集約間の連携方式の判断(ドメインイベント、結果整合性、Sagaパターン) - 可変(Mutable)な集約コードを不変(Immutable)設計にリファクタリングする - publicフィールド、直接参照、push/appendなどカプセル化違反の検出・修正 キーワード例:集約、Aggregate、aggregate boundary、集約ルート、AggregateRoot、 エンティティ設計、DDD実装、Vernon Rules、Evans Rules、集約の分割、真の不変条件
cross-aggregate-constraints
集約間の制約チェック(「集約Aのユースケースで集約Bの状態を確認したい」)に対する 設計判断を支援する。Sagaの誤用検出、ビジネス要件の問い直し、CQRS/ESにおける 技術的制約の理解、不整合データの許容判断を含む。コードレビュー、アーキテクチャ設計、要件分析時に集約間の制約問題に直面した場合に使用。
対象言語: 言語非依存。
トリガー:「集約間の制約チェック」「他の集約の状態を確認したい」「Sagaで制約チェック」 「ブランドに商品が紐づいているか確認」「集約間のバリデーション」 「イベントソーシングで逆引き」「集約をまたぐビジネスルール」「CQRS/ESの制約」 「コマンド側からリードモデルを参照していい?」「ユースケースで他の集約を参照」 といった集約間制約関連リクエストで起動。
含まれるファイル(1)
- SKILL.md15.1 KB
SKILL.md(原文)
インストールする前に、エージェントに与えられる指示の中身を確認できます。
集約間の制約チェック
集約間の制約に直面したら、まず要件を疑い、次に技術的制約を理解し、最後に覚悟を決める。
典型的な問題
「集約Aのユースケースで集約Bの状態を確認したい」という要求が発生する。
例: 「一つでも商品が紐づいているブランドは削除できない」
Brand集約(削除したい)←── 制約チェック ──→ Product集約(紐づき確認)
この種の要求に対して、安易に技術的解決策に飛びつくべきではない。
ステップ1: 要件を疑う
技術の前に、ビジネス要件自体を問い直す。
問い直しのフレームワーク
その制約は本当に必要か?
↓
ライフサイクル全体で考えるとどうなるか?
↓
実運用では実質的にどういう制約になるか?
↓
複雑な実装に見合うビジネス価値があるか?
具体例: ブランド削除制約
「一つでも商品が紐づいているブランドは削除できない」を問い直す:
| 観点 | 分析 |
|---|---|
| 初期状態 | ブランド登録時は商品ゼロ。削除可能 |
| 運用中 | ほとんどのブランドに何かしらの商品が紐づく |
| 実質的な制約 | 「ブランドは削除できない」と同義になる |
| 結論 | 複雑な制約チェック機構を作る意味があるか再検討すべき |
要件緩和の選択肢
| 緩和案 | 説明 |
|---|---|
| 論理削除 | ブランドを「非アクティブ」にする。商品紐づきチェック不要 |
| 制約の廃止 | ブランド削除自体を禁止し、制約チェックを不要にする |
| 条件の変更 | 「N日以上商品が紐づいていないブランドのみ削除可」等 |
| 許容 | 紐づき先のないゴミデータの存在を受け入れる |
ステップ2: そもそもモデリングの問題ではないか
集約間の制約が必要に見える場合、集約の境界が間違っている可能性がある。
トランザクションは複数のエンティティにまたがりますか? この質問の答えがイエスならば、間違った集約ルートを持っていると言えるでしょう。 --- Lightbend Academy
関係性に基づく判断フロー
2つの「集約」を常に一緒に操作する必要がある場合:
A : B の関係は?
├─ 1:1 → 同一集約に統合を検討
│ 例: Task(report: TaskReport)
│ → 1トランザクションで不変条件を維持できる
│
├─ 1:N(少量) → 要件調整で小規模化できないか検討
│ → 可能なら同一集約に統合
│
├─ 1:N(大量) → 別集約 + 結果整合性
│ → ドメインイベントで連携
│
└─ Bは純粋なクエリ要件か?
├─ YES → CQRSのリードモデル(プロジェクション)で対応
│ → Bは集約ではなくビューとして構築
└─ NO → ドメイン知識を持つ → 独立集約 + 結果整合性
具体例: Task と TaskReport
「TaskReportの作成を忘れるとまずい」ならば、両者は独立できない可能性が高い。
// 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))
// TaskReport作成忘れが構造的に不可能になる
}
}
}
TaskReportが純粋なクエリ要件なら、CQRSのリードモデルとして構築すべき。集約ではなくプロジェクションで対応することで、制約チェック自体が不要になる。
ステップ3: Sagaの誤用を避ける
集約間の制約チェックにSagaを使おうとするのは、よくある誤用パターンである。
Sagaの本来の目的
Sagaは複数の操作を順番に実行し、失敗時に補償するためのパターンである。
Sagaの正しい適用:
注文受付 → 在庫引当 → 決済処理 → 配送手配
(どこかで失敗したら補償トランザクションで巻き戻す)
Sagaの誤用:
「ブランドに商品が紐づいているか確認して、紐づいていたら削除を拒否する」
→ これは一連の操作ではなく、ただの制約チェック。Sagaの出番ではない
誤用パターンの検出基準
| 観点 | Sagaが適切 | Sagaが不適切 |
|---|---|---|
| 目的 | 複数ステップの分散トランザクション管理 | 単純なデータ存在チェック |
| 性質 | 長時間にわたる複数操作の協調 | 同期的な制約確認 |
| 失敗時 | 補償トランザクションで巻き戻し | 操作の拒否 |
ステップ4: CQRS/ESの技術的制約を理解する
大原則: コマンド側はコマンド側だけで解決する
コマンドがクエリ側のリードモデルに依存してはならない。コマンド側はコマンド側だけで解決できないと設計が破綻する。
❌ 禁止: コマンド側 → リードモデル(クエリ側)を参照して判断
✅ 許可: コマンド側 → 他の集約にコマンド/メッセージで問い合わせ
なぜリードモデルに依存してはいけないか:
| 理由 | 説明 |
|---|---|
| イベントの非同期性 | イベントストアへの書き込みとリードモデルへの反映の間にラグが発生する |
| レース条件 | リードモデル参照→コマンド実行の間に状態が変わる可能性がある |
| 責任分離違反 | コマンドモデル自体の検証ロジックが曖昧になる |
| 結果整合性との矛盾 | 強い整合性が必要な操作で結果整合性に依存する危険性 |
アンチパターン: リードモデルで事前チェック
// ❌ コマンド側がクエリ側に依存している
class CreateProductUseCase(
private val productRepository: ProductRepository,
private val brandReadModelRepository: BrandReadModelRepository, // リードモデルへの依存
) {
fun execute(brandId: BrandId, productName: String) {
// ① リードモデルでブランド存在チェック
val brandExists = brandReadModelRepository.existsById(brandId)
if (!brandExists) throw BrandNotFoundException(brandId)
// ② 商品作成
// ⚠ ①と②の間でブランドが削除される可能性(レース条件)
val product = Product.create(brandId, productName)
productRepository.store(product)
}
}
このリードモデル参照は「無いよりまし」程度の位置づけであり、完全な整合性は保証できない。
他集約の状態確認方法(コマンド側で完結する方法)
リードモデルではなく、コマンド側の仕組みで他集約の状態を確認する。
方法1: 集約に直接問い合わせ(アクターモデル)
// Akka/Pekko: 組み込みメッセージで存在確認
brandActorRef ! Identify(brandId)
// → ActorIdentity メッセージが返る
// Typed Actor: カスタムメッセージで状態確認
brandActorRef ! ExistsBrand(brandId, replyTo = self)
方法2: リポジトリ/ドメインサービスで参照(参照のみ)
// ✅ 他の集約をリポジトリで参照するだけなら許可(更新は不可)
class CreateProductUseCase(
private val productRepository: ProductRepository,
private val brandRepository: BrandRepository, // コマンド側のリポジトリ
) {
fun execute(brandId: BrandId, productName: String) {
// コマンド側のリポジトリで存在確認(リードモデルではない)
val brand = brandRepository.findById(brandId)
?: throw BrandNotFoundException(brandId)
val product = Product.create(brandId, productName)
productRepository.store(product)
// ※ brandの更新はしない(参照のみ)
}
}
注意: 参照はDDD原則に反しないが、複数集約の更新を同一トランザクションにすることは不可。
Application層での複数集約操作の原則
| 操作 | 可否 | 理由 |
|---|---|---|
| 他集約の参照 | OK | 読み取りのみなら問題なし |
| 他集約の更新(別トランザクション) | OK | 結果整合性で対応 |
| 他集約の更新(同一トランザクション) | NG | 集約の整合性境界を破壊する |
イベントストアの制約
イベントストアは基本的に集約IDでしかアクセスできない。
可能: 商品ID → 商品集約のイベント履歴
不可: ブランドID → そのブランドに紐づく商品の一覧
「このブランドIDに紐づく商品があるか?」という逆引きはイベントストアでは直接実行できない。
逆引きを実現する場合のコスト
逆引きが必要な場合、ブランドIDから商品IDを解決できるインデックス用リードモデルを別途用意し、商品の登録・更新・削除のたびにそのインデックスも更新する必要がある。
Product集約 → ProductCreatedEvent → リードモデル更新
↓
Brand-Product インデックス
↓
Brand削除時 → インデックスを参照して紐づき確認
できなくはないが、かなり複雑になる。 その複雑さに見合うビジネス価値があるかを先に検討すべき。
ステップ5: 覚悟を決める
CQRS/ESを採用する覚悟
CQRS/ESの非同期・イベント駆動的な性質上、以下は避けられない:
- イベント処理遅延による一時的な不整合
- 補償トランザクション失敗時のゴミデータ
- リードモデルと書き込みモデルの一時的なズレ
これらを「問題」と見なすのではなく、分散システムの基本的な特性として設計段階から組み込む必要がある。
CQRS/ESをやるんだったらそれぐらいの覚悟を持たないとだめ
許容すべきこと
| 許容すべき | なぜ |
|---|---|
| 一時的な不整合データ | 結果整合性の本質 |
| 紐づき先のないデータ | 集約の独立性の代償 |
| リードモデルの遅延 | 非同期処理の本質 |
許容してはいけないこと
| 許容不可 | なぜ |
|---|---|
| 集約内部の不整合 | 集約は強い整合性境界 |
| ビジネスルールの破壊 | 不整合と要件違反は別 |
| 永続的なデータ不整合 | 結果整合性は「最終的に一致する」こと |
判断フロー(全体)
集約間の制約チェックが必要になった
↓
1. その制約は本当に必要か? → 要件を疑う
├─ 不要 → 制約を廃止。問題解消
└─ 必要 ↓
2. そもそもモデリングの問題ではないか?
├─ 1:1の関係 → 同一集約に統合。制約チェック不要に
├─ 片方がクエリ要件 → CQRSのリードモデルで対応。集約不要
└─ 独立した集約である必要がある ↓
3. Sagaを誤用しようとしていないか?
├─ 制約チェックにSaga → 不適切。Sagaは分散トランザクション管理用
└─ OK ↓
4. コマンド側だけで解決できるか?
├─ YES → 他集約にリポジトリ/メッセージで参照(リードモデル依存は不可)
└─ NO ↓
5. リードモデルでの「ベストエフォート」チェックで許容できるか?
├─ YES → リードモデル参照 + レース条件を許容 + 結果整合性
└─ NO ↓
6. 強い一貫性が絶対に必要か?
├─ YES → 集約境界の見直しが必要(設計の問題)
└─ NO → 結果整合性 + ゴミデータの許容
関連スキルとの関係
| スキル | 関係 |
|---|---|
aggregate-design | 集約内部の設計原則。本スキルは集約間の制約を扱う |
aggregate-transaction-boundary | トランザクション境界と結果整合性。本スキルは制約チェックに焦点 |
cqrs-tradeoffs | 結果整合性の概念的トレードオフ。本スキルは実践的な覚悟を扱う |
レビューチェックリスト
要件
- 集約間の制約が本当にビジネス上必要か問い直したか
- ライフサイクル全体で制約の実効性を検証したか
- 要件緩和の可能性を検討したか
設計
- Sagaを制約チェックに誤用していないか
- 同一集約への統合の可能性を検討したか(1:1関係なら統合が自然)
- 片方が純粋なクエリ要件なら、CQRSのリードモデルで対応できないか
- イベントストアの逆引き制約を理解しているか
- コマンド側がクエリ側(リードモデル)に依存していないか
- 他集約の参照はコマンド側のリポジトリ/メッセージングで行っているか
- Application層で複数集約の更新を同一トランザクションにしていないか
実装
- 逆引きが必要な場合、リードモデルのコストを見積もったか
- 一時的な不整合データの存在を許容する設計になっているか
- 複雑さに見合うビジネス価値があるか評価したか
関連スキル(併読推奨)
このスキルを使用する際は、以下のスキルも併せて参照すること:
aggregate-design: 集約境界の設計(制約問題の根本原因)aggregate-transaction-boundary: 1トランザクション=1集約ルールと結果整合性cqrs-aggregate-modeling: CQRS/ESによるイベント駆動の結果整合性
レビュー
まだレビューはありません。使ってみた感想をお寄せください。
同じリポジトリのスキル
概要と使いどころ
集約とトランザクション境界の関係を明確化し、複数集約を単一トランザクションに含めるアンチパターンを 検出・是正する。集約は強い整合性境界であり、ユースケースで複数集約を更新する場合は結果整合性を 使うべきという原則を適用する。コードレビュー、ユースケース設計、リファクタリング時に トランザクション境界の問題を検出する場合に使用。 対象言語: 言語非依存(Java, Kotlin, Scala, TypeScript, Go, Rust, Python等すべて)。 トリガー:「複数集約を同じトランザクションで更新している」「ユースケースに@Transactionalがある」 「集約間の整合性をどう取るか」「Sagaパターンを使うべきか」「トランザクション境界の設計」 「1トランザクション1集約」「結果整合性の実装」「集約をまたぐトランザクション」 といったトランザクション境界関連リクエストで起動。
後方互換性がゴミコードを量産する構造を検出し、互換性を「契約と撤去計画」として管理する ガバナンスを支援するスキル。公開API境界の明確化、非推奨化サイクル(deprecation cycle)の 制度化、互換層の局所化(Adapter/Strangler Fig)、契約テスト(CDC)による互換性検証、 AI生成コードの互換性ゲート設計を含む。コードレビュー、API設計、リファクタリング、 レガシー移行時に互換性起因の技術的負債を防ぐために使用。 対象言語: 言語非依存(Java, TypeScript, Go, Python, Rust等すべて)。 トリガー:「後方互換性を保ちたい」「非推奨APIをどうする」「互換層が増えてきた」 「レガシー移行の戦略」「API設計レビュー」「互換性のためのコードが多い」 「deprecation policyを作りたい」「破壊的変更の管理」といった互換性管理関連リクエストで起動。
getterの濫用を防ぐための命名規約スキル。ドメインモデルでgetterが必要な場合(永続化、JSON変換など)に `breachEncapsulationOf` プレフィックスを付与することで、カプセル化を破っていることを明示する。 これにより、Tell Don't Ask原則の違反を未然に防ぎ、getterの意図しない使用を抑制する。 コードレビュー、新規実装、リファクタリング時にgetter設計が必要な場合に使用。 対象言語: Java, Kotlin, Scala, TypeScript, Python, Go, Rust。 トリガー:「getterの命名規約」「カプセル化を破るgetter」「永続化用のgetter」 「breachEncapsulation」「getterを作りたいが濫用を防ぎたい」といったgetter命名関連リクエストで起動。
クリーンアーキテクチャを採用しているプロジェクト向けの設計・レビュー支援。4層構造(ドメイン層、 ユースケース層、インターフェースアダプタ層、インフラストラクチャ層)に基づく。特にインフラ層は 横断的関心事(ロギング、設定管理)のみ、永続化やRPCはインターフェースアダプタ層に配置すべき という原則を適用する。トリガー:「クリーンアーキテクチャで」「クリーンアーキテクチャに従って」 「クリーンアーキテクチャのレビュー」など、クリーンアーキテクチャを明示的に指定した場合のみ起動。 一般的な「設計レビュー」「アーキテクチャ相談」では起動しない。
CQRS/ESが集約の境界定義とモデリングに与える影響を解説する。CQRSを導入すると集約は コマンド実行に必要な最小限の状態のみ保持すればよくなり、読み取り責務はリードモデルに 委譲できる。大きすぎる集約の軽量化、集約境界の再定義、イベントによる状態管理を支援する。 集約設計、CQRS導入時のモデリング見直し、パフォーマンス問題の解決時に使用。 対象言語: 言語非依存。 トリガー:「CQRSで集約が変わる」「集約が大きすぎる」「集約にメッセージ1000件」 「集約の更新が重い」「CQRS導入で集約を見直す」「集約を軽量化したい」 「集約にクエリ用データが混ざっている」「集約の境界を再定義」 といったCQRS/モデリング関連リクエストで起動。