📤
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 Write | Outbox Pattern |
|---|---|---|
| 部分失敗のリスク | 高い(DB 成功・MQ 失敗が起きる) | ない(同一 TX で原子的) |
| 実装の複雑さ | 低い | 中(outbox テーブル + ポーラーが必要) |
| 配信保証 | なし | At-Least-Once |
| 冪等性の必要性 | 場合による | 受信側に必要 |
| 適用場面 | 一時的な移行・整合性が緩い場合 | イベント駆動で整合性が重要な場合 |
関連概念
- → Dual Write(シンプルな代替と落とし穴)
- → CDC(Change Data Capture)(アプリを汚さない代替)
- → 適切な結合(サービス間の結合度設計)
出典・参考文献
- Chris Richardson, Microservices Patterns (2018) Chapter 3 “Managing transactions with sagas”
- Gunnar Morling, “Reliable Microservices Data Exchange With the Outbox Pattern” — debezium.io
- 1. 🧬進化的アーキテクチャとは:定義・誕生背景・3原則
- 2. 📐適応度関数(Fitness Functions):アーキテクチャ特性を自動検証する
- 3. 🔗適切な結合(Appropriate Coupling):Connascence と分散モノリスの罠
- 4. 🚀段階的変更(Incremental Change):Feature Toggle・Blue-Green・Canary・Dark Launch
- 5. 🔄Expand-Contract パターン:ゼロダウンタイムでスキーマ変更する
- 6. ✍️Dual Write:マイクロサービス分割時のデータ整合性パターンと落とし穴
- 7. 📤Outbox Pattern:トランザクション境界を越えた安全なイベント発行
- 8. 📡CDC(Change Data Capture):アプリを汚さずに変更を伝播する
- 9. 🐚Unix 哲学を現代アーキテクチャへ翻訳する
- 10. 🧅クリーンアーキテクチャが進化耐性をもたらす理由
- 11. 🏢Conway's Law:組織構造がアーキテクチャを決める
出典: Microservices Patterns(Chris Richardson)/ Enterprise Integration Patterns