Goイベント駆動アーキテクチャ実践:Outboxパターンと信頼性の高いトランザクションメッセージング
概要
- イベント駆動アーキテクチャ(EDA)は2026年のマイクロサービス疎結合の主流アプローチ
- Outboxパターンはビジネスデータとメッセージを同一トランザクションで書き込み、デュアルライト一貫性問題を解決
- At-Least-Once配信 + コンシューマー冪等性 = 本番級メッセージ信頼性
- Go + Kafka + PostgreSQLは2026年EDAの黄金スタック
- 原理からGo実装までの完全ソリューション。Outbox Relayとデッドレターキュー処理を含む
目次
- なぜイベント駆動アーキテクチャか
- デュアルライト問題:EDAの最大の課題
- Outboxパターンの原理と実装
- Go Outbox Relay完全実装
- Kafkaコンシューマーと冪等処理
- イベントスキーマ設計とバージョン管理
- 本番デプロイと監視
- 面接ポイントとアーキテクチャ選定
- まとめと関連記事
なぜイベント駆動アーキテクチャか
| 課題 | 同期呼び出し | イベント駆動 |
|---|---|---|
| サービス結合 | 強結合、AはBの応答を待つ | 疎結合、Aはイベント発行のみ |
| 可用性 | 下流障害で全体障害 | 下流復旧後にバックログを消費 |
| 拡張性 | 新コンシューマーは呼び出し元変更 | 新コンシューマーはTopic購読のみ |
| ピーク処理 | 同期ブロック、雪崩リスク | 非同期バッファリングでピーク平準化 |
適したシナリオ:注文作成→在庫/物流/ポイント/SMS通知(1書き込みN読み取り)、ユーザー登録、決済完了、CDC駆動の検索インデックス更新。
不向きなシナリオ:即時応答が必要なクエリ、強一貫性が必要な金融送金。
デュアルライト問題:EDAの最大の課題
注文サービスは:(1) PostgreSQLに書き込み、(2) Kafkaメッセージ送信が必要。
ステップ1成功・ステップ2失敗 → 注文は作成されたが在庫は不明 → オーバーセル!
| ソリューション | 一貫性 | 複雑度 | 推奨度 |
|---|---|---|---|
| Outbox(ローカルメッセージテーブル) | 強一貫 | 中 | ★★★★★ |
| 分散トランザクション(2PC/XA) | 強一貫 | 高 | ★★(性能差) |
| Sagaパターン | 結果整合 | 高 | ★★★★(長トランザクション) |
| メッセージ先→DB後 | 不整合 | 低 | ★(非推奨) |
Outboxパターンがほとんどのシナリオで最適解。
Outboxパターンの原理と実装
「送信するメッセージ」をレコードとして、ビジネスデータと同一DBトランザクションで書き込む。独立したRelayプロセスがOutboxテーブルを非同期読み取り、Kafkaに配信。
CREATE TABLE outbox_events (
id BIGSERIAL PRIMARY KEY,
aggregate_type VARCHAR(64) NOT NULL,
aggregate_id VARCHAR(64) NOT NULL,
event_type VARCHAR(128) NOT NULL,
payload JSONB NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
published_at TIMESTAMPTZ,
retry_count INT NOT NULL DEFAULT 0
);
CREATE INDEX idx_outbox_pending ON outbox_events (created_at)
WHERE status = 'pending';
func (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) (*Order, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
order := &Order{ID: uuid.New().String(), UserID: req.UserID, Amount: req.Amount, Status: "created"}
_, err = tx.ExecContext(ctx,
`INSERT INTO orders (id, user_id, amount, status) VALUES ($1, $2, $3, $4)`,
order.ID, order.UserID, order.Amount, order.Status,
)
if err != nil {
return nil, err
}
eventPayload, _ := json.Marshal(map[string]interface{}{
"order_id": order.ID, "user_id": order.UserID, "amount": order.Amount, "items": req.Items,
})
_, err = tx.ExecContext(ctx,
`INSERT INTO outbox_events (aggregate_type, aggregate_id, event_type, payload)
VALUES ($1, $2, $3, $4)`,
"order", order.ID, "order.created", eventPayload,
)
if err != nil {
return nil, err
}
return order, tx.Commit()
}
注文とイベントは同一トランザクションでコミット — 両方成功か両方失敗。
Go Outbox Relay完全実装
FOR UPDATE SKIP LOCKED:複数Relayインスタンスが同じレコードを奪い合わない。PostgreSQLが他トランザクションでロックされた行をスキップ。
aggregate_idでパーティション分割し、同一注文のイベント順序を保証。
失敗時:retry_countが閾値超過 → failedマーク → デッドレターキュー。
Kafkaコンシューマーと冪等処理
At-Least-Once配信ではコンシューマー側の冪等性が必須:
func (h *InventoryHandler) HandleOrderCreated(ctx context.Context, msg *kafka.Message) error {
eventID := getHeader(msg, "event_id")
var processed bool
h.db.QueryRowContext(ctx,
`SELECT EXISTS(SELECT 1 FROM processed_events WHERE event_id = $1)`, eventID,
).Scan(&processed)
if processed {
return nil
}
tx, _ := h.db.BeginTx(ctx, nil)
defer tx.Rollback()
// ビジネスロジック + processed_events INSERT
return tx.Commit()
}
イベントスキーマ設計とバージョン管理
CloudEvents標準フォーマットを使用。後方互換スキーマ進化戦略:
| 戦略 | 説明 | 適用シナリオ |
|---|---|---|
| 後方互換 | 新フィールドはoptional | ほとんどのシナリオ |
| 二重書き込み移行 | v1とv2イベントを同時発行 | 重大なスキーマ変更 |
| コンシューマーバージョンルーティング | 異なるコンシューマーグループが異なるバージョンTopicを購読 | マルチチーム独立進化 |
本番デプロイと監視
| メトリクス | アラート閾値 | 意味 |
|---|---|---|
| outbox_pending_count | 5分間>1000 | Relayが追いつかない |
| outbox_oldest_pending_age | >60秒 | メッセージ遅延過大 |
| outbox_failed_count | >0 | 配信失敗イベントあり |
| relay_publish_latency_p99 | >5秒 | Relay性能低下 |
面接ポイントとアーキテクチャ選定
Q1:OutboxとCDCの違いは?
Outboxはアプリケーション層でビジネスセマンティクス(order.created)を表現。CDCはDB層で行レベル変更(INSERT/UPDATE)をキャプチャ。複雑なビジネスはOutbox、シンプルなデータ同期はCDC。
Q2:Relayがダウンしたら?
Outboxテーブルのpendingイベントは失われない。Relay復旧後に配信再開。長時間ダウン時は遅延が発生するがメッセージは失われない。
Q3:Outboxテーブルは無限に増える?
増える。publishedイベントは定期アーカイブまたは削除。7日保持推奨、日付パーティションテーブル併用。
Q4:イベント駆動とSagaの選び方は?
イベント駆動は1書き込みN読み取りの通知シナリオ。Sagaは注文→決済→発送→確認のような長いクロスサービストランザクション(任意ステップ失敗時に補償)。両方組み合わせ可能。
Outbox vs CDC(Debezium)の選び方
Outbox はビジネスセマンティクス、CDC はゼロ侵入だがセマンティクスが弱い。
Saga パターンと Outbox の組み合わせ
オーケストレーション型 Saga で補償ロジック、Outbox イベントで下流に非同期通知。
DLQ とフルチェーン可観測性
リトライ超過で DLQ、Kafka Header で traceparent 伝播。
2026年トレンド
Outbox スターター標準化、Event Sourcing 復活、CloudEvents 標準化、イベント駆動 + AI。
まとめと関連記事
イベント駆動アーキテクチャの核心課題はメッセージ信頼性。Outboxパターンは「同一トランザクション書き込み」でデュアルライト一貫性問題をエレガントに解決。
設計要点:
- デュアルライト一貫性はEDA最大の課題、Outboxが最適解
- ビジネスデータとイベントは同一DBトランザクションでコミット
- RelayはFOR UPDATE SKIP LOCKEDでマルチインスタンス対応
- コンシューマーは冪等必須:processed_eventsテーブル + ビジネス一意制約
- イベントスキーマはCloudEvents標準、後方互換で進化
関連記事:
参考資料:
ブラウザローカルツールを無料で試す →