📨

CQRS + ES について独学してわかってきたことをモノレポにまとめてみた

に公開

どうも皆さんこんにちは.
鞍馬ぽめると申します, よろしくお願いいたします.

はじめに

CQRS + ES についてはだいぶ長いこと独学を続けているのですが, 今回, 以前奥さんと結婚式の際に一緒に開発した結婚式アプリ( 429-wedding )の CQRS + ES へのリライトに挑戦し始めてみました.

https://x.com/nullpommel/status/1913604322279231705

リライト自体はまだまだ始めたばかりですが, ひとまずライトモデルの更新をリードモデルアップデータからリードモデルに流す一連の構成をローカル環境で動かすことができたので, サンプルとして公開してみました.

https://github.com/kuramapommel/cqrs-es-example

本記事ではサンプルリポジトリ内で使用している技術や技法についてをまとめてみたいと思います. [1]

もしも間違いがあればご指摘いただけると嬉しいです.
独学で進めてきたことから正しい理解ができていない可能性もあるため, ご指摘いただきより学びを深めたいと思っています m_ _m

また, CQRS + ES についての説明は, 過去にアクターモデルの記事を書いた際に触れているのでそちらに委ねたいと思います. 良ければそちらも見てやってください.

https://zenn.dev/kuramapommel/articles/self-study_actor-model-using-akka#cqrs-%2B-es

リポジトリ構成と CQRS のマッピング

大まかに下記のようなプロジェクト群を抱えたモノレポ構成を取っています.

.
└── cqrs-es-example/
    ├── mobile-app/ # ios/android モバイルフロントエンド
    ├── read-api-server/ # read model
    ├── read-model-updater/ # read model updater
    ├── web-frontend/ # web フロントエンド
    └── write-api-server/ # write model

CQRS はコマンド側とクエリ側の責務を明確に分離し, コマンド側の更新に基づいてクエリ側を更新することでアプリケーションを構築します.
それぞれは以下のようにマッピングできます.

CQRS の概念 呼称 対象ディレクトリ
コマンド側 Write Model cqrs-es-example/write-api-server/
クエリ側 Read Model cqrs-es-example/read-api-server/
コマンド側の更新に基づいてクエリ側を更新 Read Model Updater cqrs-es-example/read-model-updater/

また, フロントエンドは WEB 用( cqrs-es-example/web-frontend/ )とモバイル用( cqrs-es-example/mobile-app/ )を用意しました.
それぞれのプロジェクトで使用している技術や技法を紹介してみます.

Write Model: cqrs-es-example/write-api-server/

Akka と Write Model の相性

Akka と Write Model がなぜ相性がいいのかについては, こちらも先程の過去記事( アクターモデルや akka について独学してわかってきたことをまとめてみる )にまとめてありますので, そちらに説明を委ねさせてください.

結果整合性のために Saga パターンの導入

read-api-server ではユースケースの整合性に Saga パターン を採用しています.
Saga パターンの簡単な説明はこちらの書籍から一文引用させていただきます.

クリス・リチャードソン, 長尾高弘(訳), 橂澤広亨(監修).
マイクロサービスパターン 実践的システムデザインのためのコード解説.
株式会社インプレス.

サーガ(Saga)は、マイクロサービスアーキテクチャで分散トランザクションを使わずにデータ整合性を維持するためのメカニズムです。複数のサービスのデータを更新しなければならないひとつひとつのシステムコマンドに対してサーバを定義します。サーガは、ローカルトランザクションのシーケンスです。個々のローカルトランザクションは、先ほども触れたなじみの ACID トランザクションフレームワーク/ライブラリを使ってサービス内のデータを更新します。

2022, 125p

Saga パターンには コレオグラフィベースの Sagaオーケストレーションベースの Saga がありますが, 今回はオーケストレーションベースの Saga を採用しています.

https://github.com/kuramapommel/cqrs-es-example/blob/main/write-api-server/src/main/scala/com/kuramapommel/cqrs_es_example/adapter/usecase/reservation/ReservationUseCaseImpl.scala

上記では Akka Streams を用いて下記のユースケースを定義しています.

  1. 予約 ID の作成
  2. 予約作成
  3. テーブルの確保
    1. テーブルの確保に失敗した場合, 作成した予約の取り消し

ReservationActorTableActor も Akka Cluster 上の "どこか" に配置されており, それぞれのトランザクションはアクターごとで管理されるため, いづれかのアクターの処理が失敗した場合はそれ以外のアクターの処理も何らかの形で補償する必要があります.
今回は予約テーブルの確保の処理が失敗した場合, 3回まではリトライを行い, そのうえで失敗する場合は予約自体を取り消すように補償しています.

この一連のユースケースを UseCase クラスが指揮しています. このような型をオーケストレーションベースの Saga パターンと呼びます.
オーケストレーションベースの Saga は一連の処理の流れがわかりやすい(定義もし易く理解もし易い)点にメリットがありますが, 一方で密な結合になってしまうというデメリットがあります.

この代わりとして EventHub を用いるという方法もあります.
ざっくりと下記のような処理の流れになります.

  1. ReservationActor はイベントを EventHubActor に通知
  2. EventHubActor は通知されたイベントを適したアクターに割り振るだけ
    1. "予約確定イベント" を受信したら TableActor にテーブル予約コマンドを発信
  3. TableActor はイベントを EventHubActor に通知
    1. "テーブル確保イベント" を受信したら成功を大元に返信
    2. "テーブル確保失敗イベント" を受信したら ReservationActor に予約取消コマンドを発信

このようにすることでそれぞれのアクターは完全に独立して仕事し, 自身が受信したメッセージを捌き続けるだけで他のアクターの状況を気にする必要がなくなります.
このような型をコレオグラフィベースの Saga パターンと呼びます. コレオグラフィベースの場合はオーケストレーションベースと比較してかなり疎結合になるメリットがありますが, 単純に難易度が高くなります.

コレオグラフィベースの Saga は概念を理解できただけでまだ試すことができておらず, 今回は "簡単" を取るためにオーケストレーションベースの Saga を採用しましたが, いづれはコレオグラフィベースの Saga も試してみたいと思います.

Read Model: cqrs-es-example/read-api-server/

  • 主な使用言語: TypeScript
  • 主なフレームワーク, ライブラリ, ツールキット
  • データベース: MySQL
  • 主なテストツール, テスティングライブラリ
  • ミドルウェア
    • ランタイム: Bun
    • フォーマッタ, リンタ: Biome

Read Model は純粋なクエリ

今回 Read Model は雑に書いてます(すみません).
リードモデルは特にフロントエンドのためのものという認識なので, フロントエンドエンジニアが馴染み深い技術を選ぶと良いかもしれません.
今回もフロントエンドに寄り添ったデータ構造で非正規化の状態のままデータを保持する予定ですので, ほぼロジックのない純粋なクエリのみになる想定です.

Read Model は正直強いこだわりをもって技術選定をしたわけではなく, 前々から触ってみたいと思っていた技術を選んでみました.
ちなみに, 小規模で制約も厳しくないプロダクトであれば, Next.js の Server Componet を使って直接データベースからデータを取得してしまうのもありだと思います.

今後を見越して MySQL を選択

今のところ本番環境では Amazon Aurora を使用するつもりなのですが, 読み書き共にスケーラブルな TiDB という NewSQL データベースがおすすめという話をどこかで耳にした(現在全くの無知です)ので, 乗り換える可能性を考慮してどちらにも互換性のある MySQL を選択しました.

この辺は今後のアップデートをお待ち下さい.

Read Model Updater (RMU): cqrs-es-example/read-model-updater/

"速さ" を求めて Rust を採用

Read Model Updater は CQRS を選択するうえで肝となる結果整合性を担保する重要なモジュールです. ライトモデルとリードモデルの同期のために大量のデータを高速に捌くことが求められるため, 以前から爆速で実行できると聞いていた Rust を採用してみました.

https://xtech.nikkei.com/atcl/nxt/column/18/02872/062500002/

Rust の実行速度は、おおむね C 言語や C++に匹敵することが知られています。つまり、言語として最速の部類です。

Rust は今回初めて触りましたが, 以前 C++ で開発していたときに感じていた "マニュアル車を運転するような楽しさ" を感じられる言語です.

なお, RMU はリードモデル同様フロントエンジニアに馴染みのある言語を選択するのも良いと思います. リードモデル側のテーブルレイアウトがフロントエンドに寄り添っていることを考慮すると, ライトモデルから流れてきたイベントデータを格納するのに適した場所と適したデータ構造はフロントエンドエンジニアのほうが詳しくなりがちなので, フロントエンドエンジニアに馴染みある言語を選択したほうが開発がし易くなるのではないかと考えたためです.
この辺は, 実際に本プロジェクトで試してみないと個人開発では見えてこない部分ですね.

CDC (Change data capture)に DynamoDB Streams を選択

DynamoDB には変更データキャプチャとしてほぼリアルタイムで変更後の内容を参照できる DynamoDB Streams が用意されています. これによりジャーナルに保存したイベントデータを DynamoDB Streams を用いて RMU に流すことができるようになります. また, DynamoDB Streams は AWS Lambda のトリガーを作成することができる ため, DynamoDB の更新を契機に RMU を実行することが可能になります.

ただしトリガー作成はローカル環境ではできませんので, 今回は下記の記事を参考にさせていただき DynamoDB Streams の更新を監視し, 更新があればローカルのラムダ関数を実行する /read-model-updater/main.py を作成しました.

https://www.isoroot.jp/blog/7753/

DynamoDB Streams の設定自体はテーブル作成時に合わせて行ってしまいたいので, DDL に --stream-specification を付与するようにしました.

https://github.com/kuramapommel/cqrs-es-example/blob/78c001d9a025d4a8356c936508021f66482016d7/write-api-server/scripts/dynamodb/create-tables.sh#L32-L33

フロントエンド

フロントエンドは今回 WEB とモバイルの2つを用意しましたが, ここについては現状本当にこだわりがなく純粋に自分がいま興味があって触ってみたい技術を選んだだけなので, 主な技術スタックの紹介だけにします.
自由に得意なものを選んであげるのが良いと思います.

WEB フロントエンド: cqrs-es-example/web-frontend/

  • 主な使用言語: TypeScript
  • 主なフレームワーク, ライブラリ, ツールキット
  • 主なテストツール, テスティングライブラリ
  • ミドルウェア
    • パッケージマネージャ: npm

モバイルアプリ(iOS/Android): cqrs-es-example/mobile-app/

CQRS + ES には直接関係ないその他の採用技術

本編には直接関係ないので折りたたんで起きます. 気になる方は覗いてみてください.

その他採用技術

E2E テスト: cqrs-es-example/specs/

自然言語でテストを書くことができる Gauge を採用しています.
これにより動作する仕様書を実現することができます.

今回は Playwright を使って画面を操作するようにカスタムしています.

git hooks: cqrs-es-example/lefthook.ynml

git hooks を定義しておくことで git commit 時に自動テストを実行する事ができますが, lefthook を使うことで, 対象のディレクトリ以下に変更があった場合のみ対象の hooks を実行するというような設定が簡単に定義できるため, モノレポ管理がし易くなります.

これからやってみたいこと

ひとまずローカルで動くようにはなりましたがまだまだ試してみたいことはあります.
どれもこれもまだ知識がない状態(聞いた話程度)ですのでトンチンカンなこと言っているかもしれないですが, その場合はソフトにご鞭撻ください.

GraphQL に置き換える

GraphQL は CQRS と相性が良いらしいです.

https://x.com/naoya_ito/status/1840256141449720132

これは, GraphQL が更新は mutation 参照は query と分かれていることからだと解釈しています.

現行の 429-wedding は AWS Amplify で構築しており, AWS AppSync を使っているのですが, 今回の構成に合わせるのであれば Yoga などで graphql server を自作することになると思います.
その場合, query であればそのまま Prisma 使ってリードモデル側のデータベースからデータを取得し, mutation であれば Akka サーバにリクエストを送るような形で実装すれば, 良い感じにまとまるんじゃないかなと考えています.

Kafka を採用し外部システムともイベント駆動で連携できるようにする

Kafka はストリームデータをリアルタイムに捌く分散データストアです.
RMU でリードモデルの更新が成功した後に Kafka Producer にデータを流すようにしておけば, 外部システムが自由に接続して自由にイベントデータを購読することができるようになります.

これにより,疎結合にシステム連携ができるようになるのではないかと考えているため試してみたいと考えています.

また, (こういう使い方が良い使い方なのかはわからないですが)GraphQL の subscription を使って Kafka のイベントストリームを購読することでチャットアプリのようなリアクティブなサービスにも対応できるようになるのではないかと考えていますので, それも試してみたいです.

AWS 上に構築

何度も述べている通り現在はまだローカル環境でしかためしていないので, この構成を AWS 上に構築したいです.
恥ずかしながらぼくはまだ IaC を自身で触ってみたことがないため, その際には AWS CDK のような IaC ツールに触れてみたいと思っています.

なお, すでに Minikube 上での起動はできるようにしているので, 基本的には Amazon EKS か Amazon ECS 上での構築を考えています.

Argo CD を使った継続的デリバリーの導入

Argo CD は Kubernetes 向けの CD ツールで GitOps に対応しているようです.

https://openstandia.jp/oss_info/argocd/

また, Argo Rollouts と組み合わせることで, カナリアリリースやブルーグリーンデプロイにも対応することができるようです.

CQRS + ES には直接関係ないその他のやってみたいこと

同じくこちらも本編には直接関係ないため折りたたんでおきます.

その他やってみたいこと

Docker のマルチステージビルドに対応

現状のビルドプロセスは各プロジェクトの README.md に記載している通り, すべてターミナル上でビルドするようにしているのですが, Docker にはマルチステージビルドという機能があり, ビルドも Docker コンテナ内で実行することができるようになります.

https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/

現在手動ビルドで行っていますが, マルチステージビルドに対応することでビルドも自動化することができますので, 手間削減のためにも品質担保のためにも導入したいです.

WebComponents の導入

WebComponents は再利用可能なカスタム要素を作成することができる技術です.

https://developer.mozilla.org/ja/docs/Web/API/Web_components

ブラウザ標準の機能でカスタム要素を作成することができるため, プロダクトをまたいで全社共通のコンポーネントライブラリを作りたいときなどに活かすことができる [2] という認識です.

これを利用することにより, 外部システムであっても同じ組織内のプロダクトであれば統一感をもたせることができ, また頻繁に使われる部品の再生産の防止や品質担保につなげることができるようになると考えているため試してみたいです.

CQRS + ES の構築に取り込んでみて

様々な概念をまとめ上げた総集編のようなアーキテクチャなので学ぶことは多く, ソフトウェアエンジニアとしてのスキルレベルが今回だいぶ上がったと思います.
理解するのも実装するのもとても大変でしたが, スケーラブルなアーキテクチャの実装パターンを獲得できた(獲得できたとは言ってない)ことは強い武器を得られたと思います.

いまは個人開発でいろいろと試している段階ですが, 今後はお仕事での導入にも挑戦してみたいですね.
そのためにもまずは 429-wedding のリライトをやりきり, ひとつ実績を作りたいと考えています.

最後までお付き合いいただきましてありがとうございました.
ここに書ききれていない内容もありますので, ご指摘だけでなくもし質問などがあればぜひ Twitter にご連絡ください.

https://x.com/nullpommel

ありがとうございました.

ところで

この記事を書いているときにこんなツイートを目にしました.

https://x.com/AI_masaou/status/1915947542296334591

で, まあ試してみたんですよおもむろに.
10分弱待って出来上がったのがこちらです.

https://deepwiki.com/kuramapommel/cqrs-es-example

すごすぎる... 若干の間違いはあれど, この記事書かなくてよかったんじゃないかと思うくらいにはきれいにまとまってる...

ということで "かがくの ちからって すげー!" となった余談でした.

脚注
  1. 現時点ではまだローカル環境での構築のみとなっております. 将来的には AWS 上に環境を構築し, 本番環境での動作を確認できれば記事の内容をアップデートしようと考えています. ↩︎

  2. ブラウザ標準機能のため, React や Vue などのフレームワークに依存せず, プロダクトをまたいで再利用することができる ↩︎

Discussion