ひとことで言うと#
「データを変更する操作(コマンド)」と「データを取得する操作(クエリ)」を明確に分けるという設計原則。1つのメソッドやAPIが「変更しながら結果を返す」のをやめることで、コードの予測可能性とテスト容易性が向上する。
押さえておきたい用語#
- Command(コマンド)
- 状態を変更するが値を返さない操作。「注文を確定する」「在庫を減らす」など副作用を持つ処理を指す。
- Query(クエリ)
- 状態を変更せずに値を返す操作。「注文一覧を取得する」「在庫数を確認する」など副作用のない問い合わせを指す。
- CQRS(Command Query Responsibility Segregation)
- CQSをアーキテクチャレベルに拡張し、書き込みモデルと読み取りモデルを完全に分離するパターン。
- Side Effect(副作用)
- 関数の実行によってシステムの状態が変わること。データベースへの書き込みやファイルの更新が典型例。
CQSの全体像#
こんな悩みに効く#
- 「このメソッドを呼ぶとデータが変わるのか変わらないのか」がコードを読まないとわからない
- 読み取り処理と書き込み処理のパフォーマンス要件が大きく異なる
- テスト時に副作用があるメソッドの検証が複雑になっている
基本の使い方#
すべてのパブリックメソッド/APIを「Command」「Query」「混在」に分類する。混在しているものがリファクタリング対象。
createOrder() → Order→ 混在(作成しつつ結果を返す)getOrders() → List<Order>→ Query(取得のみ)deleteOrder(id)→ Command(削除のみ)
createOrder() → Order を createOrder() (void) と getOrder(id) → Order に分ける。コマンド実行後にクライアントが結果を知りたければ、IDを返して別途Queryする。具体例#
月間PV500万のECサイト。商品一覧APIのレスポンスが平均800msで、ユーザーの離脱率が問題になっていた。原因は商品テーブルが書き込み最適化(正規化)されており、一覧表示のために5テーブルJOINが必要だったこと。
CQRSを導入し、Read用の非正規化ビューテーブルを作成。書き込み時にイベントで非正規化テーブルを更新する仕組みにした。
| 指標 | Before | After |
|---|---|---|
| 一覧API応答時間 | 800ms | 45ms |
| JOIN数 | 5テーブル | 0(単一テーブル) |
| 書き込みへの影響 | なし | なし |
読み取り性能が 約18倍 に改善し、離脱率は12%低下した。
証券取引システム。「注文を出しながら結果を返す」APIが監査上の問題になっていた。1つのAPIコールで状態変更と結果取得が混在しており、監査ログのタイミングが曖昧だった。
CQSに沿ってAPIを再設計。
POST /orders→ 注文を作成(Commandで、注文IDのみ返す)GET /orders/{id}→ 注文の状態を取得(Query)
コマンド実行時に厳密な監査ログを記録し、クエリは何度呼んでも副作用がないことが保証される。金融庁の検査でも「操作と照会が明確に分離されており、ログの信頼性が高い」と評価された。
工場向けIoTプラットフォーム。1日2,000万件のセンサーデータの書き込みと、ダッシュボードの分析クエリが同じDBを共有しており、分析クエリの実行中に書き込みがタイムアウトする障害が月3〜4回発生していた。
CQRSで書き込みと読み取りを完全に分離。
- Write側: TimescaleDBでセンサーデータを高速書き込み
- Read側: ClickHouseに15秒遅延で同期。ダッシュボードはここから読む
書き込みのタイムアウトはゼロに、ダッシュボードの応答速度も平均 12秒 → 0.8秒 に改善。15秒の遅延はリアルタイム監視には十分許容範囲だった。
やりがちな失敗パターン#
- すべてのAPIにCQRSを適用する — CQRSはアーキテクチャの複雑さが増す。読み書きの性能要件が大きく異なる箇所だけに適用する。CQSレベル(メソッドの分離)なら全体に適用してもコストは低い
- コマンドの結果を完全に返さない設計にこだわる — 実用上、コマンドの結果としてIDやステータスを返すのは許容される。「副作用のないQueryが状態を変更しない」という方向を守るのが本質
- Read/Write間の同期を考慮しない — CQRSでは結果整合性が前提になる。「書き込み直後に読んだら反映されていない」ケースのUI設計を先に考える
- 既存コードを一度に全部リファクタリングしようとする — 新規APIからCQSに沿って書き始め、既存APIは段階的に移行する
まとめ#
CQSは 「変更する操作」 と「取得する操作」を明確に分ける設計原則。メソッドレベルで適用するだけでもコードの予測可能性が上がり、テストも書きやすくなる。読み書きの性能要件が大きく異なる場合はCQRSに拡張し、データモデルごと分離する。まずは新規コードからCQSに沿って書き始め、段階的に既存コードも移行していくのが現実的な進め方になる。