Outbox パターン
DB の変更とイベント発行を 1 つのトランザクションで保証するメッセージング信頼性パターン
Outbox パターンとは
Outbox パターンは、DB の変更とイベント発行を 1 つのトランザクションで保証するパターンである。DB に書き込んだがイベントが発行されない (またはその逆) という不整合を防ぐ。同じパターンを トランザクショナルアウトボックス とも呼び、CDC やポーリングでの取り出し方、Outbox レコードの後始末といった実装寄りの論点はそちらに詳しく書いてある。ここでは何が防げて何が防げないのかに絞る。
問題: 二重書き込み
メッセージブローカーは DB のトランザクションに参加しない。1 回のコミットで DB の行とブローカーへの送信を同時に確定させる手段がないため、どちらを先に置いても間に窓が残る。DB を先に書けば発行漏れになり、発行を先にすればコミット失敗後に実体のないイベントが下流へ流れる。
❌ DB 書き込み + イベント発行が別トランザクション:
1. DynamoDB に注文を保存 → 成功
2. EventBridge にイベント発行 → 失敗
→ 注文は保存されたがイベントが失われる
Outbox パターンの仕組み
DB への書き込みとイベント発行を原子的に行うため、ビジネスデータと Outbox レコードを同一トランザクション (DynamoDB の TransactWrite) で書き込む。その後、DynamoDB Streams が Outbox の変更を検知し、Lambda がイベントを EventBridge に発行する。DB 書き込みが成功した変更は、発行待ちのレコードとして DB に残るため、遅れても取りこぼされない。
1. DynamoDB に注文 + Outbox レコードを TransactWrite
2. DynamoDB Streams が Outbox の変更を検知
3. Lambda が Outbox レコードを読み取り、EventBridge に発行
→ DB 書き込みとイベント発行が原子的
効いているのは、発行すべきイベントを外部への送信から DB の中の行へ置き換えた点である。トランザクションに参加できない相手 (ブローカー) を境界の外へ追い出すと、残る 2 つの操作はどちらも同じ DB への書き込みになり、1 つのコミットに収まる。送信の責任は後段の別プロセスへ移り、そこが落ちても未発行のレコードが DB に残っているので再試行できる。原子性が保証されるのは「業務データの変更」と「発行の予定」の間であって、発行そのものが 1 回で済むという意味ではない。
DynamoDB での実装
DynamoDB では TransactWriteItems が業務データと Outbox レコードをまとめて書く役を担う。対象にできるのは同一アカウント・同一リージョンの複数テーブルで、1 回に最大 100 の書き込みアクション・合計 4 MB までという制限がある。同じ項目を 1 つのトランザクション内で 2 回操作すると検証エラーになるので、Outbox のキーは業務データのキーと衝突しない形で組む。
await db.transactWrite({
TransactItems: [
{ Put: { TableName: 'orders', Item: { PK: `ORDER#${id}`, ...order } } },
{ Put: { TableName: 'outbox', Item: {
PK: `OUTBOX#${id}`,
eventType: 'OrderCreated',
payload: JSON.stringify(order),
createdAt: Date.now(),
} } },
],
});
// 両方が成功 or 両方が失敗 (原子性)
Outbox vs 直接イベント発行
直接発行との差は、失敗した瞬間に何が残るかに現れる。
| 観点 | 直接発行 | Outbox |
|---|---|---|
| 原子性 | 担保できない (DB とブローカーへの二重書き込みになる) | 担保できる (1 トランザクションに収まる) |
| 複雑さ | 低い | 中 |
| 遅延 | 低い | やや高い (Streams 経由) |
| 信頼性 | 低い | 高い |
いつ使うか
判断は、イベントが 1 通失われたときに後から気づいて直せるかどうかに集約される。
| 場面 | 判断 | 理由 |
|---|---|---|
| イベントが 1 件でも失われると業務が食い違う | Outbox にする | 書き込みと発行を同一トランザクションに載せるため、DB に残った変更に対応するイベントが必ず後続処理へ渡る |
| 入金や注文のように後追いでの復旧が難しい | Outbox にする | 直接発行では DB 側だけ成功した状態が残り、記録とイベントのどちらが正しいかを人手で突き合わせることになる |
| 通知や集計の更新で、遅れても最終的に揃えばよい | 直接発行にリトライを添える | Streams を経由する分の遅延と Outbox の運用を負う価値が薄く、失敗時に再送すれば実害が収まる |
保証されないこと
Outbox を入れても配信が 1 回だけになるわけではない。DynamoDB Streams のレコード自体はストリーム内に 1 度しか現れないが、それを読む Lambda のイベントソースマッピングは各イベントを少なくとも 1 回処理する仕様で、公式ドキュメントも重複処理が起こり得るとして関数を冪等に作るよう明記している。発行に成功した直後、処理済みの印を付ける前に落ちれば同じイベントがもう一度出る。したがって下流のコンシューマー側で一意な ID による重複排除が必須になる (冪等性)。
順序も部分的な保証にとどまる。ストリームのレコードは同じ項目の変更については実際の順に並ぶが、別々の項目の前後関係は保証されない。注文 A と注文 B のイベントが入れ替わって届く前提で下流を設計する。
Outbox テーブルを放置すれば無限に太るため、発行済みレコードの削除や TTL を併せて設計する必要がある。ここで急ぐと、発行に失敗したまま期限切れでレコードが消えて事故が静かに埋まる。滞留しているレコードの件数と最古の作成時刻を監視する方が先だ。
この記事は役に立ちましたか?
関連用語
イベントソーシング
状態の変更をイベントとして記録し、イベントの再生で現在の状態を復元する設計パターン
変更データキャプチャ (CDC) とは - DB の変更をリアルタイム連携するパターン
変更データキャプチャ (CDC、Change Data Capture) はデータベースの INSERT/UPDATE/DELETE を検出して検索エンジンや分析基盤へリアルタイムに伝播させるデータ統合パターン。実装 4 方式と AWS での構成を解説
冪等性
同じ操作を何度実行しても結果が変わらない性質で、分散システムの信頼性を支える
トランザクショナルアウトボックス
データベースへの書き込みとイベント発行を原子的に行うための分散システムパターン
Saga コレオグラフィ
各サービスがイベントを発行 / 購読し、中央のオーケストレーターなしで分散トランザクションを実現する方式
イベント駆動アーキテクチャ
イベントの発行と購読を中心にシステムを構成し、サービス間の疎結合と非同期処理を実現するアーキテクチャスタイル
関連する記事
技術書の読む順番戦略 - 複数冊を組み合わせて理解を加速させる
技術書を 1 冊ずつ読むのではなく、複数冊を戦略的に組み合わせることで、1 冊では届かない理解の深さと広さに達する方法を解説します。
技術書ランキングの決定版はどれか - IT エンジニア本大賞の歴代大賞と言及数データで読む
技術書ランキングを探している人向けに、検証可能な 2 つのデータ (IT エンジニア本大賞の歴代大賞 2014-2026 年と、当サイトの Qiita / Zenn 言及数ランキング) を整理。歴代受賞作の一覧表と、ランキングを選書に活かす方法を解説します。
技術書の知識を定着させる間隔反復法 - 読んだのに忘れる問題を解決する
技術書を読んでも内容を忘れてしまう原因を認知科学の観点から分析し、間隔反復法を使って知識を長期記憶に定着させる具体的な方法を紹介します。