GoにおけるCQRSの実装:スケーラブルなアーキテクチャへの実践的ガイド
不要な形式主義を排して、GoでCQRSを構築する
CQRS(コマンドとクエリの責務分離)は、過大宣伝され、複雑化され、単なるCRUD(Create, Read, Update, Delete)の退屈さを解消するための万能薬と誤解されがちないくつかのパターンの一つです。
有用なバージョンははるかにシンプルです。状態を変更するコードと状態を読み取るコードを分離し、それぞれが本来の役割に合わせて進化できるようにします。マーティン・ファウラーは、CQRSを情報を更新するためのモデルと読み取りのために使用するモデルを異なるものとして使用すると定義しつつも、大多数のシステムにおいてそれはリスクのある複雑さを追加すると警告しています。マイクロソフトも同様の核心点を、より運用用語で説明しており、読み取りモデルと書き込みモデルを分離することで、それぞれを独立して最適化できるようにしています。

Goで作業する場合、この考え方は言語に非常に適合します。Goは明確な境界線、小さなインターフェース、地味なデータ型、ユースケース指向のパッケージに長けています。そのため、Goにおける基本的なCQRSは、カンファレンスのスライドでよく見られるような劇的なものよりもずっとシンプルです。始めるために、イベントソーシング、Kafka、3つのデータベースを必要としません。実際、マイクロソフトのCQRSガイダンスとThree Dots LabsのGoの例はどちらも、単純な実装では同じ基盤ストアを共有でき、コマンドハンドラとクエリハンドラをまず追加し、問題が実際に必要とする場合にのみ高度なインフラストラクチャを導入することを示しています。
CQRSが実際に意味すること
CQRSの核心は、コマンドとクエリの間で明確な線を引くことです。クエリはデータを読み取り、システムの状態を変更すべきではありません。コマンドは状態を変更し、ドメインデータを主要な結果として返すべきではありません。Three Dots Labsはこれを実用的なGoの用語で表現しており、クエリはデータを返し、コマンドは変更を行い、エラーはコマンドの正常な結果であると述べています。これが基本的な手法です。それ以外はすべてオプションです。
一般的な誤解として、CQRSは自動的に個別のデータベース、非同期プロジェクション、またはイベントソーシングを意味すると考える人がいます。しかし、それは事実ではありません。マイクロソフトのパターングイドは、個別のデータストアをデフォルトではなく、より高度な形態として明確に扱っており、Three Dots Labsは、現在のシステムにとって十分であるため、書き込みと同じデータベースから読み取るGoの実装を示しています。もしあなたの記事が一つだけ明確に教えるものがあるとすれば、それはこれです。CQRSは主にモデリングとアプリケーション構造の選択であり、必須の分散システムパッケージではありません。
もう一つの重要な詳細は命名規則です。コマンドはストレージのミューテーション(変更)ではなく、ビジネス意図をモデル化するべきです。マイクロソフトの例では「ホテルの部屋を予約する」ことと「予約ステータスを予約済みに設定する」ことを対比させ、Three Dots Labsはドメインエキスパートが話す言葉に近い名前、例えば「ScheduleTraining(トレーニングをスケジュールする)」や「CancelTraining(トレーニングをキャンセルする)」のような、一般的な「Create(作成)」や「Delete(削除)」などの動詞ではなく、そのような名前を推奨しています。Goでは、この命名規律は効果的です。なぜなら、コマンドの名前は頻繁に型名、ハンドラ名、パッケージの境界となるからです。
チームがそれを目指す理由
CQRSは、単一のCRUDモデルが多すぎることをごっちゃにしてしまうようになると魅力的になります。マイクロソフトのガイダンスは、通常の圧力点を挙げています。同じデータの読み取り表現と書き込み表現が分岐し、並行更新がロック競合を引き起こし、クエリの複雑さの下で読み取りパフォーマンスが低下し、共有エンティティがセキュリティルールを複雑な絡まりものにします。つまり、問題はCRUDが道徳的に間違っているということではありません。問題は、1つのモデルが互いに矛盾する関心事を同時に満たすことを強制されていることです。
これは技術製品で特に一般的です。書き込みは、検証、不変条件、トランザクション、ビジネスルールを重視する傾向があります。読み取りは、フィルター、結合、集計、キャッシュ、ソート、およびページやAPIが必要とする正確な形状を提供することを重視する傾向があります。CQRSは、書き込み側が厳格でドメイン指向であり続け、読み取り側が実用的でDTO指向であり続けることを可能にします。マイクロソフトは、検証と一貫性に焦点を当てた書き込みモデルと、プレゼンテーションと応答性を最適化されたDTOまたはプロジェクションに焦点を当てた読み取りモデルを明確に推奨しています。
また、チームレベルでの利点もあります。Three Dots Labsは、コマンドとクエリを分割することで結合が軽減され、実行フローが明確になり、ランダムなサービス層をたどってロジックを追う代わりに、利用可能なコマンドとクエリの短いリストを検査できるため、オンボーディングが高速化されると主張しています。マイクロソフトもまた、CQRSは複数のユーザーが同じデータを更新し、コマンドが競合を防ぐまたは解決するのに十分な粒度を必要とする協力的な環境で特に有用であると指摘しています。
私の少し主観的な見解は、多くのチームはCQRSを取り入れるのが遅すぎることです。すでに1つの「サービス」が中身のないモノリシック(単一巨大な)構造に変わってから取り入れます。しかし、多くのチームはまた、アーキテクチャ図が高価で、したがって深刻に見えたという理由だけで、取り入れるのが早すぎます。適切なタイミングは、読み取りと書き込みが形状、速度、またはルールにおいて明らかに分岐し始めたときであり、あなたのTodoアプリが野望を持っているときではありません。
利点とコスト
メッセージングや個別のストアを追加する前でも、基本的なCQRSには本物の利点があります。それは、より小さなコマンドモデル、より小さなクエリモデル、より明確なユースケース、そしてロギングや計装のような横断的関心事を適用するためのより明らかな場所を提供します。Three Dots Labsは、コードの整理、結合の軽減、よりシンプルなモデルを即時の勝利として明確に指摘し、Microservices.ioはよりシンプルなコマンドとクエリモデル、および非正規化されたスケーラブルな読み取りビューのサポートを強調しています。
問題がそれを正当化するようになると、CQRSはより強力な読み取り側の最適化の門戸を開きます。マイクロソフトのガイダンスは、個別の読み取りモデルがDTO、プロジェクション、読み取り専用レプリカ、またはまったく異なるストレージ技術を使用できることに言及しています。また、重い結合やORM依存のクエリパスを避ける方法としてマテリアライズドビューを指摘しています。書き込み側にどのデータアクセスレイヤーを使用するかを検討している場合、Comparing Go ORMs for PostgreSQL は、GORM、Ent、Bun、sqlc間のトレードオフを実用的な観点から解説しています。そこがCQRSが構造的だけでなく、運用面でも恩恵をもたらす場所です。
コストは同様に現実的です。ファウラーの警告は、依然として正しい出発点です。大多数のシステムにおいて、CQRSはリスクのある複雑さを追加します。マイクロソフトは、複雑さの増加と最終的な一貫性を核心的な考慮事項として挙げており、Microservices.ioは読み取りビューにおける潜在的なコードの重複とレプリケーションラグを追加しています。ストアを分割する場合、データベースとブローカー間の整然とした分散トランザクションに依存せずに、通常はイベントを通じてそれらを同期させる作業を引き受けることになります。
イベントソーシングは、そのコストを消去するものではありません。その形状を変更するだけです。マイクロソフトのCQRSガイダンスは、イベントソーシングはイベントストアを真実の唯一のソースにし、履歴を再生してマテリアライズドビューを再構築できるようにすると述べていますが、Event Horizonは追跡可能性と監査ログを主要な利点として挙げています。しかし、マイクロソフトはまた、ビューの生成、再生、およびイベント処理がより多くの設計複雑さを追加すると警告し、再生コストを削減するためにスナップショットを提案しています。そのため、私はイベントソーシングを「CQRSに2つ目の困難な決定を加えたもの」として説明することを好みます。入門チケットではありません。
覚えておく価値のある実用的な経験則は、基本的なCQRSは安価であり、分散型のCQRSは高価であり、この2つの会話を混同することは、チームが問題が必要とするよりもはるかに多くの複雑さに終わる最も一般的な方法の一つです。
Goにおける単純なCQRSの実装
Goにおける理にかなった最初のステップは、1つのデータベースを保持し、アプリケーションレイヤーのみを分割することです。コマンドはビジネスルールと永続性を所有します。クエリは呼び出し元に形状付けられた読み取りモデルを返します。これは、Three Dots Labsが非同期バスや個別の読み取りストアに手を伸ばす前に推奨するタイプの基本的なCQRSです。
コマンドから始める
package blog
import (
"context"
"errors"
"time"
)
type PublishPostCommand struct {
Title string
Slug string
BodyMD string
Author string
}
type PostRepository interface {
NextID(ctx context.Context) (string, error)
Save(ctx context.Context, post Post) error
}
type Post struct {
ID string
Title string
Slug string
BodyMD string
Author string
PublishedAt time.Time
}
type PublishPostHandler struct {
Repo PostRepository
Now func() time.Time
}
func (h PublishPostHandler) Handle(ctx context.Context, cmd PublishPostCommand) error {
if cmd.Title == "" || cmd.Slug == "" || cmd.BodyMD == "" {
return errors.New("title, slug, and body are required")
}
id, err := h.Repo.NextID(ctx)
if err != nil {
return err
}
post := Post{
ID: id,
Title: cmd.Title,
Slug: cmd.Slug,
BodyMD: cmd.BodyMD,
Author: cmd.Author,
PublishedAt: h.Now(),
}
return h.Repo.Save(ctx, post)
}
このハンドラは、ページを提供したり、リストレスポンスの形状付けしたり、カードグリッド用のSQLを最適化したりしようとはしません。それは単に意図を強制し、有効なアグリゲートを永続化するだけです。これがコマンド側が1つの仕事を適切に行う姿です。
クエリを追加する
package blog
import "context"
type PostView struct {
ID string
Title string
Slug string
Author string
PublishedAt string
Excerpt string
}
type LatestPostsQuery struct {
Limit int
}
type PostReadModel interface {
Latest(ctx context.Context, limit int) ([]PostView, error)
BySlug(ctx context.Context, slug string) (PostView, error)
}
type LatestPostsHandler struct {
ReadModel PostReadModel
}
func (h LatestPostsHandler) Handle(ctx context.Context, q LatestPostsQuery) ([]PostView, error) {
limit := q.Limit
if limit <= 0 {
limit = 10
}
return h.ReadModel.Latest(ctx, limit)
}
type GetPostBySlugQuery struct {
Slug string
}
type GetPostBySlugHandler struct {
ReadModel PostReadModel
}
func (h GetPostBySlugHandler) Handle(ctx context.Context, q GetPostBySlugQuery) (PostView, error) {
return h.ReadModel.BySlug(ctx, q.Slug)
}
読み取り側が書き込みモデルではなく PostView を返すことに注目してください。これは、マイクロソフトが読み取りモデルはDTOとプレゼンテーションのために最適化され、書き込みモデルはトランザクション整合性とドメインルールのために調整されるべきであると推奨していることを反映しています。
神社ではなく、Goアプリケーションのように配線する
package app
import "your/module/internal/blog"
type Application struct {
Commands Commands
Queries Queries
}
type Commands struct {
PublishPost blog.PublishPostHandler
}
type Queries struct {
LatestPosts blog.LatestPostsHandler
GetPostBySlug blog.GetPostBySlugHandler
}
この形状は偶然ではありません。Three Dots Labsは、Wild Workoutsで非常に同様のパターンを使用しています。Commands と Queries を公開する Application 型、そして個別の app/command および app/query パッケージから具体的ハンドラが配線されています。彼らのサービス構成コードは、それらのパッケージを個別にインポートし、それらから単一のアプリケーションオブジェクトを構築します。これは、フレームワークのドラマなしで境界線を明確にするためのクリーンでGoらしい方法です。ハンドラが増えるにつれて依存関係グラフが複雑になる場合、Dependency Injection in Go は、このハンドラベースの構造と自然に組み合わさるWire、Dig、およびコンストラクターインジェクションパターンをカバーしています。
後で非同期コマンド、サービス間イベント、または非正規化された検索インデックスが必要になった場合、この基盤からそれらを追加できます。Three Dots Labsは、非同期コマンドバスと個別のクエリデータベースは出発点ではなく、後からの最適化であると明確に提示しています。
知っておくべきGoライブラリ
GoのCQRSエコシステムは.NETのものよりも狭く、正直なところそれは祝福です。午後半天で実際のオプションを調査し、必要のない3つの抽象化を採用することから逃れることができます。
Watermill
Watermillは、CQRSとメッセージングを望む場合の最も明確な現代的选择です。そのCQRSコンポーネントは、生メッセージではなくGoの構造体で作業できるようにする高レベルAPIであり、そのビルディングブロックには EventBus、EventProcessor、CommandBus、CommandProcessor が含まれます。ドキュメントは、共有トピック上の順序付き処理のためのイベントハンドラグループ、読み取りモデルの例、カスタムマーシャリングメタデータもカバーしています。CQRSレイヤーの外では、WatermillはRabbitMQ、Kafka、NATS Jetstream、Redis Streams、Google Cloud Pub/Sub、SQL、HTTPなど、幅広いパブリッシュ/サブスクライブバックエンドをサポートしています。Pkg.go.devは、Watermillをv1.0.0以降の安定したパブリックAPIを持つプロダクションレディとマークしており、現在の公開モジュールバージョンはv1.5.2で、GitHubはそのリリースを5月13日にリストしています。
commandBus, err := cqrs.NewCommandBusWithConfig(pub, cfg)
eventBus, err := cqrs.NewEventBusWithConfig(pub, cfg)
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(router, cfg)
eventProcessor, err := cqrs.NewEventProcessorWithConfig(router, cfg)
コマンドとイベントがプロセス境界を越える必要がある場合、リトライと再配信セマンティクスをファーストクラスにしたい場合、またはあなたの「単純な」サービスがすでにイベント駆動の現実の半ばにあることを知っている場合に、Watermillを使用します。 downside(欠点)は、ブローカー、トピック、順序付け、および idempotency についての会話を、望んでいようといまいと行うことになるということです。それはWatermillの欠陥ではありません。それは問題領域のコストです。
Event Horizon
Event Horizonは、GoのためのCQRSおよびイベントソーシングツールキットです。そのメンテナーは、プロダクションシステムで使用されていると説明していますが、APIは最終的ではないとも注記しています。ツールキットは、アグリゲート、コマンド、およびイベント登録ヘルパー、メモリおよびMongoDBバリエーションのための公式イベントストア実装、プロジェクションおよびリポジトリサポート、そしてアウトボックスパターンに基づくアプリケーションを含む例を提供します。リリースストリームは依然としてアクティブで、GitHubは6月16日にv0.17.0を示しており、以前のリリースはスナップショット、リトライ可能なプロジェクション、永続的なコマンドスケジューリング、およびアウトボックスパターンなどの機能を追加しました。
eh.RegisterAggregate(func(id uuid.UUID) eh.Aggregate {
return &InvoiceAggregate{ID: id}
})
eh.RegisterCommand(func() eh.Command {
return &CreateInvoiceCommand{}
})
Event Horizonは、イベントソーシングがポイントであり、オプションの将来の拡張ではなく、最も理にかなっています。監査フレンドリーなストリーム、再生可能な履歴、プロジェクション、およびイベントストア中心のモデルを望む場合、それは真剣なオプションです。モノリス内でよりクリーンなアプリケーションサービスだけを望む場合、それは必要以上の機械装置かもしれません。「APIは最終的ではない」という注記はまた、Watermillよりも時間とともに少し更多的な適応を予算する必要があることを意味します。
Go-MediatR
Go-MediatRは完全なCQRSフレームワークではありませんが、プロセス内CQRSに有用です。そのREADMEは、CQRSと併用されるメディエーターパターン実装と説明しており、コマンドとクエリのリクエスト/レスポンスディスパッチ、イベントの通知ディスパッチ、および横断的関心事のためのパイプラインビヘイビアを備えています。プロジェクトはまたタグ付きリリースを持っており、GitHubはv1.4.0を最新リリースとしてリストし、スレッドセーフなハンドラ登録および並行性関連の改善を指摘しています。
resp, err := mediatr.Send[*CreateProductCommand, *CreateProductResponse](ctx, cmd)
post, err := mediatr.Send[*GetPostBySlugQuery, *PostView](ctx, query)
ハンドラベースのコマンドとクエリを望むが、ブローカー、プロジェクションエンジン、またはイベントストアを望まない場合に、これは良い適合です。.NETのMediatRから来たチームに特にフレンドリーです。トレードオフは同様に明確です:あなたは依然として独自の永続性、読み取りモデルのリフレッシュ戦略、およびプロセス外統合ストーリーを設計する必要があります。つまり、それはアーキテクチャ全体ではなく、アプリケーション境界を提供します。
古いフレームワークと参考資料
依然として教訓的である古いGo CQRSライブラリがあります、しかし私はそれらをグリーンフィールドデフォルトとして扱う前に、参考資料として扱います。
jetbasrawi/go.cqrs は、Greg Youngの原則に基づくサンプルアプリケーションを持つGo CQRS参考実装と説明しています。しかし、pkg.go.devは有効な go.mod、タグ付きバージョン、および安定バージョンを示しておらず、GitHubはリリースを示しておらず、パッケージメタデータは7.4年前に公開されました。それは有用な歴史ですが、2026年の新しいプロダクション採用のための強いシグナルではありません。
andrewwebber/cqrs も同様です:イベントソーシング、コマンドの発行と処理、イベントの発行、および発行されたイベントからの読み取りモデルの生成を提供しますが、パッケージメタデータも7.4年前に公開されました。あなたが以前のGo CQRSライブラリがどのように問題を扱ったかを理解したい場合、私はそれを絶対に読むでしょう。しかし、あなたが自分のアーキテクチャスタックのパートタイムメンテナーになることに満足していない限り、新しいコードベースの基盤としてそれを作ることは慎重になります。
実用的なGoプロジェクトレイアウト
典型的なGo CQRSレイアウトは、ユースケースを明確にするべきであり、一般的な抽象化の下にそれを埋めるべきではありません。Wild Workoutsはここで良い参考です。リポジトリは internal 下でバウンデッドコンテキストを分離し、コマンドとクエリを個別のアプリケーションパッケージに保持し、それらを Commands と Queries を公開する Application 型に配線します。サービス構成は、アダプター、ハンドラ、および依存関係を明確に組み立てます。ここで説明されているパターンは、Go Project Structure: Practices & Patterns のより広範なガイダンスと一致しており、Goコードベースが成長するにつれてチームが直面するレイアウト決定のより広範なセットをカバーしています。
実用的なレイアウトは次のようになります:
internal/
blog/
app/
app.go
command/
publish_post.go
unpublish_post.go
query/
get_post_by_slug.go
latest_posts.go
domain/
post.go
slug.go
adapters/
postgres/
post_repository.go
post_read_model.go
ports/
http/
handler.go
service/
application.go
このレイアウトにはいくつかの利点があります。
第一に、コマンドとクエリのハンドラは、それらが実装するユースケースの近くに存在します。それは、ビジネス振る舞いをリポジトリやトランスポートレイヤーにちなんで命名されたハンドラに隠すことを難しくします。Three Dots Labsは、Wild Workoutsで直接これを行っており、app/command と app/query は個別のパッケージであり、トップレベルの Application が責任によってハンドラをグループ化しています。
第二に、ドメインパッケージは不変条件と振る舞いに焦点を当てたままにすることができ、読み取り側はDTOとプロジェクションを返す自由にされています。それはマイクロソフトの書き込みモデルと読み取りモデルのガイダンスと一致し、読み取り側がイデオロギー的な純粋さのためにドメインオブジェクトを介して強制的に戻される一般的なCQRSアンチパターンを回避します。
第三に、この構造は、最も小さい有用なCQRSから重いバリエーションまでスケーリングします。あなたは今日1つのPostgreSQLデータベースと2つのリポジトリ実装を保持し、後で検索インデックスまたはイベント駆動の読み取りプロジェクションを追加して、アプリケーションの形状全体を書き直す必要はありません。Three Dots Labsは、システムが必要とする場合にのみ、基本的なCQRSから非同期コマンドバスおよび個別のクエリストアへのその進行を明確に説明しています。
CQRSがフィットする時とフィットしない時
CQRSは、読み取りと書き込みが真に異なる問題である時に理にかなっています。マイクロソフトは、読み取りと書き込みモデルが独立した最適化を必要とするワークロード、複数のユーザーが同じデータを共同で使用する、そして明確な分離がパフォーマンス、スケーラビリティ、およびセキュリティに役立つ場合にそれを推奨しています。Microservices.ioは、ドメインイベントまたはマテリアライズドプロジェクションから構築された非正規化された高性能ビューというもう一つの古典的な適合を追加しています。Three Dots Labsもまた、複雑なビジネスロジック、保守性、および非同期コマンドまたは専門的な読み取りストアへの将来の拡張への延長を、Goでそれを採用する強力な理由として指摘しています。
実際には、それは多くの場合、リッチなドメインルール、高価な読み取りモデル、アグリゲートに綺麗にマップしないレポートビュー、またはイベントを発行し他方でプロジェクションを構築するマイクロサービスを持つシステムを意味します。そのような文脈では、Saga pattern for distributed transactions は、サービス境界をまたぐマルチステップのビジネス操作の調整メカニズムとしてCQRSの横に頻繁に現れます。それはまた、書き込み側が厳格で監査可能でなければならず、読み取り側がUIまたはAPIの消費のために速く形状付けられなければならない製品にも適合します。あなたがプロジェクション、レプリカ、またはイベントからのビューの再構築について話している場合、ラベルを使用するかどうにかかわらず、あなたはすでにCQRSの領域にいる可能性が高いです。
CQRSは、あなたのサービスが straightforward(直截な)データエディタである時には理にかなっていません。ファウラーは率直に、大多数のシステムにおいてCQRSはリスクのある複雑さを追加すると述べており、Three Dots Labsは、本質的に同じデータを受信して返す単純なCRUDサービスは良い適合ではないと言っています。彼ら自身のWild Workoutsの例では、より単純なユーザーサービスはClean ArchitectureとCQRSを使用しません。なぜなら、パターンはそこでその価値を払わないからです。
それは技術ブログで明確に言う価値のある部分です:CQRSは成熟度のバッジではなく、意図的なトレードであり、あなたが実際にそれが提供するものを必要とする場合にのみ理にかなっています。あなたの管理パネルが行を書き込み、同じ行を読み戻す場合、単にできるからといってモデルを分離しないでください。あなたのコマンドハンドラが主に「レコードYのフィールドXを設定する」である場合、あなたはCQRSの問題を持っていません。あなたは通常のアプリケーションを持っており、それは完全に尊敬できるソフトウェアです。
結び
GoでCQRSを実装する最善の方法は、地味なバージョンから始めることです。コマンドハンドラをクエリハンドラから分離します。コマンドがビジネス意図をモデル化するさせます。クエリが読み取りモデルを返すさせます。必要なのはそれだけなら、同じデータベースを保持します。そして、システムがあなたの手を強いる場合にのみ、非同期バス、プロジェクション、個別のストア、またはイベントソーシングを追加します。その進行は、ファウラーの複雑さについての警告、マイクロソフトの段階的CQRSガイダンス、およびThree Dots Labsの実用的なGoの例と一致しています。
ライブラリを必要とする場合、WatermillはGoにおけるメッセージ駆動CQRSのための最も強力な汎用選択であり、イベントソーシングが重心の場合、Event Horizonは魅力的であり、Go-MediatRはプロセス内コマンドとクエリディスパッチだけを必要とする場合に良い軽いタッチです。他のすべては、その場所を非常に慎重に獲得すべきです。プロダクションGoシステムにおけるコード構造、統合、およびデータアクセスパターンのより広範なマップのために、App Architecture guide は有用な補足です。
結局、それがCQRSに対する最もGoらしい答えです:パターンを使用し、衣装(コスチューム)は使用しないでください。