SuperAPI AI Directory

aggregate-transaction-boundary

General & other

View repository

Updated 10-16 07:23

About this skill

Aggregates and Transaction Boundaries

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:

  1. Violation of the aggregate definition: An aggregate is a strong consistency boundary. Putting multiple aggregates in one transaction effectively creates one huge consistency boundary
  2. Hinders scalability: Updates to aggregate A and aggregate B always come as a set, making later scaling and server distribution difficult
  3. Impossible in heterogeneous DB environments: If a different data store is used for each aggregate, the same transaction cannot be implemented
  4. 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 consistency
  • cqrs-aggregate-modeling: Aggregate weight reduction and resolution of transaction issues through CQRS/ES