CQRS
データの読み取り (Query) と書き込み (Command) を別々のモデルで処理し、それぞれを独立して最適化するアーキテクチャパターン
CQRS とは
CQRS (Command Query Responsibility Segregation) は、データの書き込み操作 (Command) と読み取り操作 (Query) を異なるモデルで処理するアーキテクチャパターンである。Greg Young が 2010 年頃に体系化した。
従来の CRUD アプリケーションでは、同じデータモデル (同じテーブル、同じエンティティ) を読み書き両方に使う。しかし、読み取りと書き込みでは要件が大きく異なることが多い。書き込みはビジネスルールの検証と整合性の担保が重要であり、読み取りは複数テーブルの結合や集計が必要で、パフォーマンスが重視される。
CQRS はこの非対称性を認め、読み取り用と書き込み用のモデルを分離する。
動作の仕組み
クライアント ──Command──→ Write Model ──→ データベース (正規化)
│
(イベント/同期)
↓
クライアント ←──Query───← Read Model ←── 読み取り用ストア (非正規化)
Write Model はドメインロジックを含む豊かなモデルで、ビジネスルールの検証を行う。Read Model は表示に最適化された非正規化データで、JOIN なしで高速にクエリできる。
イベントソーシングとの関係
CQRS はイベントソーシングと組み合わせて語られることが多いが、両者は独立した概念だ。CQRS だけを採用し、Write Model と Read Model を同じデータベースの異なるテーブル (またはビュー) で実現することも可能だ。
イベントソーシングと組み合わせる場合、Write 側はイベントストアにイベントを追記し、Read 側はイベントを購読して読み取り用のビューを構築する。この構成では読み取りデータは結果整合性 (Eventual Consistency) となる。
実務での適用判断
CQRS が有効なケース:
- 読み取りと書き込みのスケーリング要件が大きく異なる (読み取りが書き込みの 100 倍以上)
- 複雑な集計クエリが必要で、正規化されたデータモデルではパフォーマンスが出ない
- 複数のマイクロサービスのデータを結合した読み取りビューが必要
CQRS が過剰なケース:
- 単純な CRUD アプリケーション
- 読み取りと書き込みの要件に大きな差がない
- 強い整合性が必須で、結果整合性を許容できない
よくある落とし穴
最大の落とし穴は、結果整合性の扱いだ。Write Model に書き込んだデータが Read Model に反映されるまでにタイムラグがある。ユーザーが注文を確定した直後に注文履歴を見ると、まだ反映されていない可能性がある。この問題には「書き込み直後は Write Model から直接読む」「楽観的 UI 更新で即座に画面に反映する」などの対策がある。
「マイクロサービスパターン」(Chris Richardson 著) 第 7 章で、API コンポジションと並ぶクエリパターンとして詳しく解説されている。DDD の文脈では「実践ドメイン駆動設計」(Vaughn Vernon 著) が参考になる。
CQRS の構造
| 側 | 責務 | データストア |
|---|---|---|
| Command | データの変更 | DynamoDB |
| Query | データの取得 | OpenSearch, GSI |
CQRS vs 従来の CRUD
| 観点 | CRUD | CQRS |
|---|---|---|
| モデル | 読み書き共通 | 読み書き分離 |
| 複雑さ | 低い | 高い |
| スケーラビリティ | 中 | 高い (読み書き独立) |
| 用途 | シンプルな CRUD | 読み書きの負荷が異なる |
// Command
await db.put({ TableName: "orders", Item: order });
// DynamoDB Streams → Lambda → 読み取り用ビューを更新
// Query (最適化されたビュー)
const results = await opensearch.search({ index: "orders", body: { query } });
CQRS の関連書籍も参考になる。