[DDD]CQSとCQRS、イベントソーシング的設計について①
CQSとCQRS、イベントソーシング的設計について
本記事ではCQSとCQRSの違いを整理し、さらにEvent Sourcing的設計についてまとめます。
理論的な話がメインで実装例はこの記事ではありませんが、
それはまた別で書いていきたいと思っています。
📍CQS (Command Query Separation):コマンド・クエリ分離の原則
CQSは1988年にBertrand Meyerが提唱したソフトウェア設計における原則の一つで、
「副作用(データの変更など)を伴うコマンド」か「副作用を伴わずデータを返すクエリ」の
どちらかに明確に分け、両方を同時に実行するメソッドを避けることで、
システムの副作用によるリスクを最小限に抑え、品質を向上させる考え方のことだ。
- Command(コマンド):システムの状態を変更するが、値を返さない操作
- Query(クエリ):システムの状態を変更せず、値のみを返す操作
data class User(
private var name: String,
private var age: Int
) {
// Query: 公開用プロパティ (getter のみ)
val name: String
get() = name
val age: Int
get() = age
// Command: 状態を変更する操作はメソッドで定義
fun updateName(newName: String) {
require(newName.isNotBlank()) { "名前は空にできません" }
_name = newName
}
fun haveBirthday() {
age++
}
}
// --- Kotlinらしい拡張関数でQueryを追加 ---
fun User.isAdult(): Boolean = this.age >= 20
こうすることにより、副作用を最小限に抑えることができます。
📍CQRS (Command Query Responsibility Segregation):コマンド・クエリ責務分離
CQRSはCQSを発展させた概念で、Greg Youngによって普及されました。
データの読み取り(Query)と書き込み(Command)のモデルを完全に分離すると言うものです。
特徴:
- 読み取り用と書き込み用で異なるデータモデルを使用
- 読み取り専用データベースと書き込み専用データベースを分離することも可能
- 複雑なドメインでのパフォーマンス最適化に有効
// Command側 - 書き込み専用モデル
data class CreateUserCommand(val name: String, val email: String)
class UserCommandHandler {
fun handle(command: CreateUserCommand) {
// ユーザー作成ロジック
// 書き込み専用データストアに保存
}
}
// Query側 - 読み取り専用モデル
data class UserViewModel(
val id: String,
val displayName: String,
val department: String // 表示用に最適化されたデータ
)
class UserQueryService {
fun getUserById(id: String): UserViewModel? {
// 読み取り専用データストアから取得
return null
}
}
📖 まとめると
Command(状態変更)と Query(データ取得)を明確に分けるという思想に違いがないですが、
まとめると以下のようになります。
項目 | CQS (Command Query Separation) | CQRS (Command Query Responsibility Segregation) |
---|---|---|
種類 | 設計原則の一つ | アーキテクチャパターン |
分離対象 | メソッド (状態変更とデータ取得を分離) |
モデル (書き込みモデルと読み取りモデルを分離) |
目的 | 予測可能性・可読性・テスト容易性の向上 | 複雑なドメインの整理、スケーラビリティ・性能の向上 |
適用範囲 | 小規模〜中規模の設計でも有効 | 複雑・大規模なシステムに適する |
デメリット | 単体では構造的な負担は少ない | 小規模システムでは過度な複雑性を招く可能性 |
❓ なぜCQRSが生まれたのだろう??
CQSの原則をOODでは、
エンティティやドメインモデルに「状態の取得」と「状態の変更」を混在させるのが自然だったが、
システムの規模が大きくなり、以下のような問題がでてきた、
-
責務の混在
読み取りと書き込みを同じモデルに詰め込むと、コードが肥大化し,可読性やテスト容易性が下がる。 -
スケーラビリティの限界
読み取り処理は大量だが軽量、書き込み処理は少ないが複雑、と
特性が異なるのに同じ仕組みで扱うため効率が悪い。 -
複雑なドメインの整理困難
特にイベントソーシングや高スループットなシステムでは、
読み取りと書き込みの関心を明確に分けないと保守が難しい。
ドメイン駆動設計(DDD)では
「複雑なビジネスルールを正しくモデル化し、コードで表現する」ことを目的としているが、
DDDを適用した場合でも読み取りと書き込みが同じモデルに収めると、
モデルが次第に肥大化する問題は出てきます。
-
集約(Aggregate)の責務
- 集約は「一貫性を保証するための境界」を持ち、状態の変更(Command)に強く関わるものだ。
- しかし読み取り用途(Query)では、一貫性よりも「効率的にデータを取得できること」 が求められる。
-
関心の分離
- 書き込みは「不変条件を守る」「ドメインルールを実行する」ことが目的。
-
読み取りは「利用者に必要な情報を返す」ことが目的。
→ 両者を同じモデルに入れると関心がぶつかり合い、DDDの恩恵(モデルの明確さ)が失われやすい。
こうした課題に対する答えとして生まれたのが、CQSの原則をアーキテクチャに落とし込んだ
CQRS(Command Query Responsibility Segregation)と言える。
「読み取りと書き込みの責務や特性の違いを無視した結果生じる、
複雑さやスケーラビリティ問題を解決するため」
「集約は書き込み専用に集中させ、読み取りは別のモデル(リードモデル)で最適化する」
これにより DDD の「ドメインの複雑さを明示的に管理する」という目的が
より達成しやすくなる。
👀 生まれた理由から見るCQRSの使い所
小規模なアプリケーションや単純な CRUD 中心のシステムに導入するには
逆に複雑性が増してしまうからあわないと言える。
必ずしも適用すべきものではなく、
「システムが抱えるドメインの複雑性」や「スケーラビリティ要求」に応じて適用すべきだ。
📍 State Sourcing と Event Sourcing
先にイベントソーシングの概要を言うと、
アプリケーションの現在の状態を直接保存するのではなく、
「その状態に至るまでに起きた出来事(イベント)の履歴」を保存する設計手法であり、
ドメインで発生したイベントを他のドメインに供給するためのデザインパターンだ。
< 従来型 (State Sourcing) >
- DBに「今の状態(current state)」を保存する。
- CRUDベースのシステムはほぼState Sourcingに該当する
<Event Sourcing>
- イベントを時系列で記録し、必要に応じてそれをリプレイして現在の状態を再構築する。
📍 イベントとは? DDDのドメインイベントとは?
Event Sourcing における イベント とは、
単なる操作ログではなく 「システム上で実際に起きた事実」 を表します。
つまり、「過去に起きた出来事」 を記録したものです。
DDDでいうドメインイベントも、「ドメインで意味を持つ出来事」を表す概念だ。
- ドメインイベントは過去形で表される(例: OrderPlaced, CustomerRelocated)。
- これにより、ビジネス的に重要な事実を時系列で捉えることができ、
後から状態を再構築したり、他のコンテキストへ通知したりできる。
Event Sourcing の「イベント」と DDD の「ドメインイベント」は密接に関係しており、
ビジネス上の出来事を履歴として保存し、それを基盤に状態を構築する
という発想でつながっている。
この辺の話は関数型ドメインモデリングの本が理解しやすかったですのでぜひ。
📖 コマンドは「リクエスト」、イベントは「結果」
コマンドは必ずしも成功するとは限らないが、
イベントはすでに起きた事実なので不変であり否定できない。
コマンド(Command)
- 「こうしてほしい」という 命令
- 未来志向であり、まだ実行されていないアクションを表す
例: PlaceOrder(注文を出す), ChangeAddress(住所を変更する)
イベント(Event)
- 「こうなった」という 事実
- 過去志向であり、すでに起きた出来事を表す
例: OrderPlaced(注文が出された), AddressChanged(住所が変更された)
📍 CQRSとイベントソーシングの関係性
今まで何度も出てきましたが、
- CQRS は「読み取り」と「書き込み」の責務を分けるアーキテクチャパターン
- Event Sourcing は「状態」ではなく「イベントの履歴」を保存する設計手法
両者独立した概念であるけども、
実際のシステム設計では相性が非常に良く一緒に語られる(使用される)ことが多い。
Event Sourcingを採用すると「イベントを発生 → 状態再構築 or Projection」といった流れが
必須になり、結果的にCQRSの仕組みが自然と組み込まれる。
目的は違うが、お互いを補完し合う関係にあるからセットで語られることが多い。
📍 Event Sourcing の意義とは
Event Sourcing は単なるデータ保存の仕組みではなく、
DDDの「複雑なビジネスルールを正しくモデル化する」という目的を
強力にサポートしますが、これらのメリットは複雑なドメインでこそ真価を発揮するため、
シンプルなCRUDアプリケーションでは過剰な複雑性を招く可能性があることも念頭に置くべきだ。
📖 ドメインが明瞭化される
上記でも挙げた関数型ドメインモデリングという本でも述べられている通り、
DDDではデータ型ではなくドメインとそのドメインが持つイベントが大事であり
焦点の当たるべきことで、その理由は
ビジネスは単にデータを持っているだけではなく、何らかの方法でデータを変換するからだ。
つまり、ユビキタス言語の中で「出来事」としてモデル化することで、
ドメインの動きや価値をより自然に表現できるようになる。
📖 イベントとコマンドの対比によって真のドメイン知識が明らかになる
Event Sourcing では、状態の保存ではなく「時間の経過とともに起きた出来事」を捉える。
コマンド(命令)とイベント(事実)の区別が明確になり、
システムの中で何が起きたかを「過去形」で表現することができる。
「時間の経過とともにどのように変化するか」
「ある事象が別の事象の前後に発生した場合の影響」
といった視点が、ドメインの問題を明らかにする鍵となるのだ。
つまり、イベントにフォーカスすることで
データでは見えにくかった本質的なドメイン知識が浮かび上がると言える。
📖 モデルの同期とスケーラビリティ(柔軟性と拡張性の向上)
読み取りモデルは用途ごとに複数用意できるため、
UI・集計・レポートなど、それぞれに最適化されたビューを生成可能だ。
新しい要求が出ても、既存のイベント履歴から新しいプロジェクションを構築できるため、
システムの柔軟性と拡張性が向上する。
📖 不変性と履歴の完全保存
状態そのものではなく「起きた出来事(イベント)」を時系列に沿って記録しますが、
イベントは append-only(追記のみ) で保存され、一度起きた事実は消えません。
そのため「いつ」「誰が」「何をしたのか」という履歴を失うことなく保持でき、
後からの検証や監査に強い基盤となる。
終わりに
今回は文章つらつら書いていったので
実践的なことをまた書いていきたいと思います。
参考
-
CQRS Documents by Greg Young
-> Greg Youngさんによる原典 -
関数型ドメインモデリング ドメイン駆動設計とF#でソフトウェアの複雑さに立ち向かおう
-> DDDについて、個人的にこの本はわかりやすくて好きですおすすめです -
CQRS & Event Sourcing モダンアーキテクチャにおける役割と実装
-> AWSでそう実装するのか、わかりやすいです。
Discussion