コマンドクエリ分離原則(CQS)

英語名 Command Query Separation
読み方 コマンド クエリ セパレーション
難易度
所要時間 30分〜1時間
提唱者 Bertrand Meyer
目次

ひとことで言うと
#

「データを変更する操作(コマンド)」と「データを取得する操作(クエリ)」を明確に分けるという設計原則。1つのメソッドやAPIが「変更しながら結果を返す」のをやめることで、コードの予測可能性とテスト容易性が向上する。

押さえておきたい用語
#

押さえておきたい用語
Command(コマンド)
状態を変更するが値を返さない操作。「注文を確定する」「在庫を減らす」など副作用を持つ処理を指す。
Query(クエリ)
状態を変更せずに値を返す操作。「注文一覧を取得する」「在庫数を確認する」など副作用のない問い合わせを指す。
CQRS(Command Query Responsibility Segregation)
CQSをアーキテクチャレベルに拡張し、書き込みモデルと読み取りモデルを完全に分離するパターン
Side Effect(副作用)
関数の実行によってシステムの状態が変わること。データベースへの書き込みやファイルの更新が典型例。

CQSの全体像
#

CQS:コマンドとクエリの分離
Command(コマンド)状態を変更する値を返さない(void)例: placeOrder(), cancelOrder()Query(クエリ)状態を変更しない値を返す(データ)例: getOrders(), getStock()分離Write Model正規化されたDBビジネスルールの検証Read Model表示に最適化されたビューキャッシュ・非正規化OK同期(イベント / CDC)
CQS/CQRS導入の進め方フロー
1
読み書きの分析
APIやメソッドを「変更する操作」と「取得する操作」に分類
2
インターフェース分離
Command用とQuery用にインターフェースを分ける
3
モデル分離(CQRS)
必要に応じてWriteとReadのデータモデルも分離
最適化
Read側をキャッシュ・非正規化で高速化

こんな悩みに効く
#

  • 「このメソッドを呼ぶとデータが変わるのか変わらないのか」がコードを読まないとわからない
  • 読み取り処理と書き込み処理のパフォーマンス要件が大きく異なる
  • テスト時に副作用があるメソッドの検証が複雑になっている

基本の使い方
#

既存のAPIやメソッドを分類する

すべてのパブリックメソッド/APIを「Command」「Query」「混在」に分類する。混在しているものがリファクタリング対象。

  • createOrder() → Order → 混在(作成しつつ結果を返す)
  • getOrders() → List<Order> → Query(取得のみ)
  • deleteOrder(id) → Command(削除のみ)
混在しているものを分離する
createOrder() → OrdercreateOrder() (void) と getOrder(id) → Order に分ける。コマンド実行後にクライアントが結果を知りたければ、IDを返して別途Queryする。
必要に応じてCQRSに拡張する
読み取りと書き込みのスケーリング要件が大きく異なる場合は、データモデルごと分離する(CQRS)。Write側は正規化DBでビジネスルールを守り、Read側は非正規化ビューで高速に返す。

具体例
#

例1:ECサイトの商品一覧APIを高速化する

月間PV500万のECサイト。商品一覧APIのレスポンスが平均800msで、ユーザーの離脱率が問題になっていた。原因は商品テーブルが書き込み最適化(正規化)されており、一覧表示のために5テーブルJOINが必要だったこと。

CQRSを導入し、Read用の非正規化ビューテーブルを作成。書き込み時にイベントで非正規化テーブルを更新する仕組みにした。

指標BeforeAfter
一覧API応答時間800ms45ms
JOIN数5テーブル0(単一テーブル)
書き込みへの影響なしなし

読み取り性能が 約18倍 に改善し、離脱率は12%低下した。

例2:金融システムの監査ログ問題をCQSで解消する

証券取引システム。「注文を出しながら結果を返す」APIが監査上の問題になっていた。1つのAPIコールで状態変更と結果取得が混在しており、監査ログのタイミングが曖昧だった。

CQSに沿ってAPIを再設計。

  • POST /orders → 注文を作成(Commandで、注文IDのみ返す)
  • GET /orders/{id} → 注文の状態を取得(Query)

コマンド実行時に厳密な監査ログを記録し、クエリは何度呼んでも副作用がないことが保証される。金融庁の検査でも「操作と照会が明確に分離されており、ログの信頼性が高い」と評価された。

例3:IoTプラットフォームがセンサーデータの書き込みと分析を分離する

工場向けIoTプラットフォーム。1日2,000万件のセンサーデータの書き込みと、ダッシュボードの分析クエリが同じDBを共有しており、分析クエリの実行中に書き込みがタイムアウトする障害が月3〜4回発生していた。

CQRSで書き込みと読み取りを完全に分離。

  • Write側: TimescaleDBでセンサーデータを高速書き込み
  • Read側: ClickHouseに15秒遅延で同期。ダッシュボードはここから読む

書き込みのタイムアウトはゼロに、ダッシュボードの応答速度も平均 12秒 → 0.8秒 に改善。15秒の遅延はリアルタイム監視には十分許容範囲だった。

やりがちな失敗パターン
#

  1. すべてのAPIにCQRSを適用する — CQRSはアーキテクチャの複雑さが増す。読み書きの性能要件が大きく異なる箇所だけに適用する。CQSレベル(メソッドの分離)なら全体に適用してもコストは低い
  2. コマンドの結果を完全に返さない設計にこだわる — 実用上、コマンドの結果としてIDやステータスを返すのは許容される。「副作用のないQueryが状態を変更しない」という方向を守るのが本質
  3. Read/Write間の同期を考慮しない — CQRSでは結果整合性が前提になる。「書き込み直後に読んだら反映されていない」ケースのUI設計を先に考える
  4. 既存コードを一度に全部リファクタリングしようとする — 新規APIからCQSに沿って書き始め、既存APIは段階的に移行する

まとめ
#

CQSは 「変更する操作」 と「取得する操作」を明確に分ける設計原則。メソッドレベルで適用するだけでもコードの予測可能性が上がり、テストも書きやすくなる。読み書きの性能要件が大きく異なる場合はCQRSに拡張し、データモデルごと分離する。まずは新規コードからCQSに沿って書き始め、段階的に既存コードも移行していくのが現実的な進め方になる。