面接問題:決済成功・予約失敗の分散トランザクション障害への対処戦略
問題¶
Payment succeeds, but booking fails. What's your recovery strategy? (決済は成功したが、予約の作成に失敗した。どう復旧するか?)
解答¶
主なアプローチは Saga パターン と 補償トランザクション(Compensating Transaction)。
1. 補償トランザクション(即時ロールバック)¶
// Saga の補償ロジック例
func PlaceOrder(ctx context.Context, req OrderRequest) error {
// Step 1: 決済
paymentID, err := paymentService.Charge(ctx, req.Amount)
if err != nil {
return fmt.Errorf("payment failed: %w", err)
}
// Step 2: 予約作成
_, err = bookingService.Create(ctx, req.BookingDetails)
if err != nil {
// 補償: 決済を返金する
if refundErr := paymentService.Refund(ctx, paymentID); refundErr != nil {
// 返金失敗 → Dead Letter Queue に積む(手動対処または再試行)
dlq.Publish(RefundJob{PaymentID: paymentID, Reason: err.Error()})
}
return fmt.Errorf("booking failed, payment refunded: %w", err)
}
return nil
}
2. アウトボックスパターン(Outbox Pattern)で信頼性を高める¶
[決済DB] [メッセージブローカー]
payments table
outbox table →→→→→→→ booking_requested event
↓
[予約サービス]
↓ 失敗したら
booking_failed event → 返金トリガー
アウトボックスにより、決済と「予約リクエスト発行」がアトミックになる。
3. ステータス管理と冪等性¶
PAYMENT_PENDING → PAYMENT_COMPLETED → BOOKING_PENDING → BOOKING_CONFIRMED
↘ BOOKING_FAILED → REFUND_PENDING → REFUNDED
リトライのために各操作は idempotency key を持つ。同じキーで2回呼んでも2回課金されない。
面接でのポイント¶
- 「まず問題の境界を確認する」:決済は冪等か?予約は非同期か?
- Saga の2種類を区別できると加点:Choreography(イベント駆動)vs Orchestration(中央コントローラー)
- 最終的整合性 vs 強整合性のトレードオフを説明できる
- 返金失敗シナリオ(補償も失敗した場合)の対処まで言及すると高評価