決済のような API を、二重に送られても困らないように作るには、どうしますか

Web の基礎

送る側が 1 回の操作ごとに一意の鍵 (冪等キー) を付け、受ける側は鍵と結果を保存して、同じ鍵が来たら処理をやり直さずに前の結果を返す、と答える問い。処理中に同じ鍵が来たとき、中身が違うとき、鍵をいつまで持つかの決め方まで話します。

まず 4 択で考える

2 問

答えに入れるべき考えを選びます。選ぶと、すぐに正誤と解説が出ます。そのあと下の欄で、自分の言葉で答えてみます。

  1. 問 1冪等キーを付けた決済の API に、同じ鍵のリクエストがもう一度届いた。受ける側の動きとして正しいものはどれか

  2. 問 2同じ冪等キーで、前と金額の違うリクエストが届いた。正しい扱いはどれか

2 問中 0 問に答えました

答えてみる

面接官に「決済のような API を、二重に送られても困らないように作るには、どうしますか」と聞かれたつもりで、答えを見る前に自分の言葉で、声に出して答えてみます。

解説を読む

聞かれ方から関連するコースまで、この問いの解説を全部ここに置いています。

こう聞かれます

  • 「決済のような API を、二重に送られても困らないように作るには、どうしますか」
  • 「通信が途中で切れて、利用者がもう一度ボタンを押しました。お金が 2 回引かれないようにするには」
  • 「POST の API を、再試行しても安全にするには、どうしますか」

面接官が見ている点

  • 二重の送信は、ボタンの二度押し、通信の切れ、再試行の仕組みなど、ふつうに起きると分かっているか
  • 画面でボタンを押せなくするだけでは足りず、サーバーの側で止める必要があると言えるか
  • 同じ鍵が、処理の途中に来たとき、違う中身で来たときまで考えているか
  • 鍵の保存と本体の処理を、同じトランザクションにそろえる理由を言えるか

短い答え

  • 送る側が 1 回の操作ごとに一意の鍵 (冪等キー) を作ってリクエストに付け、再試行のときも同じ鍵を送ります。
  • 受ける側は鍵と結果を保存し、同じ鍵がもう一度来たら、処理をやり直さずに保存した結果を返します。これで何回送られても、お金が動くのは 1 回だけになります。
  • 鍵の保存は本体の処理と同じトランザクションで行い、鍵に一意の制約を付けて、同時に 2 つ来ても片方しか通らないようにします。

この問いを含む分野をレッスンで練習する

レッスンを開く

少し詳しい答え

なぜ二重に届くのか

送った側は、応答が返ってこないとき、「サーバーに届かなかった」のか「届いて処理されたが、応答だけが失われた」のかを見分けられません。だから安全のために送り直します。後者だったとき、サーバーに何の備えも無いと、支払いが 2 回行われます。

HTTP では、GET や PUT や DELETE は何回送っても結果が同じになる (冪等) と決められていますが、POST はそう決められていません。決済や注文のような「新しく作る」操作は POST が多いので、自分で冪等にする仕組みが要ります。

冪等キーの流れ

送る側は、ボタンを押したときなど、1 回の操作の初めに一意の値 (UUID など) を作り、リクエストヘッダーに付けます。Idempotency-Key という名前のヘッダーで送る形が広く使われていて、標準にするための草案も出ています。

受ける側の動きを、メモリの Map で小さく書くと次の通りです。

JavaScript

const saved = new Map();
let balance = 1000;

function pay(key, amount) {
  if (saved.has(key)) {
    return saved.get(key);
  }
  balance -= amount;
  const result = "支払い済み 残高 " + balance;
  saved.set(key, result);
  return result;
}

console.log(pay("order-1-a", 300));
console.log(pay("order-1-a", 300));
console.log(pay("order-2-b", 200));
支払い済み 残高 700
支払い済み 残高 700
支払い済み 残高 500

2 回目の呼び出しは同じ鍵なので、残高を引かずに 1 回目と同じ結果を返します。実際のサービスでは Map ではなく DB に保存します。サーバーが何台あっても、再起動しても、同じ鍵を見つけられるようにするためです。

抜けやすい 3 つの場面

  • 処理の途中に同じ鍵が来る 1 回目がまだ終わっていないのに 2 回目が来ることがあります。鍵の列に一意の制約を付け、先に「処理中」として鍵を入れた側だけが進み、後から来た側には「処理中です」と返して、少し待ってから送り直してもらいます
  • 同じ鍵で違う中身が来る送る側の不具合で、金額の違うリクエストに同じ鍵が付くことがあります。鍵と一緒にリクエストの中身のハッシュも保存し、中身が違えばエラーで返します。黙って前の結果を返すと、利用者は別の支払いをしたつもりになります
  • 鍵の保存と本体の処理がずれる支払いの記録だけ書けて鍵が書けないと、再試行でまた支払いが走ります。鍵と結果の保存は、本体の書き込みと同じトランザクションで行います

外の決済サービスを呼ぶときは、その先にも同じ考えで鍵を渡します。自分のサーバーだけ冪等でも、外への呼び出しが 2 回走れば、お金は 2 回動きます。

トレードオフ

鍵を保存する表が増え、処理中の扱いや中身の照らし合わせのぶん、作りは複雑になります。鍵はいつまでも持たず、再試行が起こりうる期間だけ持って消します。その期間を過ぎた再試行は、新しい操作として扱われることを、送る側と取り決めておきます。

注文番号のように、もともと一意に決まる値があるなら、その列に一意の制約を付けるだけで十分なこともあります。冪等キーは、そうした自然な一意の値が無い操作で使います。

話す例は架空の例で構いません。「購入のボタンを二度押しされても注文が 1 つだけできるように、鍵の列に一意の制約を付けた」のように、どこで止めたかを 1 つ話せると伝わります。

よくある間違い

  • 画面でボタンを押せなくするだけで済ませる通信の切れや、再試行の仕組みからの二重の送信は止められません。サーバーの側で止めます
  • 鍵の有無を調べてから書く、を別々に行う調べた直後に同じ鍵がもう 1 つ来ると、両方が「無い」と判断して両方とも進みます。一意の制約で DB に止めてもらいます
  • 鍵だけ見て、中身を照らさない同じ鍵で違う金額が来たとき、前の結果をそのまま返してしまいます

深掘りの問い

  • 「鍵は送る側とサーバーの、どちらが作りますか」 送る側が作ります。サーバーが作ると、最初の応答が失われたときに送る側は鍵を知らず、同じ操作だと伝えられないから、と答えます
  • 「1 回目が失敗していたら、同じ鍵の再試行はどうしますか」入力の誤りのような決まった失敗は、その結果を保存して同じ失敗を返します。途中で落ちて結果が無いときは、やり直してよいように鍵を処理中から外す、と答えます
  • 「キューから同じメッセージが 2 回届くときは」受け取る側も同じで、メッセージの id を処理済みとして保存し、2 回目は何もしない、と答えます

確認日 2026年10月30日書き手 小尾 知希 (講師)

技術面接問題の一覧へ戻る