冪等性を設計時に決めて二重実行を防ぐ
「もう一度押したら二重に登録された」は設計フェーズで防げる
本番バグの中には、実装ミスというより「設計時に想定していなかった」ことが原因のものがあります。代表例が二重実行です。決済ボタンを連打した、通信が不安定でリトライが自動発火した、Webhook が同じイベントを2回送ってきた——原因はさまざまですが、結果として同じ処理が2回走り、注文が2件作られる・残高が二重に減る・通知が2通届く、といった不具合になります。
この種のバグの厄介な点は、実装フェーズで気づいてから直そうとすると、テーブル設計やAPIのインターフェースにまで手を入れる羽目になることです。冪等性は「あとから足せる制御」ではなく「設計段階で決めておくべき制約」だからです。
まず「冪等性が必要な操作」を見分ける
すべての API に冪等性が要るわけではありません。判断基準はシンプルで、「同じリクエストが2回届いたときに、状態が変わってはいけないか」です。
- 要注意(冪等性が必須): 決済実行、注文作成、ポイント付与・消費、メール/通知送信、外部 Webhook の受信処理
- 基本的に安全: GET などの参照系、PUT による「値の上書き」(同じ値を2回送っても結果は同じ)、DELETE(すでに無いものを消してもエラーにしないよう設計すれば冪等)
- 見落としがち: バッチ処理内の集計・加算処理、非同期ジョブがリトライされるケース(Solid Queue などのジョブキューはリトライ前提の設計になっている場合が多い)
失敗例:チェックがない状態でリトライが有効化されたケース
ある決済 API の実装で、フロントエンドはタイムアウト時に自動リトライする設計になっていました。バックエンド側は「1回のリクエスト = 1回の決済」という前提で作られており、二重実行のチェックがありませんでした。ネットワークが不安定な環境でタイムアウトとリトライが重なり、同じ注文に対して決済が2回走ってしまいました。原因はフロントの実装ではなく、バックエンドが冪等性を前提にしていなかった設計にありました。
実装パターンの使い分け
1. 冪等性キー(Idempotency Key)
クライアントがリクエストごとに一意なキー(UUID など)を発行し、サーバー側は「同じキーのリクエストは初回の結果をそのまま返す」という方式です。決済 API でよく使われます。
POST /api/v1/payments
Idempotency-Key: 3f9a2b1c-...
# サーバー側の擬似コード
if IdempotencyKey.exists?(key)
return IdempotencyKey.find(key).cached_response
end
result = process_payment(params)
IdempotencyKey.create!(key: key, cached_response: result)
return resultポイントは、キーと結果をセットで一定期間(例: 24時間)保存し、同一キーが来たら処理を実行せず保存済みのレスポンスを返すことです。キーの生成をクライアント任せにする場合は、UUID など衝突しない値を強制するバリデーションも忘れずに。
2. DB のユニーク制約
「同じ注文に対する決済は1件まで」のように、業務ルールとして一意性が言えるなら、アプリケーションコードでの判定より DB のユニーク制約に任せる方が確実です。競合状態(レースコンディション)はアプリ層のチェックでは防げないことが多く、DB 制約が最後の砦になります。
# マイグレーション例
add_index :payments, [:order_id], unique: true, where: "status != 'failed'"チェック→INSERT の間に別リクエストが割り込む可能性がある以上、「事前にチェックしたから安全」という設計は成立しません。ユニーク制約違反を捕捉して「すでに処理済み」として扱うのが安全な実装です。
3. 多重送信防止の UI 制御
サーバー側の対策だけでなく、フロントエンドでも二重クリックそのものを防ぐのが基本です。送信ボタンをクリック直後に disabled にする、送信中はローディング表示にする、といった対策は地味ですが効果があります。ただしこれは補助であり主対策ではありません。JavaScript が無効な環境、複数タブでの同時操作、リトライ処理は UI 制御だけでは防げないため、必ずサーバー側の冪等性設計とセットで考えます。
設計レビューで確認すべきチェックリスト
- この API は複数回呼ばれても結果が変わらないか?変わってはいけない操作か?
- 冪等性キーを使う場合、キーの保存期間とスコープ(プロジェクト単位かユーザー単位か)は決まっているか
- 一意性を担保すべき組み合わせに DB のユニーク制約が張られているか
- Webhook 受信処理は、同じイベント ID を2回受け取っても副作用が1回で済むか
- 非同期ジョブがリトライされた場合、同じジョブを2回実行しても問題ないか
Bugoon での実践
二重実行系のバグは「連打した」「戻るボタンを押してもう一度送信した」など再現条件が曖昧になりがちで、報告を受けてもどの操作が引き金だったか特定しづらいのが実務上の悩みです。Bugoon はウィジェット経由での操作ステップの記録に対応しており、報告直前のクリック順序やページ遷移を追える形で残せます。二重実行が疑われる報告が来た際に、本当に2回操作されたのか、それとも1回の操作でサーバー側の処理が重複したのかを切り分ける手がかりになります。
再現手順に加えて、報告時のリクエスト情報が一緒に残せると、この切り分けはさらにやりやすくなるはずです。