実現パターン
ドメインイベント、イベントソーシング、CQRS の概念を理解したうえで、これらを実際のシステムでどう実現するかを学びます。
全体像
┌─────────────────────────────────────────────────────────────────┐
│ 実現すべき3つの要素 │
│ │
│ ① イベントの保存場所(イベントストア) │
│ 「どこに書くか」 │
│ │
│ ② イベントの伝搬方法 │
│ 「どうやって届けるか」 │
│ │
│ ③ 読み取りモデルの構築 │
│ 「どう集計・検索するか」 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ ① │ │ ② │ │ ③ │ │
│ │ イベント │ → │ 伝搬 │ → │ 読み取りモデル │ │
│ │ ストア │ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
① イベントストアの選択
イベントをどこに保存するか。
RDB(PostgreSQL 等)のテーブル
CREATE TABLE events (
id UUID PRIMARY KEY,
aggregate_id UUID NOT NULL, -- 対象エンティティのID
type VARCHAR(255) NOT NULL, -- イベントタイプ
data JSONB NOT NULL, -- イベントデータ
created_at TIMESTAMP NOT NULL
) PARTITION BY RANGE (created_at);
| メリット | デメリット |
|---|---|
| 既存インフラで使える | 大量データの集計は苦手 |
| トランザクション保証 | パーティション管理が必要 |
| SQL で自由にクエリ | イベントソーシング専用機能なし |
| チームの学習コストなし |
ほとんどのケースではこれで十分。 専用のイベントストアが必要になるのは、イベント数が数十億を超えるか、完全なイベントソーシングを採用する場合。
Apache Kafka
Producer → [Topic: events] → Consumer
├── Partition 0: tenant-A のイベント
├── Partition 1: tenant-B のイベント
└── Partition 2: tenant-C のイベント
| メリット | デメリット |
|---|---|
| 超高スループット(数百万msg/秒) | 運用が複雑 |
| 複数コンシューマーに同時配信 | クエリ機能なし |
| 順序保証(パーティション内) | 保持期間に制限 |
| マイクロサービス間の標準的な選択 |
マイクロサービスアーキテクチャで、複数のサービスがイベントを購読する場合に適切。
EventStoreDB(専用イベントストア)
Stream: user-123
Event 0: UserRegistered {name: "Alice"}
Event 1: LoginSucceeded {ip: "1.2.3.4"}
Event 2: PasswordChanged {}
...
Stream: order-456
Event 0: OrderPlaced {items: [...]}
Event 1: PaymentCompleted {amount: 1000}
| メリット | デメリット |
|---|---|
| イベントソーシング専用に設計 | 専用インフラの追加 |
| ストリーム、プロジェクション組み込み | チームの学習コスト |
| 楽観的並行制御が自然 | エコシステムが小さい |
完全なイベントソーシングを採用する場合の専用ソリューション。
選択ガイド
┌──────────────────────────────────────────────────────┐
│ │
│ まず RDB で始める │
│ ├── 十分な性能 → そのまま │
│ ├── 複数サービス間でイベント共有 → Kafka 追加 │
│ └── 完全なイベントソーシング → EventStoreDB 検討 │
│ │
└─────────── ───────────────────────────────────────────┘
② イベント伝搬の方法
イベントストアから読み取りモデルへ、どうやってイベントを届けるか。
CDC(Change Data Capture)
┌──────────┐ WAL ┌──────────┐ ┌────────────────┐
│ PostgreSQL│ ───→ │ PeerDB │ ───→ │ ClickHouse │
│ (events) │ │ Debezium │ │ Elasticsearch │
└──────────┘ └──────────┘ └────────────────┘
- DB の WAL(Write-Ahead Log)を監視し、変更を外部に配信
- アプリ変更不要 — DB に書くだけで自動的に伝搬
- 遅延: 数秒
最も導入コストが低い。 アプリがイベントの伝搬を意識する必要がない。
メッセージキュー(Kafka, RabbitMQ 等)
┌──────┐ publish ┌───────┐ subscribe ┌────────────────┐
│ App │ ────────→ │ Kafka │ ──────────→ │ 統計サービス │
└──────┘ │ │ ──────────→ │ 通知サービス │
│ │ ──────────→ │ 検索サービス │
└───────┘ └────────────────┘
- アプリが明示的にイベントを publish
- 1つのイベントを複数の購読者が受信可能
- 遅延: ミリ秒〜秒
複数サービスがイベントを 購読する場合の標準的な選択。
アプリ内 Pub/Sub
┌──────────────────────────────────────┐
│ App │
│ │
│ EventPublisher.publish(event) │
│ → @EventListener │
│ ├→ StatisticsHandler │
│ ├→ NotificationHandler │
│ └→ AuditLogHandler │
└──────────────────────────────────────┘
- Spring の
@EventListenerや Observer パターン - 同一プロセス内で完結
- 遅延: なし
モノリスアーキテクチャで、外部インフラを増やしたくない場合。
比較
| CDC | メッセージキュー | アプリ内 Pub/Sub | |
|---|---|---|---|
| アプリ変更 | 不要 | Publisher 追加 | EventListener 追加 |
| 複数購読者 | △(ツール依存) | ◎ | ○(同一プロセス) |
| 遅延 | 秒 | ミリ秒〜秒 | なし |
| 信頼性 | 高(WALベース) | 高(at-least-once) | 中(プロセス障害で消失) |
| インフラ追加 | CDCツール | MQクラスタ | なし |
| 適用場面 | CQRS の読み取りモデル構築 | マイクロサービス間連携 | モノリス内の疎結合化 |
③ 読み取りモデルの選択
イベントから構築する「読み取り専用のデータストア」。用途に応じて最適なものを選びます。
┌─────────────────────────────────────────────────────────────┐
│ 同じイベントから │
│ 複数の読み取りモデルを構築 │
│ │
│ events ──┬──→ ClickHouse → 統計ダッシュボード │
│ ├──→ Elasticsearch → 監査ログ全文検索 │
│ ├──→ Redis → リアルタイムカウンター │
│ └──→ PostgreSQL → マテリアライズドビュー │
│ │
│ 各モデルは独立して構築・再構築可能 │
└─────────────────────────────────────────────────────────────┘
| 読み取りモデル | 得意なこと | 用途例 |
|---|---|---|
| ClickHouse | 大量データの集計(COUNT, SUM, GROUP BY) | 統計ダッシュボード、月次レポート |
| Elasticsearch | 全文検索、フィルタ | 監査ログ検索、イベント検索 |
| Redis | 超低レイテンシの読み取り | リアルタイムカウンター、セッション |
| PostgreSQL(マテビュー) | 追加インフラなし | 小規模な集計、事前計算 |
組み合わせの具体例
シンプル構成(RDB + CDC + ClickHouse)
┌──────┐ ┌──────────┐ CDC ┌────────────┐
│ App │ → │PostgreSQL│ ───→ │ ClickHouse │ → ダッシュボード
└──────┘ │(events) │ └────────────┘
└──────────┘
- インフラ: PostgreSQL + PeerDB + ClickHouse
- アプリ変更: なし(PostgreSQL に INSERT するだけ)
- 適用: 統計・分析を高速化したい
フル構成(RDB + Kafka + 複数読み取りモデル)
┌──────┐ ┌──────────┐ ┌───────┐ ┌────────────────┐
│ App │ → │PostgreSQL│ → │ Kafka │ → │ ClickHouse │ → 統計
└──────┘ └──────────┘ │ │ → │ Elasticsearch │ → 検索
│ │ → │ 通知サービス │ → メール
└───────┘ └────────────────┘
- インフラ: PostgreSQL + Kafka + ClickHouse + Elasticsearch
- アプリ変更: Kafka Publisher 追加
- 適用: マイクロサービス、多 様な読み取り要件
段階的な導入
┌─────────────────────────────────────────────────────────────┐
│ 段階的に進める │
│ │
│ Step 1: アプリ内 Pub/Sub │
│ ├── EventListener で統計更新・通知を疎結合化 │
│ └── インフラ追加なし │
│ │
│ Step 2: CDC + OLAP │
│ ├── PeerDB で ClickHouse にイベントを同期 │
│ ├── 統計APIの読み取り先を ClickHouse に │
│ └── アプリ変更なし │
│ │
│ Step 3: メッセージキュー │
│ ├── Kafka で複数サービスにイベント配信 │
│ ├── 通知・検索・分析を独立サービス化 │
│ └── マイクロサービス化の基盤 │
│ │
│ 全部一度にやる必要はない。 │
│ 問題が顕在化した部分から段階的に。 │
│ │
└─────────────────────────────────────────────────────────────┘
関連ドキュメント
読み取りモデルの構築
- 大量データ集計・分析(OLAP): ClickHouse による統計・分析
- データ投入パターン: CDC, Kafka, S3 の具体的な方法