コンテンツにスキップ

面接問題:決済成功・予約失敗の分散トランザクション障害への対処戦略

問題

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 強整合性のトレードオフを説明できる
  • 返金失敗シナリオ(補償も失敗した場合)の対処まで言及すると高評価