ストーリー
イベント駆動アーキテクチャ(EDA)とは
サービスが直接他のサービスを呼び出すのではなく、「何が起きたか」というイベントを発行し、関心のあるサービスがそのイベントを購読して処理するアーキテクチャです。
リクエスト駆動(命令型):
Order Service ──→ 在庫を減らせ ──→ Inventory Service
Order Service ──→ 決済しろ ──→ Payment Service
Order Service ──→ 通知を送れ ──→ Notification Service
イベント駆動(宣言型):
Order Service ──→ 「注文が作成された」──→ [Event Bus]
├──→ Inventory Service(在庫を減らす)
├──→ Payment Service(決済を開始)
└──→ Notification Service(通知を送る)
イベントの種類
1. ドメインイベント
ビジネスドメインで起きた事実を表すイベントです。
// ドメインイベントの例
interface OrderCreated {
type: "order.created";
data: {
orderId: string;
userId: string;
items: Array<{ productId: string; quantity: number }>;
totalAmount: number;
};
metadata: {
eventId: string; // イベントの一意なID
timestamp: string; // 発生時刻
source: string; // 発行元サービス
correlationId: string; // リクエスト追跡ID
};
}
interface PaymentCompleted {
type: "payment.completed";
data: {
paymentId: string;
orderId: string;
amount: number;
method: "credit_card" | "bank_transfer";
};
metadata: {
eventId: string;
timestamp: string;
source: "payment-service";
correlationId: string;
};
}
2. 統合イベント
サービス間の連携のために発行されるイベントです。
3. イベント通知 vs イベント伝搬
// イベント通知: 「何が起きたか」だけ通知し、詳細はAPIで取得
interface OrderCreatedNotification {
type: "order.created";
data: { orderId: string }; // 最小限の情報
// 受信側がAPI呼び出しで詳細を取得
}
// イベント伝搬: イベントに必要な情報をすべて含める
interface OrderCreatedCarrying {
type: "order.created";
data: {
orderId: string;
userId: string;
items: Array<{ productId: string; quantity: number; price: number }>;
totalAmount: number;
shippingAddress: Address;
};
// 受信側はAPI呼び出し不要
}
| 方式 | メリット | デメリット |
|---|---|---|
| イベント通知 | ペイロードが小さい | 追加のAPI呼び出しが必要(結合度UP) |
| イベント伝搬 | API呼び出し不要(疎結合) | ペイロードが大きい、データの鮮度 |
イベント駆動のパターン
Publish-Subscribe(Pub/Sub)
// Publisher(発行者): イベントを発行
class OrderService {
constructor(private eventBus: EventBus) {}
async createOrder(data: OrderData): Promise<Order> {
const order = await this.orderRepo.save(data);
// イベントを発行(誰が購読するか知らない)
await this.eventBus.publish("order.created", {
orderId: order.id,
userId: data.userId,
items: data.items,
totalAmount: data.totalAmount,
});
return order;
}
}
// Subscriber(購読者): イベントを処理
class InventoryService {
constructor(eventBus: EventBus) {
// イベントを購読
eventBus.subscribe("order.created", this.handleOrderCreated.bind(this));
}
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
for (const item of event.data.items) {
await this.inventoryRepo.reserveStock(item.productId, item.quantity);
}
}
}
Event Sourcing
状態を保存する代わりに、状態変化のイベントをすべて記録します。
// Event Sourcing: イベントの履歴からステートを再構築
class OrderAggregate {
private events: DomainEvent[] = [];
private state: OrderState = { status: "DRAFT", items: [], total: 0 };
// イベントを適用してステートを更新
apply(event: DomainEvent): void {
this.events.push(event);
switch (event.type) {
case "order.created":
this.state = { ...this.state, status: "CREATED", items: event.data.items };
break;
case "order.paid":
this.state = { ...this.state, status: "PAID" };
break;
case "order.shipped":
this.state = { ...this.state, status: "SHIPPED" };
break;
case "order.cancelled":
this.state = { ...this.state, status: "CANCELLED" };
break;
}
}
// イベント履歴からステートを再構築
static fromEvents(events: DomainEvent[]): OrderAggregate {
const aggregate = new OrderAggregate();
events.forEach(e => aggregate.apply(e));
return aggregate;
}
}
イベント駆動のメリットとデメリット
| メリット | デメリット |
|---|---|
| サービス間の疎結合 | デバッグが困難 |
| スケーラビリティが高い | イベントの順序保証が難しい |
| 新しい購読者の追加が容易 | 結果整合性の受け入れ |
| 障害の分離 | イベントストームのリスク |
| 監査ログが自然に残る | 処理の全体像が見えにくい |
まとめ
| ポイント | 内容 |
|---|---|
| EDAとは | イベントを中心にサービスが連携するアーキテクチャ |
| イベントの種類 | ドメインイベント、統合イベント |
| 2つの伝え方 | イベント通知 vs イベント伝搬 |
| 主要パターン | Pub/Sub、Event Sourcing |
チェックリスト
- リクエスト駆動とイベント駆動の違いを説明できる
- ドメインイベントの構造を理解した
- イベント通知とイベント伝搬の使い分けを判断できる
- Event Sourcingの概要を理解した
次のステップへ
次はイベントを運ぶインフラ「メッセージブローカー」の選択について学びます。Kafka、RabbitMQ、SQSの特徴を比較しましょう。
推定読了時間: 25分