
AIに冪等性の設計を相談する|リトライで二重実行させない型
「同じ申し込みが2件入っています」
問い合わせの一次受けから回ってきた画面を見て、少し息が止まる。ログを追うと、リクエストは確かに2回来ています。1回目の応答が返る前にタイムアウトして、送り手が同じ内容をもう一度投げていました。
コードは間違っていません。テストも通っています。同じリクエストが2回来ることを、誰も想定に入れていなかっただけです。
しかもこの手の不具合は、忙しい日にだけ出ます。回線が詰まった日、外部サービスが重かった日、障害から復旧してジョブを流し直した日。再現しようとしても、手元では素直に1回で通ってしまう。
この記事では、この「2回来る」を前提にした設計——冪等性(べきとうせい)の詰め方を、AIへの相談の仕方とセットで整理します。冪等性とは、同じ操作を2回・3回受け取っても、結果が1回分にしかならない性質のことです。難しい言葉ですが、やることは「同じだと見分けて、2回目を仕事にしない」だけです。
結論:順番は3つです。①決める——二重に起きたら困る操作を1つ選び、「何をもって同じ依頼とみなすか」=冪等キーを先に決める。②弾く——2回目を、アプリの条件分岐ではなくデータベースの一意制約で落とす。「存在を確認してから登録する」は、同時に来ると通り抜けます。③返す——2回目に1回目と同じ結果を返す。エラーで返すと、送り手はさらに再送します。
覚え方は 決める → 弾く → 返す。①を飛ばすと②の条件が場当たりになり、③がないと再送が止まりません。
全部を今週やる必要はありません。①の「同じとみなす条件を1行で書く」だけでも、AIに渡す相談の質が変わります。
何が起きているのか——2回来る経路は、思っているより多い

打ち手の前に、状況を整理します。原因の側を先に見ておくと、AIに渡す前提も書きやすくなります。
1つ目は、送り手が「届いたか分からない」と再送すること。通信は、応答が返ってこなかったときに「処理されなかった」のか「処理されたが応答だけ届かなかった」のかを、送り手からは区別できません。安全側に倒すなら再送する——これは送り手の判断として正しい動きです。つまり再送は事故ではなく、正常な運用の一部です。
2つ目は、再送する主体が1つではないこと。ここが見落とされやすいところです。利用者のボタン連打や再読み込み、クライアントの自動リトライ、ロードバランサやプロキシのタイムアウト再試行、メッセージキューの再配信、外部サービスからのWebhook再送、そして障害復旧後のジョブ流し直し。どれか1か所で対策しても、別の経路から同じことが起きます。
3つ目は、キューやWebhookの多くが「少なくとも1回」で作られていること。同じメッセージが2回届くことを、仕様として許している作りです。受け取る側で重複を吸収する前提になっているので、受け側が何もしなければ、そのまま二重に処理されます。
4つ目は、「存在を確認してから登録する」が競合に弱いこと。ここがいちばん実務で刺さります。同時に2つのリクエストが来ると、両方が「まだ無い」と判定してから、両方が登録に進みます。確認と登録のあいだに、別の処理が入り込む隙間があるためです。これは書き方が雑なのではなく、この形自体が同時実行に弱い、という話です(AIに非同期・並行処理を書かせる|競合とデッドロックの確認)。
5つ目は、AIに頼むと、まさにその形が出やすいこと。「二重登録を防いでください」と頼むと、多くの場合きれいに動く「存在チェック+登録」が返ってきます。読みやすく、単体テストも通ります。同時に来たときだけ破れるので、レビューでも素通りしやすい。AIが手を抜いているわけではなく、同時実行という前提を渡していないからです。
裏を返すと、これは能力や注意力の問題ではなく、前提の渡し方と、確認する場所の問題です。見る場所が決まっていれば、AIはむしろ得意な相談相手になります。
具体例——つい、こうなりがちという形
どれも「まず動くものを」という自然な流れから生まれます。心当たりのある行があれば、そこだけ見れば十分です。
- 申し込みボタンの二度押しで、レコードが2件:画面側で連打を止めても、通信のやり直しは止まりません。画面側の対策だけでは、サーバまで届いた2回目は防げないのがつらいところです。
- 決済APIをリトライして、請求が2回:外部への呼び出しは、こちらの都合で取り消せません。金額が絡むので、影響がそのまま謝罪と返金になります。
- Webhookの再送で、在庫が2回引かれる:送り手は仕様どおりに再送しているだけなので、送り手を責めても解決しません。
- 夜間バッチをやり直したら、通知メールが2通:障害対応そのものは正しい判断だったのに、後始末が増えます。
- キューのワーカーが同じジョブを2回拾う:処理が長いジョブほど、可視性のタイムアウトを越えて再配信されやすくなります。
- 「同じ内容なら弾く」で、正しい2件目まで消える:同じ人が同じ商品を本当に2回買うこともあります。内容が同じ=重複、と決めつけると、今度は取りこぼします。
- 1回目がまだ処理中に、2回目が来る:完了フラグを見て弾く作りだと、まだ完了していないので通ってしまいます。ここは設計から抜けやすい穴です。
- リトライ回数だけ増やして様子を見る:とりあえずの延命としては分かるのですが、受け側が変わっていないと、二重実行の確率も一緒に上がります(AI出力が不安定なときのフォールバック設計|リトライの型)。
影響——「たまに」なので、見つかるのが遅い
急いで全部直さないと危ない、という話ではありません。ただ、出方が遅く、出たときは説明が要る種類の問題なので、書いておきます。
- お金と信用に直結する:二重請求・二重発注は、返金処理と説明の手間が、実装の何倍もかかります。
- 再現しないので、調査が長引く:手元では1回で通ります。ログに残っていなければ、「本当に2回来たのか」から始まります(AIに障害の再現手順を整理させる|報告と切り分けを速く)。
- 復旧が遅くなる:やり直すと二重になるかもしれない、と思うと、障害のときにジョブを流し直せません。安心してリランできない状態そのものが、復旧時間を延ばします。
- その場しのぎの手当てが積み上がる:見つかるたびに個別の重複チェックが足され、どこで何を防いでいるのかが分からなくなっていきます。
- 手作業のデータ修正が増える:二重に入った分を人が消す運用が定着すると、消し間違いという別のリスクが生まれます。
- 説明の場に立たされる:原因が「通信の再送」だと、非エンジニアには伝えにくい。設計の話なのに、不注意の話に聞こえてしまうことがあります。
少し安心できる話をすると、この領域は打ち手がはっきりしています。仕組みを入れ替える必要はなく、キーを1つ決めて、制約を1つ足すところから始められます。効果もすぐ確認できます。
明日からやること(決める・弾く・返す)
上から順に、小さく始められます。今日は①だけで構いません。
1. 決める——「同じ依頼」の定義を、先に1行で書く
ここを飛ばして相談すると、AIの答えも場当たりになります。逆に、ここさえ決まれば残りは詰めやすくなります。
- 二重に起きたら困る操作を、1つだけ選ぶ。決済、発注、通知、在庫の引き当てなど、やり直しが効かないものから。全部を一度に守ろうとすると、どれも中途半端になります。
- 冪等キーを決める。おすすめは、送り手が発番した一意の値(申し込みごとに1つ作るIDなど)を受け取る形です。送り手が同じ依頼を再送するときは、同じキーを付け直してもらいます。外部の決済サービスなどでも、リクエストに冪等キーを添えると再送を1回分として扱ってくれるものがあります(ヘッダ名や有効期間は提供元のドキュメントで確認してください)。
- 内容から作るキーは、癖を知ったうえで使う。リクエストの中身をまとめて1つの値にする方法は、送り手を変えられないときに有効です。ただし、中身に時刻や連番が混ざると別物と判定され、逆に本当に2回目の正しい依頼まで同じと判定されます。使うなら、どの項目を含めるかまで決めます。
- キーが一意である範囲を決める。利用者ごとか、テナントごとか、全体か。ここを曖昧にすると、別の顧客の依頼まで弾いてしまうことがあります(AIに認証・認可の設計を相談する|権限モデルの穴を潰す詰め方)。
- キーをどれだけ保存するかを決める。無期限に持てば安全ですが、テーブルは太ります。再送があり得る期間(送り手のリトライが止まるまで+α)を目安に、日数で決めておくと運用が楽です。
2. 弾く——アプリの条件分岐ではなく、データベースに落としてもらう
いちばん確実で、いちばん安く済む工程です。作業としては制約を1つ足すだけです。
- 冪等キーに一意制約を張る。2回目は登録の時点で必ず失敗します。同時に来ても、勝てるのは1つだけ——これがデータベースに任せる理由です。
- 失敗を、エラーではなく合図として扱う。一意制約に当たったら「すでに処理済み(または処理中)」と解釈して、③の応答に進みます。ここを例外のまま500で返すと、送り手はまた再送します。
- 処理の記録を、業務データと同じトランザクションで書く。キーの記録だけ別のタイミングで書くと、片方だけ残る状態が生まれます。まとめて確定させるのが安全です。
- 「処理中」の状態を持たせる。記録に「受付済み・処理中・完了」の状態を持たせ、完了時に結果も残します。1回目がまだ動いている最中に2回目が来たとき、何を返すかを決められるようになります。
- 外部への呼び出しは、トランザクションの中に入れない。データベースを長く握ることになり、しかも外部の処理は巻き戻せません。先に受付だけ確定させ、外部呼び出しはその後、という順にすると整理しやすくなります(AIにエラーハンドリング方針を相談する|握りつぶさない設計)。
- 制約が本当に張られているか、自分の目で確かめる。ここは実際の定義を見ます。設計の文章ではなく、適用済みのスキーマを見るのが確実です(AIにDB設計を相談する|前提の渡し方と鵜呑みにしない確認)。
3. 返す——2回目に、1回目と同じ答えを返す
ここが抜けると、弾いているのに再送が止まらない、という状態になります。
- 完了済みなら、1回目と同じ結果を返す。「重複です」ではなく、1回目の結果そのものを返すのが基本です。送り手から見れば、これで「届いた」と分かります。
- 処理中なら、処理中だと分かる形で返す。少し待ってから再送してもらう想定で、状態を返します。「まだ終わっていない」と「重複です」は別のこととして扱います。
- 同じキーで中身が違うときは、通さない。これは重複ではなく、送り手側の取り違えの可能性が高い場面です。黙って1回目の結果を返すと、事故に気づけなくなります(AIにAPIエンドポイント設計を叩かせる|命名と粒度の詰め方)。
- 返し方を、送り手と合わせておく。相手が社内の別チームでも、外部でも同じです。受け側の仕様を決めても、送り手が知らなければ再送は止まりません。
- 重複を検知した回数を、ログに残す。件数が見えていると、「効いているか」を後から確認できます。急に増えたときは、上流の異変にも気づけます(AI機能の効果測定|導入後に見るログと指標の設計)。
AIに相談するときのプロンプト(コピーして使えます)
前提を渡さないと、AIは素直に「存在チェック+登録」を出してきます。同時実行と再送を前提だと明記するのがコツです(AIにコードを書かせる前に渡す前提と制約|伝え方の型)。
ある操作を、同じ依頼が複数回届いても結果が1回分になるように設計したいので、相談に乗ってください。
①まず、この操作が二重に実行されうる経路を、思いつく限り挙げてください(利用者の再操作、クライアントの再送、途中の経路のタイムアウト再試行、キューの再配信、外部からの再送、障害復旧時のやり直しなど)。
②次に、冪等キーの候補を2〜3案、それぞれ効かなくなる条件つきで挙げてください。
③「存在を確認してから登録する」形の案は出さないでください。同時に届いたときに通り抜けるためです。データベースの制約で弾く案でお願いします。
④1回目がまだ処理中に2回目が届いたときの挙動を、明示してください。
⑤外部サービスの呼び出しとデータ更新の順序、およびその途中で落ちた場合に残る状態を説明してください。
⑥最後に、この設計で守れないケースを正直に挙げてください。
なお、確実でない部分は「不明」と書いてください。埋めなくて構いません。
操作:( ) 再送してくる相手:( ) 使っているDB:( ) 外部連携:( ) やり直しが効かない処理:( )
③の一文が効きます。先に一つ禁じ手を伝えるだけで、返ってくる案の質が変わります。⑥は、そのまま人が見る場所のリストになります。
出てきた案は、次の3点だけ自分の目で確かめます。(1)一意制約が実際に張られているか、(2)外部呼び出しがトランザクションの中に入っていないか、(3)処理中の扱いが書かれているか。この3点は、それらしい説明で埋まっていても抜けていることがあります(設計レビューの見落としをAIに洗わせる|プロンプトの型)。
確認は、テストにも落としておくと安心です。同じキーで2回呼ぶ、同時に2回呼ぶ、1回目の処理中に2回目を呼ぶ——この3つが通れば、だいたいの経路は塞がっています(境界値・異常系をAIに考えさせて漏れを減らす手順)。
二重実行を防ぐチェックリスト(コピーして使えます)
全部に○が要るわけではありません。気になった行だけで十分です。
① 決める
- 二重に起きたら困る操作を1つに絞ったか
- 「同じ依頼」とみなす条件(冪等キー)を1行で書けたか
- そのキーを、誰が発番するか決めたか
- キーが一意である範囲(利用者ごと・全体など)を決めたか
- キーの保存期間を日数で決めたか
- 本当に2回目の正しい依頼を、誤って弾かない形になっているか
② 弾く
- 冪等キーに一意制約を張ったか(適用済みのスキーマで確認したか)
- 「存在を確認してから登録」に頼っていないか
- 制約違反を、エラーではなく「処理済み」の合図として扱っているか
- 処理の記録と業務データを、同じトランザクションで確定しているか
- 「受付済み・処理中・完了」の状態を持たせたか
- 外部サービスの呼び出しをトランザクションの外に出したか
③ 返す
- 完了済みの2回目に、1回目と同じ結果を返しているか
- 処理中の2回目に、状態が分かる形で返しているか
- 同じキーで中身が違うときに、通さない扱いにしたか
- 返し方を、送り手側と合わせたか
- 重複を検知した件数をログに残しているか
- 同じキーで2回・同時に2回・処理中に2回、のテストを書いたか
この記事のまとめ
二重実行は、腕前の問題ではなく前提の問題です。通信は再送されるもの、キューは同じものを届けうるもの——そこを前提に置くと、打ち手ははっきりします。
やることは3つ。①決める——守る操作を1つ選び、冪等キーとその一意の範囲、保存期間を決める。②弾く——一意制約で2回目を落とし、制約違反を「処理済み」の合図として扱う。外部呼び出しはトランザクションの外へ。③返す——完了済みなら1回目と同じ結果を、処理中なら状態を返す。同じキーで中身が違うときは通さない。
AIに相談するときは、「存在を確認してから登録する案は出さないで」と先に一言添える。それだけで、返ってくる設計の筋がよくなります。

二重に入ったデータを見つけたとき、まず自分の書いたコードを疑ったと思います。実際には、コードは指示どおりに動いていて、同じ依頼が2回届いていただけでした。
これは、気をつけて防ぐ種類のものではありません。仕組みで受け止める種類のものです。そして仕組みは、キーを1つ決めて、制約を1つ足すところから作れます。
今日は、やり直しが効かない操作をひとつだけ思い浮かべて、「何をもって同じ依頼とみなすか」を1行書いてみる。それだけで、次の障害の夜が、少し静かになります。
よければ、こちらも
- 同時実行の穴はAIに非同期・並行処理を書かせる|競合とデッドロックの確認
- リトライそのものの設計はAI出力が不安定なときのフォールバック設計|リトライの型
- 例外の扱いはAIにエラーハンドリング方針を相談する|握りつぶさない設計
- テーブル設計の相談はAIにDB設計を相談する|前提の渡し方と鵜呑みにしない確認
- APIの返し方はAIにAPIエンドポイント設計を叩かせる|命名と粒度の詰め方
- 見落としを洗うなら設計レビューの見落としをAIに洗わせる|プロンプトの型
- 前提の渡し方はAIにコードを書かせる前に渡す前提と制約|伝え方の型
- 異常系のテストは境界値・異常系をAIに考えさせて漏れを減らす手順