イベント駆動アーキテクチャ / Event-Driven Architecture
CQRS、イベントソーシング、Saga パターン。 集約間の結果整合性とシステム間連携の基盤。
→ DDD 対応:
ddd/04-domain-events.md,ddd/02-context-map.md(統合パターン)
1. ドメインイベント
定義
「ドメインで何かが起きた」ことを表す不変のオブジェクト
| 特徴 | 説明 |
|---|---|
| 過去形 | OrderAccepted (受注受付された) — 過去に起きた事実 |
| 不変 | 一度発行されたら変更不可 |
| 自己完結 | イベント自身に必要な情報をすべて含む |
コマンド vs イベント
| コマンド | イベント | |
|---|---|---|
| 時制 | 命令形 (CreateOrder) | 過去形 (OrderCreated) |
| 意図 | 「こうして欲しい」 | 「こうなった」 |
| 受信者 | 1 つ | 0 以上 (誰も聞いていなくてもよい) |
| 失敗 | あり得る | 既に起きた事実 (失敗しない) |
2. イベントストーミング (Event Storming)
目的
ドメインエキスパートと開発者が協働でドメインモデルを発見する手法。
プロセス
1. ドメインイベントを付箋で貼り出す (オレンジ)
例: 「見積が承認された」「受注が登録された」
2. コマンドを追加 (青)
例: 「見積を承認する」→ 「見積が承認された」
3. 集約を特定 (黄)
例: コマンドとイベントをグルーピング → 「見積集約」
4. バウンデッドコンテキストを発見
例: イベントの流れの断絶点 = コンテキスト境界
付箋の色
| 色 | 要素 | 例 |
|---|---|---|
| オレンジ | ドメインイベント | OrderAccepted |
| 青 | コマンド | AcceptOrder |
| 黄 | 集約 | SalesOrder |
| ピンク | 外部システム | 営業CRM |
| 紫 | ポリシー/ルール | 「承認済み見積のみ受注可」 |
| 緑 | 読取モデル | 受注一覧画面 |
3. CQRS (Command Query Responsibility Segregation)
原則
書き込み (Command) と読み取り (Query) を分離する
┌─ Command Side ─┐ ┌─ Query Side ─┐
│ CreateOrder │ │ OrderList │
│ → Domain Model │────→│ → Read Model │
│ → Write DB │ イベント │ → Read DB │
└────────────────┘ └──────────────┘
いつ使うか
| 状況 | CQRS が有効 |
|---|---|
| 読み書きの負荷が大きく異なる | 読取を最適化したい |
| 複雑なドメインロジック | 書込み側に集中したい |
| 複数集約の横断的な表示 | 読取専用のビューを作りたい |
我々のシステムでの適用
CostReport = CQRS の Read Model
Command Side: 受入登録, 日報登録, 検収登録 (各集約が独立に処理)
↓ ドメインイベント
Query Side: CostReport (材料費+労務費+検収額を非同期に集計)
4. イベントソーシング (Event Sourcing)
原則
状態を直接保存するのではなく、イベントの列として保存する
従来: orders テーブルの status = 'completed' を UPDATE
ES: events テーブルに OrderAccepted, WorkStarted, WorkCompleted を INSERT
→ 現在の状態はイベントを再生 (replay) して導出
メリットとデメリット
| メリット | デメリット |
|---|---|
| 完全な監査証跡 | 複雑性の増加 |
| 時点指定での状態復元 | イベントスキーマの進化が難しい |
| イベント駆動が自然に実現 | 読取パフォーマンス (→ CQRS で解決) |
製造業での適用判断
| ユースケース | ES が適する? | 理由 |
|---|---|---|
| 受注のステータス追跡 | △ | ステータス遷移の履歴は有用だが、フル ES はオーバー |
| 原価の積み上げ | ✅ | 材料費・労務費の積み上げは本質的にイベントの列 |
| マスタデータ | ❌ | 変更頻度が低く、最新状態だけあればよい |
推奨: フル ES ではなく、イベントの記録 + 状態テーブル のハイブリッド。
5. Saga パターン
問題
複数の集約/サービスにまたがるトランザクション。 分散トランザクション (2PC) は複雑で脆い。
解決: Saga
長期実行トランザクションを、ローカルトランザクションの連鎖 として実行する。 各ステップに 補償アクション (Compensating Action) を定義する。
コレオグラフィ vs オーケストレーション
| コレオグラフィ | オーケストレーション | |
|---|---|---|
| 制御 | 各サービスが自律的にイベントに反応 | 中央のオーケストレーターが制御 |
| 結合度 | 低い | オーケストレーターに集中 |
| 可視性 | フロー全体が見えにくい | フローが明確 |
| 適用 | シンプルなフロー (3-4 ステップ) | 複雑なフロー |
製造業での Saga 例: 受注→手配→製作指示
コレオグラフィ方式:
1. SalesOrder: save() → OrderAccepted イベント発行
2. ProcurementService: OrderAccepted を受信 → PurchaseOrder 作成
3. ProductionService: OrderAccepted を受信 → ProductionInstruction 作成
補償: 受注キャンセル時
1. SalesOrder: cancel() → OrderCancelled イベント発行
2. ProcurementService: OrderCancelled を受信 → PurchaseOrder キャンセル
3. ProductionService: OrderCancelled を受信 → ProductionInstruction キャンセル
6. トランザクショナルアウトボックス
問題
「DB 更新とイベント発行を原子的に行う」のは難しい。
❌ 1. DB に save() ← 成功
2. イベント publish() ← ここで失敗すると不整合
解決: アウトボックステーブル
✅ 1. DB に save() + outbox テーブルにイベントを INSERT (同一トランザクション)
2. 別プロセスが outbox を polling → イベントを publish → outbox から削除
7. 設計判断のガイドライン
| 要件 | 推奨パターン |
|---|---|
| 集約内の整合性 | 通常のトランザクション |
| 集約間の整合性 (同一 BC) | ドメインイベント + 結果整合性 |
| BC 間の連携 | イベント駆動 (Webhook/メッセージキュー) |
| 読取の最適化 | CQRS Read Model |
| 完全な監査証跡 | イベント記録 (フル ES は不要なら避ける) |
| 複数サービスのワークフロー | Saga (コレオグラフィ推奨) |