# Cross Aggregate Constraints

> 集約間の制約チェック（「集約Aのユースケースで集約Bの状態を確認したい」）に対する 設計判断を支援する。Sagaの誤用検出、ビジネス要件の問い直し、CQRS/ESにおける 技術的制約の理解、不整合データの許容判断を含む。 コードレビュー、アーキテクチャ設計、要件分析時に集約間の制約問題に直面した場合に使用。 対象言語: 言語非依存。 トリガー：「集約間の制約チェック」「他の集約の状態を確認したい」「Sagaで制約チェック」 「ブランドに商品が紐づいているか確認」「集約間のバリデーション」 「イベントソーシングで逆引き」「集約をまたぐビジネスルール」「CQRS/ESの制約」 「コマンド側からリードモデルを参照していい？」「ユースケースで他の集約を参照」 といった集約間制約関連リクエストで起動。

- Skill: `j5ik2o/cross-aggregate-constraints` (Agent Skill)
- Install (CLI): `npx skillmds@latest add j5ik2o/cross-aggregate-constraints`
- Raw SKILL.md: https://api.skillmd.com/api/skills/j5ik2o/cross-aggregate-constraints/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: j5ik2o (https://skillmd.com/u/j5ik2o)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/j5ik2o/cross-aggregate-constraints

---


# 集約間の制約チェック

集約間の制約に直面したら、まず要件を疑い、次に技術的制約を理解し、最後に覚悟を決める。

## 典型的な問題

「集約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の作成を忘れるとまずい」ならば、両者は独立できない可能性が高い。

```kotlin
// 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の技術的制約を理解する

### 大原則: コマンド側はコマンド側だけで解決する

**コマンドがクエリ側のリードモデルに依存してはならない。コマンド側はコマンド側だけで解決できないと設計が破綻する。**

```
❌ 禁止: コマンド側 → リードモデル（クエリ側）を参照して判断
✅ 許可: コマンド側 → 他の集約にコマンド/メッセージで問い合わせ
```

**なぜリードモデルに依存してはいけないか**:

| 理由 | 説明 |
|------|------|
| イベントの非同期性 | イベントストアへの書き込みとリードモデルへの反映の間にラグが発生する |
| レース条件 | リードモデル参照→コマンド実行の間に状態が変わる可能性がある |
| 責任分離違反 | コマンドモデル自体の検証ロジックが曖昧になる |
| 結果整合性との矛盾 | 強い整合性が必要な操作で結果整合性に依存する危険性 |

### アンチパターン: リードモデルで事前チェック

```kotlin
// ❌ コマンド側がクエリ側に依存している
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: 集約に直接問い合わせ（アクターモデル）**

```scala
// Akka/Pekko: 組み込みメッセージで存在確認
brandActorRef ! Identify(brandId)
// → ActorIdentity メッセージが返る

// Typed Actor: カスタムメッセージで状態確認
brandActorRef ! ExistsBrand(brandId, replyTo = self)
```

**方法2: リポジトリ/ドメインサービスで参照（参照のみ）**

```kotlin
// ✅ 他の集約をリポジトリで参照するだけなら許可（更新は不可）
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によるイベント駆動の結果整合性

