EXERCISE 60分

ストーリー

高橋アーキテクト が新しい課題を出しました。

高橋アーキテクト
EC サイトの注文フローをイベント駆動で設計してほしい。メッセージブローカーの選定、イベントスキーマの定義、冪等性の設計まで、すべて考えてみよう

ミッション概要

項目内容
題材ECサイトの注文フロー(注文 → 決済 → 在庫確保 → 配送 → 通知)
目標イベント駆動アーキテクチャの設計
所要時間60分
成果物イベントフロー図、スキーマ定義、冪等性設計

ミッション 1: イベントフローの設計(15分)

以下の注文フローをイベント駆動で設計してください。

フロー:

  1. ユーザーが注文を確定
  2. 決済を処理
  3. 在庫を確保
  4. 配送を手配
  5. ユーザーに通知

設計すべきこと:

  • 発行されるイベントの一覧
  • 各サービスが発行/購読するイベント
  • 同期/非同期の判断
ヒントと模範回答
イベントフロー:

[Order Service]
  │ publish: order.created

  ├──→ [Payment Service]
  │     subscribe: order.created
  │     publish: payment.completed / payment.failed

  ├──→ [Inventory Service]
  │     subscribe: payment.completed
  │     publish: stock.reserved / stock.insufficient

  ├──→ [Shipping Service]
  │     subscribe: stock.reserved
  │     publish: shipment.created

  └──→ [Notification Service]
        subscribe: order.created, payment.completed,
                   stock.reserved, shipment.created

イベント一覧:
  1. order.created       → 注文が作成された
  2. payment.completed   → 決済が完了した
  3. payment.failed      → 決済が失敗した
  4. stock.reserved      → 在庫が確保された
  5. stock.insufficient  → 在庫が不足
  6. shipment.created    → 配送が手配された

判断: 注文確定後のフロー全体を非同期イベントで連携
      (ユーザーには「注文受付済み」を即座に返す)

ミッション 2: メッセージブローカーの選定(10分)

このシステムに最適なメッセージブローカーを選定し、理由を述べてください。

考慮事項:

  • 注文の順序保証が必要
  • 障害発生時にイベントを再処理したい
  • 将来的にリアルタイム分析を追加予定
ヒントと模範回答
選定: Apache Kafka

理由:
1. 順序保証: orderId をパーティションキーにすれば、
   同一注文のイベントの順序が保証される

2. 再処理: Kafka はメッセージを保持するため、
   offset を巻き戻して再処理が可能

3. リアルタイム分析: Kafka Streams や ksqlDB で
   ストリーム処理を後から追加できる

4. 複数Consumer Group: 決済、在庫、通知の各サービスが
   独立して同じイベントを読める

Topic設計:
  - order-events (partitions: 12, key: orderId)
  - payment-events (partitions: 6, key: orderId)
  - inventory-events (partitions: 6, key: productId)
  - shipment-events (partitions: 6, key: orderId)

ミッション 3: イベントスキーマの定義(15分)

以下の3つのイベントについて、CloudEvents形式でスキーマを定義してください。

  1. order.created
  2. payment.completed
  3. stock.reserved
ヒントと模範回答
// 1. order.created
interface OrderCreatedEvent {
  specversion: "1.0";
  id: string;
  source: "/order-service";
  type: "com.example.order.created";
  time: string;
  datacontenttype: "application/json";
  correlationid: string;
  data: {
    orderId: string;
    userId: string;
    items: Array<{
      productId: string;
      quantity: number;
      unitPrice: number;
    }>;
    totalAmount: number;
    currency: string;
    shippingAddress: {
      prefecture: string;
      city: string;
      line1: string;
      postalCode: string;
    };
  };
}

// 2. payment.completed
interface PaymentCompletedEvent {
  specversion: "1.0";
  id: string;
  source: "/payment-service";
  type: "com.example.payment.completed";
  time: string;
  correlationid: string;
  causationid: string;  // order.created のID
  data: {
    paymentId: string;
    orderId: string;
    amount: number;
    currency: string;
    method: "credit_card" | "bank_transfer" | "convenience_store";
    transactionId: string;
  };
}

// 3. stock.reserved
interface StockReservedEvent {
  specversion: "1.0";
  id: string;
  source: "/inventory-service";
  type: "com.example.stock.reserved";
  time: string;
  correlationid: string;
  causationid: string;  // payment.completed のID
  data: {
    reservationId: string;
    orderId: string;
    items: Array<{
      productId: string;
      quantity: number;
      warehouseId: string;
    }>;
    expiresAt: string;  // 予約の有効期限
  };
}

ミッション 4: 冪等性の設計(15分)

以下の各サービスで冪等性を確保する方法を設計してください。

  1. Payment Service: 同じ注文の二重決済を防ぐ
  2. Inventory Service: 同じ注文の二重在庫確保を防ぐ
  3. Notification Service: 同じ通知の二重送信を防ぐ
ヒントと模範回答
// 1. Payment Service: orderIdをキーにした冪等性
class PaymentEventHandler {
  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
    const orderId = event.data.orderId;

    // orderIdで既に決済済みか確認
    const existingPayment = await this.paymentRepo.findByOrderId(orderId);
    if (existingPayment) {
      console.log(`Payment already processed for order: ${orderId}`);
      return; // 二重決済を防止
    }

    // 決済処理を実行
    const payment = await this.paymentGateway.charge({
      amount: event.data.totalAmount,
      idempotencyKey: `pay-${orderId}`, // 決済ゲートウェイにも冪等キー
    });

    await this.paymentRepo.save({ orderId, ...payment });
  }
}

// 2. Inventory Service: eventIdによるデデュプリケーション
class InventoryEventHandler {
  async handlePaymentCompleted(event: PaymentCompletedEvent): Promise<void> {
    // イベントIDで重複チェック
    if (await this.processedEvents.has(event.id)) return;

    await this.db.transaction(async (tx) => {
      for (const item of event.data.items) {
        await tx.query(
          `UPDATE inventory
           SET reserved = reserved + $1
           WHERE product_id = $2 AND available >= $1`,
          [item.quantity, item.productId]
        );
      }
      // 処理済みイベントを記録(同一トランザクション内)
      await tx.query(
        `INSERT INTO processed_events (event_id) VALUES ($1) ON CONFLICT DO NOTHING`,
        [event.id]
      );
    });
  }
}

// 3. Notification Service: 通知IDによる重複チェック
class NotificationEventHandler {
  async handleEvent(event: DomainEvent): Promise<void> {
    const notificationKey = `${event.type}:${event.data.orderId}`;

    // 送信済みか確認
    if (await this.notificationLog.exists(notificationKey)) return;

    await this.sendNotification(event);
    await this.notificationLog.record(notificationKey);
  }
}

達成チェックリスト

  • イベントフロー図を設計できた
  • メッセージブローカーを理由とともに選定できた
  • 3つのイベントスキーマをCloudEvents形式で定義できた
  • 各サービスの冪等性設計ができた

推定所要時間: 60分