SuperAPI AI Directory

cross-aggregate-constraints

Coding & development

View repository

Updated 10-15 18:26

About this skill

Constraint checks between aggregates

When you face a constraint between aggregates, first question the requirements, then understand the technical constraints, and finally make up your mind.

Typical problem

A requirement arises to "check the state of aggregate B in a use case for aggregate A."

例: 「一つでも商品が紐づいているブランドは削除できない」

  Brand集約(削除したい)←── 制約チェック ──→ Product集約(紐づき確認)

For this kind of requirement, you should not hastily jump to a technical solution.

Step 1: Question the requirements

Before technology, re-examine the business requirement itself.

Framework for re-examination

その制約は本当に必要か?
    ↓
ライフサイクル全体で考えるとどうなるか?
    ↓
実運用では実質的にどういう制約になるか?
    ↓
複雑な実装に見合うビジネス価値があるか?

Concrete example: Brand deletion constraint

Re-examining "a brand with even one product linked to it cannot be deleted":

Perspective Analysis
Initial state When a brand is registered, it has zero products. It can be deleted
During operation Almost all brands have some product linked to them
Substantive constraint It becomes synonymous with "brands cannot be deleted"
Conclusion Whether it makes sense to build a complex constraint-checking mechanism should be reconsidered

Options for relaxing requirements

Relaxation option Description
Logical deletion Make the brand "inactive." No need to check product linkage
Abolition of the constraint Prohibit brand deletion itself, making constraint checks unnecessary
Change of conditions For example, "only brands with no linked products for N or more days can be deleted"
Tolerance Accept the existence of garbage data with no linked destination

Step 2: Is this a modeling problem in the first place?

When constraints between aggregates appear necessary,the aggregate boundaries may be wrongthere is a possibility.

Do transactions span multiple entities? If the answer to this question is yes, you can say you have the wrong aggregate root. --- Lightbend Academy

Decision flow based on relationships

When two "aggregates" must always be operated on together:

A : B の関係は?
    ├─ 1:1 → 同一集約に統合を検討
    │         例: Task(report: TaskReport)
    │         → 1トランザクションで不変条件を維持できる
    │
    ├─ 1:N(少量) → 要件調整で小規模化できないか検討
    │         → 可能なら同一集約に統合
    │
    ├─ 1:N(大量) → 別集約 + 結果整合性
    │         → ドメインイベントで連携
    │
    └─ Bは純粋なクエリ要件か?
          ├─ YES → CQRSのリードモデル(プロジェクション)で対応
          │         → Bは集約ではなくビューとして構築
          └─ NO → ドメイン知識を持つ → 独立集約 + 結果整合性

Concrete example: Task and TaskReport

If "it is bad to forget to create a TaskReport," then the two are highly likely not independent.

// 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作成忘れが構造的に不可能になる
        }
    }
}

If TaskReport is a pure query requirement, it should be built as a read model in CQRS. By handling it with a projection rather than an aggregate, the constraint check itself becomes unnecessary.

Step 3: Avoid misuse of Saga

Trying to use Saga for constraint checks between aggregates is a common misuse pattern.

The original purpose of Saga

SagaはExecute multiple operations in sequence and compensate on failureThis is a pattern for.

Sagaの正しい適用:
  注文受付 → 在庫引当 → 決済処理 → 配送手配
  (どこかで失敗したら補償トランザクションで巻き戻す)

Sagaの誤用:
  「ブランドに商品が紐づいているか確認して、紐づいていたら削除を拒否する」
  → これは一連の操作ではなく、ただの制約チェック。Sagaの出番ではない

Criteria for detecting misuse patterns

Perspective Saga is appropriate Saga is inappropriate
Purpose Managing multi-step distributed transactions Simple data existence check
Nature Coordination of multiple operations over a long period Synchronous constraint verification
On failure Roll back with compensating transactions Rejection of the operation

Step 4: Understand the technical constraints of CQRS/ES

Guiding principle: The command side must be resolved by the command side alone

Commands must not depend on the read model on the query side. If the command side cannot be resolved by the command side alone, the design falls apart.

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

Why you must not depend on the read model:

Reason Explanation
Asynchronicity of events A lag occurs between writing to the event store and reflecting it in the read model
Race conditions The state may change between referencing the read model and executing the command
Violation of separation of responsibilities The validation logic of the command model itself becomes ambiguous
Contradiction with eventual consistency The danger of relying on eventual consistency for operations that require strong consistency

Anti-pattern: Pre-checking with the read model

// ❌ コマンド側がクエリ側に依存している
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)
    }
}

This read model reference is positioned as being merely "better than nothing," and complete consistency cannot be guaranteed.

Methods for checking the state of other aggregates (methods that are completed on the command side)

Check the state of other aggregates not via the read model but via mechanisms on the command side.

Method 1: Query the aggregate directly (actor model)

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

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

Method 2: Reference via a repository/domain service (reference only)

// ✅ 他の集約をリポジトリで参照するだけなら許可(更新は不可)
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の更新はしない(参照のみ)
    }
}

Note: Reference does not violate DDD principles, butupdating multiple aggregates in the same transaction is not possible。

Principles for operating multiple aggregates in the Application layer

Operation Permissibility Reason
Other aggregates'reference OK No problem if it is read-only
Other aggregates'update(separate transaction) OK Handle with eventual consistency
Other aggregates'update(same transaction) NG Destroys the aggregate's consistency boundary

Constraints of the event store

The event store can basically only be accessed by aggregate ID.

可能: 商品ID → 商品集約のイベント履歴
不可: ブランドID → そのブランドに紐づく商品の一覧

A reverse lookup such as "Are there any products linked to this brand ID?" cannot be performed directly in the event store.

The cost of implementing a reverse lookup

If a reverse lookup is needed, you must separately prepare an index read model that can resolve product IDs from brand IDs, and update that index every time a product is registered, updated, or deleted.

Product集約 → ProductCreatedEvent → リードモデル更新
                                       ↓
                                    Brand-Product インデックス
                                       ↓
Brand削除時 → インデックスを参照して紐づき確認

It is not impossible, but it becomes quite complex. You should first consider whether there is business value commensurate with that complexity.

Step 5: Resolve yourself

Resolve to adopt CQRS/ES

Due to the asynchronous and event-driven nature of CQRS/ES, the following are unavoidable:

  • Temporary inconsistency due to event processing delays
  • Garbage data when compensating transactions fail
  • Temporary divergence between the read model and the write model

Rather than regarding these as "problems,"incorporate them from the design stage as fundamental characteristics of distributed systemsit is necessary to.

If you are going to do CQRS/ES, you need that level of resolve

Things that should be tolerated

Should be tolerated Why
Temporary inconsistent data The essence of eventual consistency
Data with no linked destination The cost of aggregate independence
Read model delay The essence of asynchronous processing

Things that must not be tolerated

Not tolerable Why
Inconsistency within an aggregate An aggregate is a strong consistency boundary
Destruction of business rules Inconsistency and requirement violations are different
Persistent data inconsistency Eventual consistency means "eventually matching"

Decision flow (overall)

集約間の制約チェックが必要になった
    ↓
1. その制約は本当に必要か? → 要件を疑う
    ├─ 不要 → 制約を廃止。問題解消
    └─ 必要 ↓
2. そもそもモデリングの問題ではないか?
    ├─ 1:1の関係 → 同一集約に統合。制約チェック不要に
    ├─ 片方がクエリ要件 → CQRSのリードモデルで対応。集約不要
    └─ 独立した集約である必要がある ↓
3. Sagaを誤用しようとしていないか?
    ├─ 制約チェックにSaga → 不適切。Sagaは分散トランザクション管理用
    └─ OK ↓
4. コマンド側だけで解決できるか?
    ├─ YES → 他集約にリポジトリ/メッセージで参照(リードモデル依存は不可)
    └─ NO ↓
5. リードモデルでの「ベストエフォート」チェックで許容できるか?
    ├─ YES → リードモデル参照 + レース条件を許容 + 結果整合性
    └─ NO ↓
6. 強い一貫性が絶対に必要か?
    ├─ YES → 集約境界の見直しが必要(設計の問題)
    └─ NO → 結果整合性 + ゴミデータの許容

Relationship with related skills

Skill Relationship
aggregate-design Design principles within an aggregate. This skill deals with constraints between aggregatesbetweendeals with the constraints of
aggregate-transaction-boundary Transaction boundaries and eventual consistency. This skill focuses on constraint checkingconstraint checkingfocuses on
cqrs-tradeoffs Conceptual trade-offs of eventual consistency. This skill deals with practical resolvepractical resolvedeals with

Review checklist

Requirements

  • Have you reconsidered whether constraints between aggregates are truly necessary for the business?
  • Have you verified the effectiveness of the constraints across the entire lifecycle?
  • Have you considered the possibility of relaxing the requirements?

Design

  • Are you not misusing Saga for constraint checking?
  • Have you considered the possibility of merging into the same aggregate? (If it is a 1:1 relationship, merging is natural)
  • If one side is a pure query requirement, can't it be handled by the CQRS read model?
  • Do you understand the reverse lookup constraints of the event store?
  • Does the command side depend on the query side (read model)?
  • Are references to other aggregates performed via the command side's repository/messaging?
  • Are multiple aggregate updates not being made in the same transaction in the Application layer?

Implementation

  • If reverse lookup is required, have you estimated the cost of the read model?
  • Is the design one that allows the existence of temporarily inconsistent data?
  • Have you evaluated whether there is business value commensurate with the complexity?

Related skills (recommended for co-reading)

When using this skill, also refer to the following skills:

  • aggregate-design: Aggregate boundary design (the root cause of constraint problems)
  • aggregate-transaction-boundary: One transaction = one aggregate rule and eventual consistency
  • cqrs-aggregate-modeling: Event-driven eventual consistency via CQRS/ES