トランザクショナルアウトボックス
データベースへの書き込みとイベント発行を原子的に行うための分散システムパターン
トランザクショナルアウトボックスとは
トランザクショナルアウトボックス (Transactional Outbox) は、データベースへのビジネスデータの書き込みとイベントの発行を原子的に行うパターンである。「注文を保存したがイベントの発行に失敗した」「イベントは発行したが注文の保存に失敗した」という不整合を防ぐ。
問題: 二重書き込み
注文を保存し、その事実を下流のサービスへ知らせる。この 2 つの処理は必ず対で現れる。
// ❌ 二重書き込み問題: DB 保存とイベント発行が別トランザクション
await db.orders.create(order); // 1. DB に保存 (成功)
await sns.publish({ Message: order }); // 2. イベント発行 (失敗したら?)
// → 注文は保存されたが、イベントが発行されない → 下流システムが同期されない
DB とメッセージングは別のシステムなので、片方だけが成立する瞬間が必ず存在する。発行の呼び出しが失敗する場合だけでなく、発行の直前にプロセスが落ちる場合も結果は同じである。順序を入れ替えて先にイベントを発行しても解決しない。今度は保存に失敗した注文について通知が飛ぶ。両者を 1 つの分散トランザクション (2 相コミット) で括る発想もあるが、マネージドなメッセージングサービスの多くは分散トランザクションに参加する仕組みを持たないため、選択肢に上がらないことが多い。
解決: Outbox テーブル
発想は単純で、イベントを外部へ送る代わりに、同じデータベースの中のテーブルへ「送るべきイベント」として書き込む。書き込み先が 1 つのデータベースに収まれば、あとは通常のトランザクションで原子性が得られる。
// ✅ 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(),
});
});
// → 注文と Outbox レコードが両方成功 or 両方失敗 (発行はこの後、非同期)
Outbox テーブルに書き込まれたイベントは、別のプロセス (ポーラー) が定期的に読み取って SNS/SQS に発行し、発行できたレコードに処理済みの印を付けるか削除する。ここで原子的になっているのはビジネスデータと Outbox レコードの書き込みであって、外部への発行は依然として非同期である。トランザクションが成功した時点で確定するのは「発行されたこと」ではなく「発行すべき事実がデータベースに残ったこと」で、そこから先はポーラーの再試行が届けきる。
アーキテクチャ
アプリケーションはビジネスデータと Outbox レコードを同一トランザクションで DB に書き込む。Outbox ポーラーまたは CDC (Change Data Capture) が Outbox テーブルの変更を検知し、メッセージングサービス (SNS、EventBridge) にイベントを発行する。トランザクションの原子性が守るのは DB への 2 つの書き込みで、そこから先の到達はポーラーや CDC の再試行が担う。この分担のため、イベントは遅れて届くことはあっても取り落とされない。
[アプリケーション]
↓ 同一トランザクション
[DB: orders テーブル] + [DB: outbox テーブル]
↓ ポーリング or CDC
[Outbox ポーラー / CDC]
↓
[SNS / SQS / EventBridge]
↓
[下流サービス]
DynamoDB での Outbox
DynamoDB はトランザクション (TransactWriteItems) で複数テーブルへの書き込みを原子的に行える。対象にできるのは同一アカウント・同一リージョンの最大 100 アイテムまでで、合計サイズは 4 MB を超えられない。
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 でイベントを発行する。ストリームのレコードは同じアイテムについては実際の変更順に並ぶため、1 つの注文に関するイベントが入れ替わることはない (別々のアイテム同士の前後関係は保証されない)。
上の例では Outbox のアイテムに ttl を付けて 1 日で消えるようにしている。テーブルを無限に太らせないための工夫だが、発行に失敗したまま期限が来ればイベントは黙って消える。未処理レコードの滞留時間を監視する仕組みを先に用意してから TTL を付ける、という順序を守りたい。
なお DynamoDB では Orders テーブル自身のストリームを読めば、Outbox を挟まずに変更を拾える。それでも Outbox を置くのは、外へ出したいドメインイベントの粒度や形が行の変更と一致しないときである。行の差分をそのまま流すと、下流がこちらのテーブル構造に依存してしまう。
CDC vs ポーリング
取り出し方は大きく 2 通りで、自分でテーブルを読みに行くか、DB が書いているトランザクションログ由来の変更通知に乗るかである。
| 方式 | 仕組み | 遅延 | 複雑さ |
|---|---|---|---|
| ポーリング | 定期的に Outbox テーブルをスキャン | 秒〜分 | 低い |
| CDC | DB のトランザクションログを読み取り | ミリ秒〜秒 | 中程度 |
| DynamoDB Streams | DynamoDB のネイティブ CDC | ミリ秒〜秒 | 低い |
DynamoDB Streams を使えば、ポーラーを自前で実装する必要がない。リレーショナルデータベースでは Debezium が同じ位置を占め、Outbox テーブルの行を宛先トピックへ振り分ける Outbox Event Router という変換も用意されている。
ポーリングを選ぶ場合、間隔を詰めるほど遅延は縮むが、その分だけ空振りのクエリが増える。ポーラーを複数並べて可用性を上げるなら、同じレコードを 2 つのプロセスが拾わないよう取得時の排他 (行ロックや条件付き更新) が要る。
イベントの重複配信
Outbox パターンでは、イベントが重複配信される可能性がある (at-least-once)。発行に成功した直後、処理済みの印を付ける前に落ちれば、同じレコードがもう一度発行される。この隙間を無くすことはできないので、下流のコンシューマーはべき等に作る。イベントに一意な ID を持たせ、処理済みの ID を記録して 2 回目以降を捨てるのが定石で、Outbox レコードの id をそのままイベント ID として運べばこの判定が素直に書ける。
トランザクショナルアウトボックスの関連書籍も参考になる。
この記事は役に立ちましたか?
関連用語
イベント駆動アーキテクチャ
イベントの発行と購読を中心にシステムを構成し、サービス間の疎結合と非同期処理を実現するアーキテクチャスタイル
変更データキャプチャ (CDC) とは - DB の変更をリアルタイム連携するパターン
変更データキャプチャ (CDC、Change Data Capture) はデータベースの INSERT/UPDATE/DELETE を検出して検索エンジンや分析基盤へリアルタイムに伝播させるデータ統合パターン。実装 4 方式と AWS での構成を解説
Outbox パターン
DB の変更とイベント発行を 1 つのトランザクションで保証するメッセージング信頼性パターン
Saga コレオグラフィ
各サービスがイベントを発行 / 購読し、中央のオーケストレーターなしで分散トランザクションを実現する方式
EventBridge
AWS のサーバーレスイベントバスで、イベント駆動アーキテクチャの中核を担う
DynamoDB Streams
DynamoDB テーブルの変更をリアルタイムでキャプチャし、Lambda で処理するイベントソース
関連する記事
技術書ランキングの決定版はどれか - IT エンジニア本大賞の歴代大賞と言及数データで読む
技術書ランキングを探している人向けに、検証可能な 2 つのデータ (IT エンジニア本大賞の歴代大賞 2014-2026 年と、当サイトの Qiita / Zenn 言及数ランキング) を整理。歴代受賞作の一覧表と、ランキングを選書に活かす方法を解説します。
障害対応の夜に思い出す、あの本の 1 ページ
本番障害の緊迫した場面で、過去に読んだ技術書の知識が助けてくれた経験はありませんか。「いつか役立つ」知識が「今この瞬間」に変わる読書の価値を考えます。
データベース本ガイド - SQL から設計まで学べる技術書の選び方
データベースの基礎から設計、パフォーマンスチューニングまで学べる技術書の選び方と学習順序を紹介します。