ストーリー
Choreography(コレオグラフィ)
各サービスがイベントを発行・購読し、自律的に協調するスタイルです。中央の制御者はいません。
注文フロー(Choreography):
[Order]──publish──→ order.created
│
[Payment]──subscribe──→ order.created
│
[Payment]──publish──→ payment.completed
│
[Inventory]──subscribe──→ payment.completed
│
[Inventory]──publish──→ stock.reserved
│
[Shipping]──subscribe──→ stock.reserved
│
[Shipping]──publish──→ shipment.created
実装例
// Order Service
class OrderService {
async createOrder(data: OrderData): Promise<Order> {
const order = await this.orderRepo.save({ ...data, status: "PENDING" });
await this.eventBus.publish("order.created", { orderId: order.id, ...data });
return order;
}
// 決済失敗イベントを購読 → 補償
async handlePaymentFailed(event: PaymentFailedEvent): Promise<void> {
await this.orderRepo.updateStatus(event.data.orderId, "CANCELLED");
}
}
// Payment Service
class PaymentService {
// 注文作成イベントを購読
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
try {
const payment = await this.paymentGateway.charge(event.data.totalAmount);
await this.eventBus.publish("payment.completed", {
orderId: event.data.orderId,
paymentId: payment.id,
});
} catch (error) {
await this.eventBus.publish("payment.failed", {
orderId: event.data.orderId,
reason: error.message,
});
}
}
}
// Inventory Service
class InventoryService {
// 決済完了イベントを購読
async handlePaymentCompleted(event: PaymentCompletedEvent): Promise<void> {
try {
await this.reserveStock(event.data.orderId);
await this.eventBus.publish("stock.reserved", {
orderId: event.data.orderId,
});
} catch (error) {
await this.eventBus.publish("stock.insufficient", {
orderId: event.data.orderId,
reason: error.message,
});
// → Payment Serviceが返金イベントを発行
}
}
}
Choreographyの補償フロー
正常系:
order.created → payment.completed → stock.reserved → shipment.created
異常系(在庫不足):
order.created → payment.completed → stock.insufficient
│
payment.refunded ← Payment が返金
│
order.cancelled ← Order がキャンセル
Orchestration(オーケストレーション)
中央のOrchestrator(指揮者)がSagaの全体フローを管理します。
注文フロー(Orchestration):
[Order Saga Orchestrator]
│
1. createOrder()
│
2. charge()──→ [Payment Service]
│ │
3. reserve()──→ [Inventory Service]
│ │
4. ship()────→ [Shipping Service]
│
完了 or 補償開始
実装例
// Saga Orchestrator
class OrderSagaOrchestrator {
private steps: SagaStep[] = [];
constructor(
private orderService: OrderService,
private paymentService: PaymentService,
private inventoryService: InventoryService,
private shippingService: ShippingService
) {
this.steps = [
{
name: "createOrder",
execute: (ctx) => this.orderService.create(ctx.orderData),
compensate: (ctx) => this.orderService.cancel(ctx.orderId),
},
{
name: "chargePayment",
execute: (ctx) => this.paymentService.charge(ctx.orderId, ctx.amount),
compensate: (ctx) => this.paymentService.refund(ctx.paymentId),
},
{
name: "reserveStock",
execute: (ctx) => this.inventoryService.reserve(ctx.orderId, ctx.items),
compensate: (ctx) => this.inventoryService.release(ctx.orderId),
},
{
name: "createShipment",
execute: (ctx) => this.shippingService.create(ctx.orderId),
compensate: (ctx) => this.shippingService.cancel(ctx.shipmentId),
},
];
}
async execute(orderData: OrderData): Promise<SagaResult> {
const context: SagaContext = { orderData };
const completedSteps: SagaStep[] = [];
for (const step of this.steps) {
try {
const result = await step.execute(context);
Object.assign(context, result); // 結果をコンテキストに追加
completedSteps.push(step);
} catch (error) {
// 失敗 → 補償を逆順で実行
console.error(`Step "${step.name}" failed: ${error.message}`);
await this.compensate(completedSteps, context);
return { success: false, error: error.message };
}
}
return { success: true, orderId: context.orderId };
}
private async compensate(
completedSteps: SagaStep[],
context: SagaContext
): Promise<void> {
// 逆順で補償を実行
for (const step of completedSteps.reverse()) {
try {
await step.compensate(context);
} catch (error) {
// 補償の失敗はログに記録して手動対応
console.error(`Compensation "${step.name}" failed: ${error.message}`);
await this.alertOps(step.name, error);
}
}
}
}
Orchestratorの状態管理
// Sagaの状態を永続化
interface SagaState {
sagaId: string;
type: "ORDER_SAGA";
currentStep: number;
status: "IN_PROGRESS" | "COMPLETED" | "COMPENSATING" | "FAILED";
context: Record<string, unknown>;
steps: Array<{
name: string;
status: "PENDING" | "COMPLETED" | "COMPENSATED" | "FAILED";
result?: unknown;
}>;
createdAt: Date;
updatedAt: Date;
}
// 状態をDBに保存して、クラッシュ後も復旧可能に
比較
| 観点 | Choreography | Orchestration |
|---|---|---|
| 制御方式 | 分散(イベント駆動) | 集中(Orchestrator) |
| 結合度 | 低い(イベントのみ) | 中程度(Orchestratorが全サービスを知る) |
| フロー可視性 | 低い(イベントを追跡) | 高い(Orchestratorのコードに集約) |
| 複雑さの増加 | サービス追加時にイベント関係が複雑化 | Orchestratorのロジックが複雑化 |
| 単一障害点 | なし | Orchestratorがリスク |
| デバッグ | 困難 | 比較的容易 |
| 適切な場面 | 3〜5ステップの単純なフロー | 5ステップ以上の複雑なフロー |
選択のガイドライン
const selectionGuide = {
choreography: {
when: [
"ステップ数が少ない(3〜5)",
"各サービスチームが独立して開発",
"イベント駆動が既に導入済み",
"循環的な依存がない",
],
avoid: "ステップが多い、フロー変更が頻繁",
},
orchestration: {
when: [
"ステップ数が多い(5以上)",
"複雑な条件分岐がある",
"フローの可視性が重要",
"監視・デバッグの容易さが必要",
],
avoid: "サービス間の独立性を最大化したい場合",
},
};
まとめ
| ポイント | 内容 |
|---|---|
| Choreography | 自律分散、イベント駆動、疎結合 |
| Orchestration | 中央集権、フロー可視、デバッグ容易 |
| 選択基準 | ステップ数、複雑さ、チーム構成 |
| 共通の課題 | 補償トランザクションの設計 |
チェックリスト
- Choreographyの動作フローを説明できる
- Orchestrationの動作フローを説明できる
- 両者のメリット・デメリットを比較できる
- プロジェクトに応じた選択ができる
次のステップへ
次は両方のスタイルに共通する「補償トランザクション」を深掘りします。失敗時にどうやってシステムを整合状態に戻すかを学びましょう。
推定読了時間: 40分