この記事の要点
• イベント駆動アーキテクチャ(EDA)はシステム間をイベントで疎結合に接続する設計
• イベントの種類: ドメインイベント・統合イベント・コマンドイベント
• 結果整合性やイベント順序の保証など考慮すべき課題がある
イベント駆動アーキテクチャとは
イベント駆動アーキテクチャ(EDA: Event-Driven Architecture)は、システム間の通信をイベント(出来事の通知)を介して行う設計パターンです。コンポーネント間の依存を減らし、スケーラブルで柔軟なシステムを構築できます。
イベントとは: システム内で発生した意味のある状態変化を表すメッセージです。「ユーザーが登録された」「注文が完了した」「在庫が減少した」などがイベントの例です。
従来のリクエスト駆動との違い
リクエスト駆動(同期)
flowchart LR
Order["注文サービス"] --> Stock["在庫サービス"] --> Payment["決済サービス"] --> Notify["通知サービス"]
Note["各サービスの応答を待つ"]
- サービス間が密結合
- 1つのサービス障害が全体に影響
- 処理時間は各サービスの合計
イベント駆動(非同期)
flowchart TB
Order["注文サービス"] --> Event["注文完了イベント"]
Event --> Stock["在庫サービス<br/>(独立して処理)"]
Event --> Payment["決済サービス<br/>(独立して処理)"]
Event --> Notify["通知サービス<br/>(独立して処理)"]
- サービス間が疎結合
- 障害が局所化される
- 並列処理が可能
イベントの種類
ドメインイベント
ビジネスドメインで発生した出来事を表します。
// ドメインイベントの例
{
"eventType": "OrderPlaced",
"eventId": "evt_123456",
"timestamp": "2024-01-15T10:30:00Z",
"payload": {
"orderId": "ord_789",
"customerId": "cust_456",
"items": [...],
"totalAmount": 5000
}
}
統合イベント
異なるサービス間で共有されるイベントです。
通知イベント
状態変化を通知するだけで、詳細データを含まないイベントです。
// 通知イベント(Fat vs Thin)
// Thin Event - 詳細は別途取得が必要
{ "eventType": "UserUpdated", "userId": "123" }
// Fat Event - 必要な情報をすべて含む
{ "eventType": "UserUpdated", "userId": "123", "name": "Alice", "email": "..." }
イベントソーシング
状態をイベントの履歴として保存するパターンです。
従来のCRUD
flowchart TB
subgraph CRUD["従来のCRUD - 現在の状態のみを保存"]
Orders["orders<br/>id: 1, status: 'shipped'"]
end
イベントソーシング
flowchart TB
subgraph Events["すべてのイベントを保存"]
E1["1. OrderCreated (2024-01-01)"]
E2["2. PaymentReceived (2024-01-02)"]
E3["3. OrderShipped (2024-01-03)"]
end
Events -->|再生| State["現在の状態: status = 'shipped'"]
メリット
- 完全な監査証跡: すべての変更履歴を追跡可能
- 時間旅行: 任意の時点の状態を再現可能
- イベント再生: バグ修正後に状態を再構築可能
デメリット
- 複雑性が増す
- イベントスキーマの進化が課題
- 読み取りパフォーマンスに工夫が必要
CQRS(コマンドクエリ責務分離)
読み取り(Query)と書き込み(Command)のモデルを分離するパターンです。
flowchart TB
Command["Command<br/>(書き込みモデル)"]
Command -->|イベント発行| EventStore["Event Store"]
EventStore -->|投影| Query["Query<br/>(読み取りモデル)"]
書き込みモデル
// コマンドハンドラ
async function handlePlaceOrder(command) {
const order = new Order(command.orderId);
order.addItems(command.items);
order.place();
await eventStore.save(order.getUncommittedEvents());
}
読み取りモデル
// 読み取り用に最適化されたビュー
const orderSummary = {
orderId: "123",
customerName: "Alice", // 顧客情報も結合済み
itemCount: 3,
totalAmount: 5000,
status: "shipped"
};
実装パターン
Sagaパターン
複数サービスにまたがるトランザクションを、イベントのチェーンで実現します。
- 注文作成 →
OrderCreated - 在庫確保 →
InventoryReserved(または失敗時InventoryReservationFailed) - 決済処理 →
PaymentProcessed(または失敗時PaymentFailed) - 注文確定 →
OrderConfirmed
失敗時は補償トランザクション: PaymentFailed → 在庫解放(補償)→ 注文キャンセル
Outboxパターン
データベース更新とイベント発行を確実に行うパターンです。
1. トランザクション内で:
- ビジネスデータを更新
- outboxテーブルにイベントを挿入
2. 別プロセスで:
- outboxテーブルをポーリング
- イベントをメッセージキューに発行
- 発行済みをマーク
注意: イベント駆動ではイベントの順序保証と重複配信(at-least-once)への対応が必要です。コンシューマーは冪等に設計してください。
実践メモ: イベントスキーマは前方互換性を保つよう設計しましょう。新フィールドの追加はOKですが、既存フィールドの削除や型変更はNGです。
考慮すべき課題
結果整合性
- 注文サービス: 注文状態 = “完了”
- 在庫サービス: まだイベント未処理 → 不整合期間が存在
- ↓ イベント処理後
- 在庫サービス: 在庫減少 → 整合性回復
イベントの順序
正しい順序:
- OrderCreated
- OrderUpdated
- OrderShipped
順序が乱れると:
- OrderShipped(?)
- OrderCreated
→ 状態が壊れる可能性
冪等性
同じイベントが複数回配信されても結果が変わらないように設計します。
async function handleInventoryReserved(event) {
// 冪等性キーで重複チェック
const processed = await db.processedEvents.findById(event.eventId);
if (processed) return;
await db.inventory.reserve(event.payload);
await db.processedEvents.insert({ eventId: event.eventId });
}
まとめ
イベント駆動アーキテクチャは、複雑なシステムを疎結合で柔軟に設計するための強力なパターンです。イベントソーシングやCQRSと組み合わせることで、監査性やスケーラビリティを向上させられます。ただし、結果整合性やイベントの順序など、分散システム特有の課題に対処する必要があります。
参考リソース
- Martin Fowler - What do you mean by “Event-Driven”?
- Microsoft - Event-driven architecture style
- CloudEvents Specification
- Apache Kafka Documentation
- AWS - Event-driven architecture