トランザクショナルアウトボックス

データベースへの書き込みとイベント発行を原子的に行うための分散システムパターン

分散システムイベント駆動

トランザクショナルアウトボックスとは

トランザクショナルアウトボックス (Transactional Outbox) は、データベースへのビジネスデータの書き込みとイベントの発行を原子的に行うパターンである。「注文を保存したがイベントの発行に失敗した」「イベントは発行したが注文の保存に失敗した」という不整合を防ぐ。

問題: 二重書き込み

// ❌ 二重書き込み問題: DB 保存とイベント発行が別トランザクション
await db.orders.create(order);        // 1. DB に保存 (成功)
await sns.publish({ Message: order }); // 2. イベント発行 (失敗したら?)
// → 注文は保存されたが、イベントが発行されない → 下流システムが同期されない

DB 保存とメッセージ発行は異なるシステムのため、片方だけ成功する可能性がある。

解決: Outbox テーブル

// ✅ Outbox パターン: 同一トランザクションで DB 保存 + Outbox 書き込み
await db.transaction(async (tx) => {
  await tx.orders.create(order);
  await tx.outbox.create({
    id: uuid(),
    aggregateType: 'Order',
    eventType: 'OrderCreated',
    payload: JSON.stringify(order),
    createdAt: new Date(),
  });
});
// → 両方成功 or 両方失敗 (原子性が保証される)

Outbox テーブルに書き込まれたイベントは、別のプロセス (ポーラー) が定期的に読み取り、SNS/SQS に発行する。

アーキテクチャ

[アプリケーション]
  ↓ 同一トランザクション
[DB: orders テーブル] + [DB: outbox テーブル]
                              ↓ ポーリング or CDC
                        [Outbox ポーラー / CDC][SNS / SQS / EventBridge][下流サービス]

DynamoDB での Outbox

DynamoDB はトランザクション (TransactWriteItems) で複数テーブルへの書き込みを原子的に行える。

await ddb.send(new TransactWriteCommand({
  TransactItems: [
    { Put: { TableName: 'Orders', Item: order } },
    { Put: { TableName: 'Outbox', Item: {
      id: uuid(),
      eventType: 'OrderCreated',
      payload: JSON.stringify(order),
      ttl: Math.floor(Date.now() / 1000) + 86400,
    }}},
  ],
}));

DynamoDB Streams で Outbox テーブルの変更を検知し、Lambda でイベントを発行する。

CDC vs ポーリング

方式 仕組み 遅延 複雑さ
ポーリング 定期的に Outbox テーブルをスキャン 秒〜分 低い
CDC DB のトランザクションログを読み取り ミリ秒〜秒 中程度
DynamoDB Streams DynamoDB のネイティブ CDC ミリ秒〜秒 低い

DynamoDB Streams を使えば、ポーラーを自前で実装する必要がない。

イベントの重複配信

Outbox パターンでは、イベントが重複配信される可能性がある (at-least-once)。下流のコンシューマーはべき等に実装する必要がある。

トランザクショナルアウトボックスの関連書籍も参考になる。

関連用語