SuperAPI AI Directory

cqrs-to-event-sourcing

Data & analytics

View repository

Updated 10-15 22:39

About this skill

Why CQRS Leads to Event Sourcing

When you implement CQRS, you face the synchronization problem from the C side to the Q side, which leads to event sourcing. This is not an option; it is an implementation necessity.

A common misunderstanding: "CQRS does not require separating models"

This interpretation is a dangerous misreading.

The correct meaning is that "a system can be divided into CQRS areas and non-CQRS areas, and CQRS can be introduced partially." The reason models do not need to be separated isnon-CQRS areasand within CQRS areas, splitting the command model and the query model is mandatory.

システム全体
├── CQRS領域     → モデル分割は必須
└── 非CQRS領域   → 分割不要(従来のCRUDで十分)

The interpretation that "CQRS does not require splitting models" is no longer CQRS.

Differences between data on the C side and the Q side

In CQRS, the data on the C side (commands) and the Q side (queries) is fundamentally different.

Concrete example: a cart system

C-side table(minimum data required to persist the domain model):

Cart Table Cart Item Table
Cart ID (PK) Cart Item ID (PK)
Customer account ID Cart ID (FK)
Budget limit amount Product ID
Created at Quantity
Created at

Q-side table(data required for display/search):

Cart Table Cart Item Table
Cart ID (PK) Cart Item ID (PK)
Customer account ID Product ID
Customer account name ★ Product name ★
Budget limit amount Quantity
Total amount ★ Unit price ★
Price ★

The values marked with ★do not exist in the C-side database. They are derived values calculated by the behavior of the domain object.

case class Cart(id: CartId, items: CartItems, ...) {
  // 合計金額はドメインオブジェクトの計算結果であり、DBに保存されない
  def totalPrice(priceResolver: ItemId => Price): Price =
    items.fold(Price.zero){ (t, item) => t + item.price(priceResolver) }
}

case class CartItem(id: CartItemId, itemId: ItemId, quantity: Quantity, ...) {
  // 単価は外部から提供され、価格は計算される
  def price(priceResolver: ItemId => Price): Price =
    priceResolver(itemId) * quantity
}

Step-by-step examination of synchronization methods and their limitations

Method 1: Synchronization using triggers

When a C-side table is updated, write to the Q side using SQL.

Limitations:

  • Effective for transferring static data (such as customer names)
  • Calculated values (★) cannot be synchronized - The calculation results of domain logic do not exist in the DB
  • Writing SQL for the same calculation logic is difficult and creates duplication of business logic
  • If, as a desperate measure, calculation results are also stored on the C side, the repository interface becomes distorted
// ❌ リポジトリに計算ロジックの責務が漏れる
trait CartRepository {
  def store(cart: Cart, priceResolver: ItemId => Price): Unit
  // priceResolverはリポジトリの責務ではない
}

Method 2: Synchronization using polling

A program reads the C-side table, calculates with domain objects, and writes to the Q side.

Limitations:

  • Transferring calculation results to the Q side is possible
  • It cannot detect "when a change occurred" - There is no change trigger
  • It is necessary to poll the entire set of aggregates,No scalability
  • which is not practical when a large number of aggregates exist

Conclusion: Even to obtain the latest state,an update event is necessary。

Method 3: Introducing an event notification queue

On change, publish an event to a queue, and the Q-side update program receives the event and synchronizes.

C側リポジトリ → RDB(テーブルA) + メッセージキュー(更新イベント)
                                         ↓
                            リードモデル更新プロセス
                                         ↓
                            C側テーブルから集約再現 → Q側テーブル書き込み

Limitation: The dual commit problemoccurs.

  • The RDB and the message queue are different storages
  • and cannot be integrated into the same transaction
  • A successful write to the RDB + a failed write to the queue (or vice versa) can occur
  • and may incur high costs

The inevitable destination: Event Sourcing

To avoid the dual commit problem,make events the true data source。

A shift in design

従来:
  C側DB(状態を保存) + メッセージキュー(通知用)
  → 2つのストレージへの書き込み = ダブルコミット問題

イベントソーシング:
  イベントストア(イベントが真のデータソース)
  → 1つのストレージへの書き込みのみ
  → C側の状態はイベントから導出
  → Q側の状態もイベントから導出

Result

  • The C-side DB no longer needs to be an RDB (any system capable of appending events and reading by ID will do)
  • NoSQL (KVS) is often suitable
  • With DynamoDB Streams and the like, events can be read scalably
  • This is Event Sourcing
イベントストア(真のデータソース)
    │
    ├──→ C側: イベントからドメイン状態を再構築
    │
    └──→ Q側: イベントからリードモデルを構築
              (計算された値も含めて自由に構築可能)

Why most CQRS systems adopt Event Sourcing

According to a survey by Lightbend, most CQRS systems adopt Event Sourcing.

Stage Method Problem
1 Trigger Cannot synchronize calculated values
2 Polling Cannot detect changes, does not scale
3 Event Queue Dual commit problem
4 Event Sourcing Solves all of the above

Event Sourcing is not an option; when CQRS is implemented seriously,it is a design pattern that is inevitably reachedis.

Decision flow

CQRSの導入を検討している
    ↓
C側とQ側のデータは同一か?
    ├─ YES → CQRSは不要。従来のCRUDで十分
    └─ NO(Q側に計算値・結合データがある) ↓
        C→Qの同期をどうするか?
        ├─ トリガー → 計算値は同期できない
        ├─ ポーリング → 変更検知・スケーラビリティの問題
        ├─ イベントキュー → ダブルコミット問題
        └─ Event Sourcing → すべて解決

Relationship with related skills

Skills Relationship
cqrs-tradeoffs Consistency, availability, and scalability of CQRS. This skill coverswhy ES is necessary
aggregate-transaction-boundary Transaction boundaries. This skill coversC→Q synchronizationissues
cross-aggregate-constraints Cross-aggregate constraints. A problem on a different dimension from this skill

Review Checklist

CQRS design

  • Are the CQRS domain and non-CQRS domain clearly separated?
  • Are the command model and query model separated within the CQRS domain?
  • Is it not a contradictory design, namely “CQRS but models are not separated”?

C→Q synchronization

  • Have you checked whether the data needed on the Q side includes calculated values that do not exist in the C-side DB?
  • If there are calculated values, do you understand that triggers cannot solve this?
  • Is a mechanism for C→Q synchronization (event-driven) designed?
  • Are you aware of the dual commit problem and addressing it?

Event Sourcing

  • Is the design one in which events are the true data source?
  • Can the state on the C side be derived from events?
  • Can the read model on the Q side be built from events?

Related skills (recommended for concurrent reading)

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

  • cqrs-tradeoffs: Trade-off analysis for deciding to adopt CQRS
  • cqrs-aggregate-modeling: Aggregate modeling under event sourcing
  • aggregate-transaction-boundary: Resolving the double commit problem with an event store