【ITニュース解説】I Fixed an Idempotency Bug in My Spring Boot E-Commerce Backend
2026年10月12日に「Dev.to」が公開したITニュース「I Fixed an Idempotency Bug in My Spring Boot E-Commerce Backend」について初心者にもわかりやすく解説しています。
ITニュース概要
ECサイトのSpring Bootバックエンドで、重複注文を防ぐ「冪等性」のバグを修正した。当初は重複リクエストを拒否していたが、注文が成功してもクライアントに結果が届かない場合に問題が発覚。成功した結果を再送したり、同時処理中のリクエストを保護したりする仕組みを導入し、システムの信頼性を向上させた。
ITニュース解説
システムエンジニアがサービス開発で直面する重要な課題の一つに「べき等性」がある。これは、同じ操作を何度実行しても、システムの状態が常に同じ結果になる性質を指す。特にEコマースのバックエンドシステムでは、顧客が意図せず二重に注文してしまうのを防ぐために、このべき等性の実装は非常に重要である。しかし、単に重複リクエストを拒否するだけでは不十分な場合がある。
今回、Spring Bootで構築されたEコマースバックエンド「ShopEase」の開発において、まさにその課題に直面し、べき等性に関するバグを修正した経験が語られている。当初の実装では、二重注文を防止するために、一度使用された「べき等性キー」を使って再度リクエストが来た場合、HTTP 409 Conflictエラーを返すことで処理を拒否していた。これは、同じ注文が複数回作成されるのを防ぐという点では機能していた。
しかし、ある重要な指摘がこのアプローチの問題点を浮き彫りにした。それは、「注文処理はサーバー側で正常に完了し、データベースへのコミットも成功したにもかかわらず、ネットワークの不具合などにより、その成功を示すHTTPレスポンスがクライアント(顧客)に届かなかった場合」である。この状況で顧客が同じべき等性キーを使って注文を再試行すると、サーバーは以前と同じように409 Conflictエラーを返してしまう。この結果、二重注文は防がれるものの、顧客は自身の注文が成功したことを知ることができない。サーバーは注文が成功したことを認識しているにもかかわらず、クライアントはそれを確認できないため、顧客体験としては非常に悪い状態となる。本来、再試行は、単にキーがすでに使用済みであると知らされるだけでなく、最初の成功したリクエストの結果を回復できるべきであるという点が、根本的な問題だった。
この問題は、システムが多重処理(マルチスレッド)環境で動作する場合にさらに複雑になる。例えば、同じべき等性キーを持つ複数の注文リクエストが、ほぼ同時にサーバーに到着する可能性がある。もし、どのリクエストもキーが使用済みかどうかをチェックするだけで、誰もそのキーを「処理中」であると主張する前に、両方のリクエストが独立して注文処理を開始しようとすると、複数の注文処理が同時に走ってしまう可能性がある。
信頼性の高いべき等性の実装では、以下の状態を正確に区別し、適切に処理する必要がある。 一つは、同じべき等性キーと全く同じ内容(ペイロード)を持つリクエストが再送されてきた場合である。この場合は、以前に完了したリクエストの成功結果をクライアントに返す(リプレイする)べきである。 もう一つは、同じべき等性キーだが、注文内容(ペイロード)が異なるリクエストが送られてきた場合である。これは、意図しないキーの再利用や誤操作の可能性があるため、リクエストを拒否するべきである。 そして、最も複雑なのが、すでに同じべき等性キーを使ったリクエストがサーバーで「処理中」である場合である。この状況では、新しいリクエストが独立して同じ注文処理を開始するのを防ぐ必要がある。これらの状態を正確に管理するには、単なる「キーが存在するかどうか」をチェックするif文のような単純なロジックでは不十分であり、複数の処理が同時にアクセスしても一貫性を保つ「原子的な状態管理」という、より厳密な仕組みが求められる。
これらの課題に対処するため、ShopEaseのバックエンドシステムに以下の改善が施された。 まず、「完了済みレスポンスのリプレイ機能」が追加された。これにより、同じキーと全く同じ注文内容で再度リクエストがあった場合、サーバーは以前に完了した注文処理の結果を返し、クライアントは最初のHTTPレスポンスが失われた場合でも、元の注文が成功したことを確認できるようになった。これにより、再試行によって新しい注文が誤って作成されるのを防ぎつつ、顧客は自身の注文状況を正確に把握できるようになる。
次に、「SHA-256ペイロードフィンガープリンティング」が導入された。これは、各注文リクエストのペイロード(注文内容)から、一意の「指紋」とも言えるハッシュ値(SHA-256)を生成する仕組みである。同じべき等性キーが再利用された際に、このペイロードフィンガープリントが以前のリクエストと異なる場合、それは異なる注文を意味するため、HTTP 422 Unprocessable Contentエラーを返してリクエストを拒否する。これにより、一つの注文のために発行されたキーが、意図せず別の注文のために再利用されるのを防ぐことが可能になる。
さらに、「明示的なリプレイヘッダー」が再実行されるレスポンスに含まれるようになった。具体的には、「Idempotency-Replayed: true」というヘッダーが付与される。このヘッダーがあることで、クライアント側は、受け取ったレスポンスが新規に処理された結果なのか、それとも以前の処理の成功結果を再送したものなのかを明確に区別できるようになる。これは、クライアント側の処理ロジックをより堅牢にする上で役立つ。
そして、最も重要な改善点の一つとして、「処理中リクエストの保護」が強化された。これは、同じべき等性キーを持つリクエストが複数同時に到着した場合に、いずれか一つのリクエストだけが注文処理を進められるようにする仕組みである。つまり、最初のスレッドが処理を開始したら、他のスレッドはその処理が完了するまで待機するか、処理を中止するように調整される。この保護は、もしシステムが複数のサーバーインスタンスで動いている場合(マルチインスタンスデプロイメント)、各サーバーがそれぞれ独立して処理状態を持つだけでは不十分である。リクエストがどのサーバーに到達しても正しく一貫した動作をするよう、共有された、そして原子的な操作が可能なストレージ(例えば、分散ロックシステムやデータベースの排他制御など)を通じて、キーの「処理中」状態を調整する必要がある。
これらの修正が正しく機能することを確認するためには、具体的なテストケースが必要である。最も重要な受け入れテストは、まずチェックアウトリクエストを送信し、注文トランザクションがデータベースにコミットされるのを待つ。次に、その注文に対するHTTPレスポンスがクライアントに届かない状況を意図的に作り出す(例えば、ネットワーク接続を遮断する)。その後、元のべき等性キーと全く同じ注文内容でリクエストを再試行する。この再試行で、最初に作成された注文のIDが返されること、そして実際に新しい注文が作成されず、在庫の減少も一度しか行われていないことを確認する必要がある。さらに、複数の同時リクエストや、同じキーで異なる注文内容を送信した場合の動作もテストするべきである。
この一連の改善と経験を通じて学んだ最も重要な教訓は、単に重複リクエストを拒否するだけでは、全体としてのチェックアウト体験が信頼できるものになるとは限らないという点である。真に堅牢なバックエンドシステムを開発するには、成功するAPI呼び出しだけでなく、ネットワーク障害、データベーストランザクションのコミット、複数の処理が同時に行われる状況(同時実行)、そして顧客によるリトライなど、あらゆる種類の「失敗シナリオ」を想定し、それらを適切にハンドリングすることが不可欠である。クライアントにレスポンスが届かなくても、サーバー側では処理が成功している可能性がある、という「エッジケース」まで考慮に入れることで、システムの信頼性は大きく向上する。このようなフィードバックと修正のプロセスは、実際の開発において非常に価値のある学びとなる。