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 CQRScqrs-aggregate-modeling: Aggregate modeling under event sourcingaggregate-transaction-boundary: Resolving the double commit problem with an event store