CQRS(Command Query Responsibility Segregation/コマンドクエリ責務分離)は、データを変更する処理(コマンド)と、データを読み取る処理(クエリ)に別々のモデルを使う設計パターンです。注文の登録やキャンセルは整合性を守るためのWrite Modelが受け持ち、一覧画面や集計は読み取り専用のRead Modelが受け持ちます。
この記事では、CQSとの違い、Read ModelとQuery Serviceの作り方、同じデータベースのまま始める段階から別ストアに分ける段階までの進め方を、実行して確かめたTypeScriptとSQLのコードで説明します。後半では、Read Modelを非同期で更新する構成で起きやすい「更新したのに一覧に出てこない」問題の扱い方と、2026年時点の実装ライブラリの事情もまとめました。なお、SQLの「クエリ」という言葉そのものを調べている場合はクエリとは?SQLとの違い・種類・実行の仕組みと高速化を実装目線で解説が答えになります。
まとめ
- CQRSは、更新用のWrite Modelと参照用のRead Modelを分ける設計です。Bertrand MeyerのCQS(メソッド単位の分離)を、モデルやサービスの単位に広げたものにあたります。
- 参照はQuery Serviceが担い、リポジトリや集約を通さずに画面用のDTOを返します。
- 最初から別データベースにする必要はありません。同じDBでコードを分ける、同じトランザクションで読み取り用テーブルも更新する、別ストアに分けてイベントで同期する、の順に段階を上げられます。
- 別ストアに分けると、更新が一覧に出るまで遅れる結果整合性が生まれます。アウトボックスで確実にイベントを届け、投影処理を冪等にし、画面側で古い表示への対策を決めておく必要があります。
- Martin Fowlerは「ほとんどのシステムではCQRSは危険な複雑さを足す」と書いています。CRUD画面中心の業務や、読み書きの負荷差が小さいシステムには向きません。
CQRSの定義:更新モデルと参照モデルの分離
多くの業務システムは、1つのモデルで登録・更新・参照をすべてこなします。Orderクラスが、在庫チェックや状態遷移のルールを持ちながら、一覧画面のための顧客名や合計金額の項目まで抱える形です。ドメインが複雑になるほど、このモデルは更新のための制約と、画面ごとに異なる表示項目の両方で膨らみます。
CQRSはここでモデルを2つに割ります。Write Modelはコマンドを受けて業務ルールを守ることだけを考え、Read Modelは画面が欲しい形でデータを返すことだけを考えます。Martin Fowlerはbliki記事CQRS(2011年7月14日)で、中心にある考えを「情報を更新するモデルと、情報を読むモデルに別のものを使えるということ」と説明しています。
CQSとの違い:メソッド単位とモデル単位
CQRSの元になったのは、Bertrand Meyerが著書『Object-Oriented Software Construction』で示したCQS(Command Query Separation)です。CQSは「メソッドは状態を変えるコマンドか、値を返すクエリのどちらかであるべきで、両方を兼ねてはならない」という1メソッド単位の規律です。FowlerもCommandQuerySeparationでこの原則を紹介しています。
CQRSはこの語彙を借りて、分ける単位をメソッドからモデル全体へ引き上げました。CQSを守ったクラスでも、更新と参照が同じOrderモデルを共有していればCQRSではありません。
| 項目 | CQS | CQRS |
|---|---|---|
| 分ける単位 | メソッド | モデル・サービス・データストア |
| 提唱 | Bertrand Meyer | Greg Young |
| 守らない例 | pop()が要素を返しつつ削除 | 1つのOrderで更新と一覧表示を兼ねる |
| 変更する対象 | メソッドの戻り値と副作用 | モデル・ハンドラー・(段階により)同期処理 |
提唱の経緯:Greg YoungとFowlerの警告
CQRSという名前はGreg Youngが広めました。Fowlerの記事も「Greg Youngから最初に聞いたパターン」と書いています。同じ記事には「この分離が役立つ状況もあるが、ほとんどのシステムではCQRSは危険な複雑さを足すことに注意してほしい」という警告と、「CQRSの利用にはとても慎重であるべきだ」という一文があります。CQRSの解説で引用されることが多いのは前半の定義ですが、提唱を紹介した側が最初から用途を限定していた点は押さえておくべきです。
Write ModelとRead Modelの役割分担
Write Model:業務の意図を表すコマンド名
Write Modelはコマンドを受け、検証と業務ルールを通したうえで状態を変えます。「在庫を超える注文は受け付けない」「出荷済みの注文はキャンセルできない」といった不変条件は、すべてこちらに置きます。DDDと組み合わせる場合、集約やエンティティを設計するのはこの更新側です。用語と設計の進め方はドメイン駆動設計(DDD)とは?意味・提唱者・基本用語から進め方と採用判断まで解説で扱っています。
コマンドの名前は、データ操作ではなく業務の操作にします。MicrosoftのAzure Architecture CenterのCQRSパターンは、ホテル予約で「ReservationStatusを予約済みに設定する」ではなく「ホテルの部屋を予約する(Book hotel room)」というコマンドにする例を挙げています。UpdateOrderStatusのような汎用コマンドを作ると、どの業務ルールを通すべきかがコマンドから読み取れなくなり、Write Modelを分けた意味が薄れます。
Read Model:画面ごとの非正規化ビュー
Read Modelは、画面やAPIが必要とする形に整えたデータです。注文一覧なら「注文番号・顧客名・合計金額・状態」を1行に並べたテーブルやマテリアライズドビューを用意し、表示のたびに結合や集計をしないで済むようにします(PostgreSQLの通常のビューは結果を保持せず参照のたびにクエリを実行するので、この目的には使えません)。更新側が正規化されていても、参照側は非正規化して構いません。同じMicrosoftのページは、読み取り用ストアにマテリアライズドビューを置いて複雑な結合を避ける構成を挙げています。
Read Modelは業務ルールを持ちません。件数や合計金額のような表示用の集計はRead Model側で計算して構いませんが、「この注文を受け付けてよいか」のように更新の可否を決める検証がRead Model側に入り始めたら、それは更新側に戻すべきロジックです。
Query Serviceによる参照系の実装
Query Serviceは、Read Modelから画面用のDTOを返す参照専用の入口です。更新側ではリポジトリで集約を1件ずつ取り出しますが、一覧画面で注文ごとにリポジトリを呼び、その都度顧客や明細を個別に読み込むと、注文の数だけ問い合わせが走るN+1問題が起きます。Query Serviceはドメインモデルを経由せず、画面に必要な列だけを結合したSQLで取り出してDTOに詰めます。DTOとエンティティの違いはDTOとは?意味・Entityとの違いとNestJSのValidationPipe設定【v12実測】で整理しています。
リポジトリとQuery Serviceの使い分け
判断の基準は「その結果で状態を変えるかどうか」です。取り出した集約を変更して保存するならリポジトリ、画面に表示して終わりならQuery Serviceを使います。リポジトリにfindForOrderListScreen()のような画面専用メソッドが増えてきたら、参照の都合が更新側に入り込んでいるサインです。
| 項目 | リポジトリ | Query Service |
|---|---|---|
| 属する側 | Write Model | Read Model |
| 返すもの | 集約(エンティティ) | DTO |
| 取得の単位 | 集約1件ずつ | 画面1枚分を1回で |
| 結合・集計の目的 | 集約を復元するため | 画面の表示項目を揃えるため |
| 名前の例 | OrderRepository.findById | OrderListQuery.forCustomer |
CQRSの実装段階:同じDBから別ストアへ
CQRSは「データベースを2つに分けること」と説明されがちですが、MicrosoftのガイドもCQRSの基本形として、1つのデータベースを共有しながら読み取りと書き込みのロジックだけを分ける構成を挙げています。段階を上げるほど得られるものとコストが両方増えるので、必要な段階で止めるのが現実的です。
| 段階 | 分けるもの | 読み取りの鮮度 | 増える作業 |
|---|---|---|---|
| 1 コードだけ分ける | Command側とQuery側のクラス | 即時 | ほぼ無し |
| 2 読み取り用テーブル | 同じDB内のテーブル | 即時(同一トランザクション) | 更新時の書き込みが増える |
| 3 別ストア | データベースそのもの | 遅れる(結果整合性) | イベント配送・投影・再構築 |
段階1:同じDBのままのコード分離
テーブルは今のまま、コードの入口だけを分けます。更新はPlaceOrderHandlerのようなコマンドハンドラーからリポジトリ経由で行い、参照はQuery ServiceがSQLを直接書いてDTOを返します。データを二重に持たないのでRead Modelの同期遅れは起きず、既存システムでも機能単位で始められます(同時更新の排他制御など、もともとの並行処理の設計は変わらず必要です)。CQRSで得たいものが「参照の都合でドメインモデルが膨らむのを止めること」だけなら、この段階で十分です。
段階2:同一トランザクションでの読み取り用テーブル更新
一覧画面の集計が重くなったら、読み取り専用のテーブルを足し、注文を書くのと同じトランザクションで更新します。次のSQLは顧客ごとの注文件数と合計金額を持つcustomer_order_summaryを、注文の登録と同時に更新する例です。PostgreSQL 18.6で実行し、2件目の注文で件数と金額が加算されることを確認しました。
CREATE TABLE orders (order_id text PRIMARY KEY, customer_id text NOT NULL, total integer NOT NULL);
CREATE TABLE customer_order_summary (customer_id text PRIMARY KEY, order_count integer NOT NULL, total_amount integer NOT NULL);
BEGIN;
INSERT INTO orders VALUES ('o-1', 'c-1', 2400);
INSERT INTO customer_order_summary VALUES ('c-1', 1, 2400)
ON CONFLICT (customer_id) DO UPDATE
SET order_count = customer_order_summary.order_count + 1,
total_amount = customer_order_summary.total_amount + EXCLUDED.total_amount;
COMMIT;
BEGIN;
INSERT INTO orders VALUES ('o-2', 'c-1', 800);
INSERT INTO customer_order_summary VALUES ('c-1', 1, 800)
ON CONFLICT (customer_id) DO UPDATE
SET order_count = customer_order_summary.order_count + 1,
total_amount = customer_order_summary.total_amount + EXCLUDED.total_amount;
COMMIT;
SELECT customer_id, order_count, total_amount FROM customer_order_summary;
最後のSELECTの結果は次のとおりです。
customer_id | order_count | total_amount
-------------+-------------+--------------
c-1 | 2 | 3200
(1 row)
同じトランザクションなので、読み取り側が遅れることはありません。代わりに、注文を書くたびに集計テーブルの行ロックを取るため、同じ顧客への同時更新が多い業務では待ちが発生します。集計を定期的に作り直すだけで足りるなら、マテリアライズドビュー(マテビュー)とは?Oracleの作成・自動リフレッシュとPostgreSQL・SQL Serverとの違いを実測で解説で扱うマテリアライズドビューでも同じ目的を果たせます。
段階3:別ストアへの分離とイベントによる同期
読み取りの負荷が書き込みより桁違いに大きい、検索エンジンやドキュメントDBで読みたい、といった理由があるときに、Read Modelを別のデータストアに移します。更新側はイベントを発行し、投影処理(プロジェクション)がそれを受けてRead Modelを書き換えます。AWSの規範ガイダンスのCQRSパターンでは、書き込み側をDynamoDB、読み取り側をAuroraにし、DynamoDB StreamsからLambda関数経由でAuroraのテーブルを更新する構成が例として示されています。
ここで問題になるのが、データベースへの書き込みとメッセージブローカーへの発行を1つのトランザクションにできないことです。Microsoftのガイドは、状態変更とイベントをアトミックに保存するトランザクショナルアウトボックスと、重複配信に耐える冪等なコンシューマーを組み合わせるよう勧めています。アウトボックスの実装方式はOutboxパターンとは?DB更新とイベント発行を一致させる実装方式を解説で詳しく扱っています。
TypeScriptで動かすCQRSの最小構成
段階3の流れを、外部ライブラリを使わずに確かめられる形にしたのが次のコードです。コマンドハンドラーが注文とアウトボックスを書き、投影処理が顧客別サマリを作り、Query ServiceがDTOを返します。投影処理には同じイベントを2回流し、二重計上されないことを確かめています。Node.js v26.5.0でnode cqrs-demo.tsとして実行しました(型注釈はNode.jsの型除去で動くので、tscでのビルドは不要です)。
type OrderItem = { sku: string; qty: number; price: number };
type PlaceOrder = { orderId: string; customerId: string; items: OrderItem[] };
type OrderPlaced = { eventId: string; orderId: string; customerId: string; total: number };
// 書き込み側:注文とアウトボックス(ここではMapと配列で模擬。実DBでは同じトランザクションで書く)
const orders = new Map<string, { customerId: string; items: OrderItem[] }>();
const outbox: OrderPlaced[] = [];
function handlePlaceOrder(cmd: PlaceOrder): void {
if (cmd.items.length === 0) throw new Error("明細が空の注文は受け付けません");
if (orders.has(cmd.orderId)) throw new Error("同じ注文IDは登録済みです");
const total = cmd.items.reduce((sum, i) => sum + i.qty * i.price, 0);
orders.set(cmd.orderId, { customerId: cmd.customerId, items: cmd.items });
outbox.push({ eventId: crypto.randomUUID(), orderId: cmd.orderId, customerId: cmd.customerId, total });
}
// 読み取り側:画面がそのまま使える顧客別サマリ(非正規化)
const summary = new Map<string, { orderCount: number; totalAmount: number }>();
const processed = new Set<string>();
function project(e: OrderPlaced): void {
// 同じイベントが2回届いても二重計上しない(冪等)
if (processed.has(e.eventId)) return;
const cur = summary.get(e.customerId) ?? { orderCount: 0, totalAmount: 0 };
summary.set(e.customerId, { orderCount: cur.orderCount + 1, totalAmount: cur.totalAmount + e.total });
processed.add(e.eventId);
}
// Query Service:ドメインモデルを通さずDTOを返す
function getCustomerSummary(customerId: string) {
return summary.get(customerId) ?? { orderCount: 0, totalAmount: 0 };
}
handlePlaceOrder({ orderId: "o-1", customerId: "c-1", items: [{ sku: "A", qty: 2, price: 1200 }] });
handlePlaceOrder({ orderId: "o-2", customerId: "c-1", items: [{ sku: "B", qty: 1, price: 800 }] });
console.log("投影前", getCustomerSummary("c-1"));
for (const e of outbox) project(e);
// 再送で同じイベントがもう一度届いた想定
for (const e of outbox) project(e);
console.log("投影後", getCustomerSummary("c-1"));
try {
handlePlaceOrder({ orderId: "o-3", customerId: "c-1", items: [] });
} catch (err) {
console.log("拒否", (err as Error).message);
}
実行結果です。
投影前 { orderCount: 0, totalAmount: 0 }
投影後 { orderCount: 2, totalAmount: 3200 }
拒否 明細が空の注文は受け付けません
1行目が結果整合性そのものです。注文は2件とも書き込み側に入っているのに、投影処理が走る前のQuery Serviceは0件を返します。2行目は、同じイベントを2回流してもeventIdで処理済みを判定しているので件数が2のままであることを示しています。実際のシステムでは、ordersとoutboxへの書き込みを1つのDBトランザクションにし、処理済みイベントIDもRead Modelと同じトランザクションで記録します。
結果整合性で古い表示が出るときの対処
段階3の構成では、登録APIが応答した時点で投影処理が終わっている保証がありません。登録直後に一覧画面へ戻り、その問い合わせが投影より先にRead Modelへ届くと、「登録したのに一覧に出てこない」状態になります。対処は次の4つから、画面ごとに選びます。
- 書いた本人には書き込み側の結果を返す:登録APIのレスポンスに作成した注文の内容を含め、画面はそれを一覧の先頭に差し込みます。投影の速さに依存しないので、最初に検討する方法です。
- 投影済みのバージョンを待つ:コマンドの応答で集約のバージョン番号を返し、Query Serviceは「Read Modelがそのバージョンに追いついたか」を見て、追いついていなければ短時間待ちます。待ち時間の上限を決めておかないと、投影処理が止まったときに画面ごと固まります。
- 反映中であることを表示する:「反映まで数秒かかることがあります」と明示します。管理画面や集計画面のように、即時性がそれほど求められない画面に向きます。
- 判断に使う値はRead Modelから取らない:在庫の引き当てや残高の確認など、古い値で判断すると事故になる処理は、Write Model側で検証します。Read Modelはあくまで表示用です。
どの画面がどれだけの遅れを許容できるかは業務側と決める事項で、実装を始める前に一覧にしておくべきです。強整合性と結果整合性の違いそのものは結果整合性とは?強整合性との違いとレプリカ遅延の設計を実装者目線で解説で扱っています。
イベントソーシングとCQRSの関係
イベントソーシングは、現在の状態ではなく「状態を変えた出来事の列」を保存し、それを再生して状態を求める手法です。CQRSとよくセットで語られますが、別の概念で、CQRSにイベントソーシングは必須ではありません。上の段階1〜3はいずれも、現在の状態をテーブルに保存する普通の永続化で成り立っています。
組み合わせると、イベントストアがWrite Modelの唯一の正となり、Read Modelはイベントを再生すればいつでも作り直せます。新しい画面用のRead Modelを後から追加しやすいのが利点で、代わりにイベントのスキーマ変更やスナップショットの運用が増えます。Microsoftのガイドも、イベントソーシングと組み合わせると複雑さが大きくなる点を考慮事項に挙げています。イベントストアの設計はイベントソーシングとは?状態再生とイベントストア設計を実装視点で解説【2026年版】、Javaでの実装基盤はAxon Frameworkとは?Axon 5の変更点とSpring Boot導入・4系EOL後の選び方【2026年9月・5.3.2】が詳しいです。
リードレプリカによる読み書き分離との違い
「参照を別のDBに逃がす」という点だけを見ると、CQRSはリードレプリカと似ています。違いはモデルを分けるかどうかです。リードレプリカは書き込み用DBと同じスキーマのコピーで、同じテーブルに同じSQLを投げる先を変えるだけです。CQRSで必須なのは読み取り用モデルを更新用モデルと分けることで、Microsoftのガイドは読み取りストアを書き込みストアの読み取り専用レプリカにする構成も認めています。つまり両者は対立する選択肢ではなく、レプリカの上にQuery Serviceを置く併用もできます。
| 項目 | リードレプリカ単体 | CQRSのRead Model |
|---|---|---|
| 読み取りのモデル | 書き込み側と同じ | 画面ごとに設計してよい |
| 同期の仕組み | DBのレプリケーション | 同期なし・同一トランザクション・イベント投影・レプリカのいずれか |
| アプリの変更 | 接続先の振り分けだけ | 参照用のモデルとQuery Serviceを実装 |
| 解決する問題 | 読み取りの負荷 | モデルの肥大化と読み取りの負荷 |
参照の負荷だけが問題なら、先にリードレプリカを試すべきです。CQRSに進むのは、レプリカに逃がしても重い結合や集計が残る場合か、参照の都合でドメインモデルが崩れている場合です。レプリカの作り方はリードレプリカとは?RDS・Auroraでの作成手順と読み書き分離の実装を実装者目線で解説【2026年版】を参照してください。
CQRSを採用すべき場面・避けるべき場面
Microsoftのガイドが挙げる適した場面は、複数ユーザーが同じデータを同時に変更する共同作業、手順に沿って操作するタスクベースのUI、読み取りが書き込みを大きく上回り別々にチューニングしたいシステム、書き込み側と読み取り側を別チームで開発する体制などです。逆に、ドメインやビジネスルールが単純な場合と、単純なCRUDの画面とデータアクセスで足りる場合は適さないとしています。
次の条件に当てはまるシステムでは、段階3のCQRSを採用すべきではありません。
- 画面の大半がテーブルの1行をそのまま編集するCRUDで、業務ルールが入力チェック程度しかない
- 登録直後に一覧や集計を見て次の操作を判断する業務が多く、数秒の遅れも許されない
- イベント配送や投影処理の障害を監視し、Read Modelを再構築する運用を担う人がいない
よくある失敗は、システム全体に一律でCQRSを入れることです。Fowlerも、CQRSはシステム全体ではなく特定の部分(DDDでいう境界づけられたコンテキスト)に使うべきものだと書いています。注文や在庫のように読み書きの要求が食い違う部分だけに適用し、マスタ管理のような単純な画面は従来のCRUDで残します。Read Modelのテーブル設計や既存DBからの段階的な切り出しに外部の手を借りたい場合は、データベース設計・移行支援で相談を受けています。
主な実装ライブラリの現況(2026年10月)
CQRSは考え方なので、ライブラリが無くても実装できます。上のTypeScriptの例もライブラリを使っていません。使う場合は、2025年以降にライセンスや版の前提が変わったものがあるので、古い解説記事の手順をそのまま使わないよう注意が必要です。版は2026年10月28日時点のNuGet・npm・Maven Centralの登録情報です。
| ライブラリ | 言語 | 最新版 | ライセンス |
|---|---|---|---|
| MediatR | C#(.NET) | 14.2.0 | 13.0.0から商用版あり |
| @nestjs/cqrs | TypeScript(NestJS) | 12.1.2 | MIT |
| Axon Framework | Java | 5.4.1 | Apache 2.0 |
MediatRは12.5.0(2025年4月1日)まではApache-2.0でしたが、2025年7月2日公開の13.0.0から、Reciprocal Public License 1.5(RPL-1.5)と、作者のJimmy Bogardが設立したLucky Penny Softwareの商用ライセンスのデュアルライセンスに変わりました(商用版開始の告知)。RPL-1.5はソース公開の義務を伴い、規模などの条件を満たす組織向けの無償Community版もあります。業務で新しく使うなら、どのライセンスで使うかを先に決めてください。.NETでMediatRを前提にした構成はVertical Slice Architecture(バーティカルスライス)とは?機能単位の設計とC#・Go実装例でも扱っています。
@nestjs/cqrsはCommandBus・QueryBus・EventBusを提供し、コマンドとクエリのハンドラーをデコレーターで登録します。Axon Framework本体はApache 2.0のままですが、5系では一部のモジュールが商用の拡張(Axoniq Framework)側に移り、groupIdも変わりました。4系は2026年6月30日に通常のサポートが終わり、以降は有償の長期サポート(LTS)が用意されています。
よくある質問
CQRSの「クエリ」とSQLのクエリは同じ意味ですか?
同じではありません。CQRSのクエリは「状態を変えずに値を返す処理」という責務の分類で、SQLのSELECT文に限りません。SQLやデータベースへの問い合わせとしてのクエリはクエリとは?SQLとの違い・種類・実行の仕組みと高速化を実装目線で解説で扱っています。
CQRSとCQSはどう違いますか?
CQSはBertrand Meyerが示した、1つのメソッドをコマンドかクエリのどちらかにする規律です。CQRSはその分け方をモデルやサービスの単位に広げたもので、更新用と参照用でモデルそのものを分けます。
CQRSにイベントソーシングは必須ですか?
必須ではありません。現在の状態をテーブルに保存する通常の永続化のまま、コードの分離や読み取り用テーブルの追加でCQRSを始められます。イベントソーシングは、Read Modelを後から作り直したい、変更履歴そのものに価値がある、といった理由があるときに足します。
CQRSは何と読みますか?
アルファベットのまま「シー・キュー・アール・エス」と読むのが一般的です。日本語では「コマンドクエリ責務分離」または「コマンドクエリ責任分離」と訳されます。
小規模なシステムにもCQRSを使うべきですか?
別ストアに分ける段階まで進める必要はほとんどありません。小規模なら、参照用のQuery Serviceを更新用のリポジトリと分ける段階1だけで、モデルの肥大化はかなり防げます。読み取りの負荷が問題になってから段階を上げれば間に合います。