
Summary
この文書の要点
- Webhookは重複して届く前提で処理する。
- 署名検証とイベントIDの保存を最初に行う。
- イベント受信、署名検証、重複判定、業務反映、再処理を別の状態として管理する。
- 決済状態はWebhook順序ではなく、外部側の現在状態を再取得して確定する。
どこが設計の難所か
決済完了後のリダイレクトだけで注文や契約を確定すると、ブラウザを閉じた場合や通信失敗時に状態がずれます。一方でWebhookは同じイベントが複数回届いたり、イベント順序が期待通りでないことがあります。
決済は金額、提供権限、請求状態に直結します。処理の二重実行や取り消し漏れは大きな問題になります。また、Webhook受信時に外部APIやメール送信まで同期実行すると、失敗時の扱いが難しくなります。
Stripe Webhookは決済連携の重要な入口ですが、同じイベントが複数回届いたり、順序が前後したり、処理途中でアプリ側だけ失敗したりします。単純にイベントを受け取って即DB更新すると、二重処理や状態巻き戻りが起きます。
境界をどう切るか
Webhook受信では、署名を検証し、イベントIDを保存し、未処理なら内部イベントとしてキューに積みます。決済状態の更新は、現在状態と受信イベントから許可される遷移だけを実行します。副作用はジョブに分けます。
設計では、Webhookを業務処理そのものではなく、外部イベントの受信記録として扱います。まずevent_idで一意に保存し、署名検証とイベント種別を確認し、その後に冪等なユースケースへ渡します。
実装で効く細部
DBには外部イベントテーブルと内部状態テーブルを分けて持ちます。イベントIDにユニーク制約を置き、重複受信時は成功として返します。状態更新では、決済待ち、確定、失敗、返金、取消などの遷移を明示します。
PostgreSQLには`webhook_events`テーブルを置き、event_id、type、payload_hash、received_at、processed_at、status、errorを保存します。業務反映側では、支払いIDやサブスクリプションIDごとに現在状態を確認し、すでに反映済みなら何もしないようにします。
- event_idにユニーク制約を置き、重複受信は成功扱いで返せるようにする。
- HTTP応答は短く返し、重い処理はキューへ渡して再試行できるようにする。
- 状態更新時は外部APIで現在状態を確認し、古いイベントで巻き戻さない。
壊れ方を観測する
検証では、同一Webhookの複数回送信、順序逆転、署名不正、処理途中失敗、ジョブ再実行をテストします。Stripe CLIを使う場合も、実運用で起きる再送を想定したテストデータを用意します。
検証では、同一イベントの二重送信、順序逆転、処理途中例外、外部APIタイムアウトを再現します。テスト用イベントだけでなく、保存済みpayloadを使った再処理コマンドを持つと、障害時の復旧が現実的になります。
捨てた選択肢とトレードオフ
冪等性を厳密に扱うとテーブルや状態遷移が増えます。単発の簡易決済では重く見えるかもしれません。しかし、サブスクリプションや権限付与が絡む場合は、最初からイベント駆動で考える方が安全です。
すべてを同期処理にすると実装は単純ですが、タイムアウトや再送に弱くなります。キュー化すると状態管理は増えますが、再試行と調査がしやすくなります。決済では実装の短さより、二重反映しない構造を優先すべきです。
現場に残す判断軸
Stripe Webhookは通知ではなく、決済状態を確定するための重要な入力です。重複と順序逆転を前提に冪等な処理へ落とし込むことが、決済連携の基本になります。
Webhookの冪等性は、if文で重複を避けることではありません。受信イベントと業務状態を分け、何度実行しても同じ結果へ収束するように設計することです。


