冪等性パターン

英語名 Idempotency Pattern
読み方 イデンポテンシー パターン
難易度
所要時間 実装に1〜3日
提唱者 数学の冪等性概念をソフトウェア設計に応用
目次

ひとことで言うと
#

同じリクエストを2回以上送っても、1回実行したのと同じ結果になるように設計するパターン。ネットワーク障害やタイムアウトによるリトライで、決済の二重課金やデータの重複作成を防ぐ。

押さえておきたい用語
#

押さえておきたい用語
冪等性(Idempotency)
同じ操作を何度実行しても結果が変わらない性質。数学のf(f(x)) = f(x)から由来する。
冪等性キー(Idempotency Key)
クライアントが生成する一意の識別子(UUID等)。サーバーはこのキーで同一リクエストの重複を検知する。
リトライセーフ
リトライしても副作用が重複しないこと。冪等なAPIはリトライセーフである。
楽観的ロック
レコードのバージョン番号で更新競合を検知する仕組み。同時リクエストによる二重処理防止に使われる。
UPSERT
INSERT(新規作成)とUPDATE(更新)1つのSQL文で安全に処理する操作。同じデータが来ても安全に処理できる。

冪等性パターンの全体像
#

冪等性キーによるリクエスト重複防止のフロー
クライアントUUIDを生成してIdempotency-Keyヘッダーに付与キー確認Redis/DBでキーを検索存在しない → 通常処理存在する → 保存済み結果を返却処理 + 保存ビジネスロジック実行キー + レスポンスを保存TTL: 24〜72時間DB多層防御UNIQUE制約楽観的ロックUPSERT冪等性キー + DB制約の二重ガードで安全性を最大化
冪等性パターンのサーバー処理フロー
1
キー受信
リクエストから冪等性キーを取得
2
重複チェック
Redisでキーの存在を確認
3
処理実行
新規なら処理し結果を保存
4
結果返却
新規でも重複でも同じレスポンス

こんな悩みに効く
#

  • リトライで決済が二重に処理されてしまった
  • ネットワークタイムアウト後の再送で同じデータが2件作成される
  • クライアント側で「送信ボタンを2回押した」ことによる重複処理が発生する

基本の使い方
#

ステップ1: 冪等性が必要な操作を特定する

すべてのAPIに冪等性が必要なわけではない。リスクの高い操作を優先的に対応する。

  • 冪等性が必須: 決済処理、注文確定、送金、リソース作成
  • 元々冪等: GET(読み取り)、DELETE(存在しなければ何もしない)、PUT(同じ値で上書き)
  • 注意が必要: POST(作成系)。同じPOSTを2回送ると2件作成されるのがデフォルト動作

ポイント: HTTPメソッドの仕様上、GETとPUTとDELETEは冪等であるべき。POSTは設計次第。

ステップ2: 冪等性キーを導入する

リクエストごとに**一意の識別子(冪等性キー)**をクライアントに発行させる。

  • クライアントがUUIDを生成し、リクエストヘッダーに含める(例: Idempotency-Key: 550e8400-...
  • サーバーはこのキーをDBやRedisに記録する
  • 同じキーのリクエストが再度来たら、前回の結果をそのまま返す

ポイント: 冪等性キーの生存期間(TTL)を設定し、永遠にキーを保持しないようにする(例: 24時間)。

ステップ3: サーバー側の処理フローを設計する

冪等性キーに基づくサーバー側の処理フローを実装する。

  1. リクエスト受信 → 冪等性キーを確認
  2. キーが存在しない → 通常処理を実行 → 結果とキーを保存
  3. キーが存在し処理完了済み → 保存済みの結果を返す(再処理しない)
  4. キーが存在し処理中 → 409 Conflictを返す(同時リクエストを防止)

ポイント: ステップ4の「処理中」状態を管理することで、並行リクエストによる二重処理も防げる。

ステップ4: データベースレベルでも保護する

冪等性キーだけに頼らず、データベース側でも重複を防止する仕組みを入れる。

  • ユニーク制約: 注文番号やトランザクションIDにUNIQUE制約を設定
  • 楽観的ロック: バージョン番号で更新の競合を検知
  • UPSERT: INSERT OR UPDATE で同じデータが来ても安全に処理

ポイント: 多層防御の考え方。冪等性キー + DB制約 の二重ガードが最も安全。

具体例
#

例1:決済APIに冪等性キーを導入する

状況: ECサイトの決済API。ネットワーク不安定時にクライアントがリトライし、同じ注文に対して2回課金が発生。月間15件のクレーム。

実装:

  1. クライアントが注文確定時にUUIDを生成し、Idempotency-Keyヘッダーに含める
  2. サーバーはRedisにキーを記録(TTL: 24時間)
  3. 決済処理の結果(成功/失敗/レスポンスボディ)もキーに紐づけて保存
  4. 同じキーの2回目のリクエストには、保存済みのレスポンスをそのまま返す

結果: リトライによる二重課金がゼロに。月間の課金トラブル問い合わせが15件から0件に激減。

例2:ユーザー登録APIの重複作成を防止する

状況: モバイルアプリからのユーザー登録。電波が不安定な環境で「登録ボタンを押したが応答がない」ためユーザーが再タップし、同じメールアドレスで2アカウントが作成される事象が月30件発生。

対策:

  • メールアドレスにUNIQUE制約を追加(DB多層防御)
  • 登録APIにIdempotency-Key対応を追加
  • アプリ側で登録ボタン押下時にUUIDを生成、リトライ時は同じUUIDを送信

結果: 重複アカウント作成がゼロに。UNIQUE制約違反時は既存アカウントの情報を返却するフォールバック処理も追加し、ユーザー体験も向上。

例3:バッチ処理の冪等性を確保する

状況: 毎日深夜に実行する給与計算バッチ。サーバー障害でバッチが途中で止まり、再実行したところ一部の社員に給与が二重振込される事故が発生。影響額は合計480万円

対策:

  • 各給与計算レコードに処理ID(年月+社員ID)を付与
  • 振込テーブルに処理IDのUNIQUE制約を設定
  • バッチ処理をUPSERT(INSERT ON CONFLICT DO NOTHING)に変更
  • 再実行時は未処理のレコードのみが処理される

結果: バッチの途中停止からの再実行が完全に安全に。運用チームが深夜の障害対応で「再実行して大丈夫か」を悩む必要がなくなった。

やりがちな失敗パターン
#

  1. 冪等性キーのTTLを設定しない — キーが永遠に残るとストレージが肥大化する。24〜72時間程度のTTLを設定する
  2. レスポンスを保存せずにキーだけチェックする — キーの存在だけ確認して「処理済み」と返すと、クライアントが前回の結果を得られない。レスポンス全体を保存して返すことが重要
  3. GETリクエストに冪等性キーを要求する — GETは元々冪等なので、冪等性キーは不要。状態を変更する操作(POST/PATCH)にのみ適用する
  4. 並行リクエストのハンドリングを忘れる — 同じキーの2リクエストがほぼ同時に到達した場合、両方とも「キーなし」と判定して二重処理される。アトミックなロック取得(Redis SETNXやDBのSELECT FOR UPDATE)で防ぐ

まとめ
#

冪等性は分散システムの信頼性を支える基盤。ネットワークは必ず失敗する前提で、リトライしても安全なAPIを設計することが重要。冪等性キーの導入とDB制約の二重防御で、決済の二重処理やデータの重複作成を確実に防ごう