📤
概念 #進化的アーキテクチャ #分散システム #Outbox Pattern #イベント駆動 #データ整合性 📚 進化的アーキテクチャ

Outbox Pattern:トランザクション境界を越えた安全なイベント発行

DB 書き込みとメッセージ発行を同一トランザクションに収め、部分失敗による不整合を防ぐパターン。Prisma 実装例と Dual Write との使い分けを整理する

問題:DB 書き込み成功→メッセージ発行失敗の不整合

イベント駆動アーキテクチャでは「DB への書き込み」と「メッセージキューへの発行」を一緒に行う操作がよく発生する。

// 問題のあるコード:DB と MQ の操作が分離している
async function createOrder(command: CreateOrderCommand): Promise<void> {
  await this.db.orders.create({ data: mapToRow(command) }); // ① DB 書き込み成功
  await this.mq.publish('order.created', command);           // ② MQ 発行 → 失敗!

  // ①が成功して②が失敗した場合:
  // DB にはレコードが存在するが、下流サービスはイベントを受け取っていない
  // → 在庫・通知・請求が動かない(サイレントな不整合)
}

この「部分失敗」は Dual Write の最大の弱点だ。Outbox Pattern はこれを解決する。

Outbox Pattern の仕組み

DB への書き込みと「送るべきメッセージの記録」を同一トランザクションで行う。別プロセス(Outbox Poller)がこの記録を読み、MQ へ発行する。

┌─────────────────────────────────────────┐
│ Application(同一 TX 内で完結)           │
│  ┌─────────────┐    ┌───────────────┐   │
│  │  orders テーブル│    │ outbox テーブル│   │
│  │  INSERT     ├───→│  INSERT       │   │
│  └─────────────┘    └───────────────┘   │
│                                         │
│   COMMIT(原子的:両方成功 or 両方失敗) │
└─────────────────────────────────────────┘
             ↓ (非同期)
   ┌──────────────────┐
   │  Outbox Poller   │ ← outbox テーブルをポーリング
   └────────┬─────────┘

   ┌──────────────────┐
   │  Message Queue   │ ← MQ に発行(成功したら outbox レコードを削除/処理済みにする)
   └──────────────────┘

   下流サービス(在庫・通知・請求)

DB への書き込みと outbox への記録が同一 TX に収まっているため、どちらかだけ成功するという状態が起きない。

TypeScript + Prisma 実装例

outbox テーブルのスキーマ

CREATE TABLE outbox (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  event_type  VARCHAR(255) NOT NULL,
  payload     JSONB        NOT NULL,
  created_at  TIMESTAMP    NOT NULL DEFAULT NOW(),
  processed_at TIMESTAMP   -- NULL = 未処理、non-NULL = 処理済み
);

ビジネスロジック側(TX 内で outbox に書く)

class OrderService {
  async createOrder(command: CreateOrderCommand): Promise<void> {
    // 同一トランザクション内で orders と outbox を両方 INSERT する
    await this.prisma.$transaction(async (tx) => {
      // ① ドメインロジック
      const order = Order.create(command);

      // ② DB へ書き込み
      await tx.orders.create({ data: OrderMapper.toRow(order) });

      // ③ 送るべきイベントを outbox に記録(MQ への発行はまだしない)
      await tx.outbox.create({
        data: {
          eventType: 'order.created',
          payload: JSON.stringify({
            orderId: order.id,
            userId: order.userId,
            totalAmount: order.totalAmount,
          }),
        },
      });
      // COMMIT: ①②③が全て成功するか、全て失敗する
    });
  }
}

Outbox Poller(非同期で MQ へ発行)

class OutboxPoller {
  // 定期実行(例: 1秒ごと)
  async poll(): Promise<void> {
    // 未処理のイベントを取得
    const pending = await this.prisma.outbox.findMany({
      where: { processedAt: null },
      orderBy: { createdAt: 'asc' },
      take: 100, // バッチサイズ
    });

    for (const event of pending) {
      try {
        // MQ へ発行
        await this.mq.publish(event.eventType, JSON.parse(event.payload));

        // 処理済みにマーク
        await this.prisma.outbox.update({
          where: { id: event.id },
          data: { processedAt: new Date() },
        });
      } catch (error) {
        // 失敗したら次のポーリングでリトライされる
        console.error(`outbox 発行失敗: ${event.id}`, error);
      }
    }
  }
}

At-Least-Once 配信と冪等性

Outbox Pattern は 少なくとも一度(At-Least-Once) の配信を保証する。ポーラーの障害で同じイベントが重複して発行される可能性があるため、受信側は冪等に設計する必要がある。

// 冪等な受信側の例:同じ event_id が来ても安全に処理できる
class InventoryEventHandler {
  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
    const alreadyProcessed = await this.processedEvents.exists(event.id);
    if (alreadyProcessed) return; // 重複は無視

    await this.inventory.reserve(event.orderId, event.items);
    await this.processedEvents.markAsProcessed(event.id);
  }
}

Dual Write との使い分け

観点Dual WriteOutbox Pattern
部分失敗のリスク高い(DB 成功・MQ 失敗が起きる)ない(同一 TX で原子的)
実装の複雑さ低い中(outbox テーブル + ポーラーが必要)
配信保証なしAt-Least-Once
冪等性の必要性場合による受信側に必要
適用場面一時的な移行・整合性が緩い場合イベント駆動で整合性が重要な場合

関連概念

出典・参考文献

  • Chris Richardson, Microservices Patterns (2018) Chapter 3 “Managing transactions with sagas”
  • Gunnar Morling, “Reliable Microservices Data Exchange With the Outbox Pattern” — debezium.io

出典: Microservices Patterns(Chris Richardson)/ Enterprise Integration Patterns