【ITニュース解説】How to Prevent Duplicate Bookings in a SaaS Application with Idempotency
2026年10月05日に「Dev.to」が公開したITニュース「How to Prevent Duplicate Bookings in a SaaS Application with Idempotency」について初心者にもわかりやすく解説しています。
ITニュース概要
SaaSアプリで予約や決済の二重クリックや通信エラーによる重複処理を防ぐ「멱等性」を解説。クライアントから送る固有のキーで、複数回同じリクエストが来ても一度しか処理せず、常に同じ結果を返す仕組みだ。データベーストランザクションやリクエストハッシュと組み合わせ、信頼性の高いシステムを構築できる。
ITニュース解説
システム開発の世界では、ユーザーがウェブサイトやアプリを使って予約や購入などの操作をする際、意図せず同じ処理が二重に実行されてしまうという問題がよく発生する。例えば、ホテルの部屋を予約する場面を想像してみよう。ユーザーが「予約確定」ボタンをクリックしたとする。しかし、ネットワークの遅延などで応答がなかなか返ってこないことがあると、ユーザーは「あれ?何も起こらなかったのかな?」と思ってしまい、もう一度ボタンをクリックしてしまうかもしれない。
この時、バックエンド(サーバー側)では、同じ予約リクエストが二回届くことになる。もし、サーバーがこれらのリクエストを別々のものとして処理してしまうと、どうなるだろうか。二重に部屋が予約されてしまったり、二重に支払いが試みられたり、予約番号も二つ発行されてしまったり、確認メールが二通届いたり、さらには部屋の空き状況が誤って表示されたりといった、様々な問題が発生してしまう可能性がある。これは、予約システムだけでなく、オンライン決済、商品購入、チケット手配、アカウント作成、サブスクリプションの有効化、在庫管理といった、取引が伴うSaaS(Software as a Service)アプリケーションでは非常によくある課題である。
このような問題を解決するための強力な技術概念が「べき等性(Idempotency)」と呼ばれるものだ。べき等性とは、ある操作を複数回実行しても、一度実行した場合と全く同じ最終結果になる性質のことを指す。例えば、「予約リクエストを送信する」という操作がべき等であれば、そのリクエストを一度送っても、二度送っても、三度送っても、結果として作成される予約は一つだけになる。サーバーは、これらのリクエストが同じ操作を表していることを認識し、重複した処理を行わないのだ。
では、なぜこのような重複したリクエストが発生するのだろうか。ユーザーが意図的に二重クリックする以外にも、さまざまな原因がある。例えば、クライアント(ユーザーのブラウザやアプリ)がリクエストを送ったものの、一時的なネットワークの問題でサーバーからの応答を受け取れず、自動的にリクエストを再試行することがある。モバイルアプリでは、通信が途切れた直後にユーザーが再度操作を行うこともあるだろう。ブラウザがページを再読み込みしたり、HTTPクライアントやネットワーク機器が失敗したリクエストを自動的に再試行したりする場合もある。だから、単にフロントエンド(ユーザーインターフェース)で「予約確定」ボタンを一度押したら無効にするだけでは、完全に重複リクエストを防ぐことはできない。バックエンド側でも、こうした不測の事態に備えた保護が必要となるのだ。
べき等性を実装する基本的な考え方はこうだ。まず、クライアントは、特定の操作に対して一意の「べき等性キー」を生成する。これは、UUID(Universally Unique Identifier)のような、世界中で重複しない識別子であることが多い。クライアントはこのべき等性キーを、リクエストのヘッダーに含めてサーバーに送信する。サーバーは、このリクエストを受け取ると、まずこのべき等性キーが以前に処理されたことがあるかをデータベースなどで確認する。
もし、そのべき等性キーがまだ処理されていない新しいリクエストであれば、サーバーは予約作成などの本来の操作を実行する。そして、その操作が成功した結果(例えば、作成された予約IDや成功のステータスコード、応答メッセージなど)を、べき等性キーと紐付けてデータベースに保存する。その後、クライアントにその結果を返す。
一方、もしそのべき等性キーが既に処理済みであることが確認された場合は、サーバーは保存されている過去の結果をそのままクライアントに返す。これにより、同じ操作が複数回実行されたとしても、実際に予約が作成されるのは一度だけであり、クライアントには常に最初のリクエストと同じ成功の結果が返されるため、あたかも一度しか実行されなかったかのように振る舞うことができる。
この仕組みをデータベースで実現する場合、例えばidempotency_keysというテーブルを作成し、そこに「べき等性キー」「リクエストのハッシュ値」「応答のステータスコード」「応答のボディ(内容)」などを保存する方法が一般的だ。ここで重要なのは、「べき等性キー」のカラムにユニーク制約を設定することである。これにより、同じべき等性キーを持つレコードが複数作成されるのを防ぐことができる。
実際のコード(Node.jsのExpressフレームワークを例にすると)では、まずリクエストヘッダーからべき等性キーを取得する。このキーがなければエラーを返す。次に、データベースを検索して、このキーに対応する既存のレコードがあるかを確認する。もしレコードが見つかれば、そこに保存されている過去の応答内容を使って、クライアントに結果を返す。もしレコードがなければ、初めてのリクエストなので、予約作成処理などの本来のビジネスロジックを実行し、その結果をデータベースに保存してからクライアントに返す、という流れになる。
しかし、このシンプルな実装には一つ落とし穴がある。もし「べき等性キーが存在しないことを確認」→「予約を作成」→「べき等性結果を保存」という処理の途中で、サーバーがクラッシュしてしまったらどうなるだろうか?予約は作成されたかもしれないが、べき等性結果が保存されていないため、次のリクエストが来ると、サーバーはそれを新しいリクエストと判断してしまい、二重に予約を作成してしまう可能性がある。これを防ぐためには、「データベーストランザクション」という考え方が不可欠だ。
トランザクションを用いると、「べき等性キーのチェック」「予約の作成」「べき等性結果の保存」といった一連の処理全体を、一つの分割できない(アトミックな)単位として扱うことができる。つまり、これら全ての処理が成功するか、あるいは一つでも失敗すれば全てが元に戻されるか、のどちらかになる。具体的には、トランザクションを開始し、べき等性キーのチェック、予約作成、結果保存を行い、全てが成功した場合にのみトランザクションをコミット(確定)する。これにより、途中で何らかの問題が発生しても、データの一貫性が保たれる。
もう一つ考慮すべき重要な点がある。それは、「べき等性キーだけを盲目的に信用してはいけない」ということだ。例えば、あるクライアントが「部屋101を予約する」というリクエストを、べき等性キー「ABC123」で送信したとする。後日、誤って同じべき等性キー「ABC123」を使って「部屋205を予約する」という別のリクエストを送信してしまった場合、サーバーはどうすべきだろうか?べき等性キーは同じでも、リクエストの内容が全く異なるため、これを許可すべきではない。
そこで役立つのが、「リクエストハッシュ」だ。リクエストのペイロード(内容)全体をハッシュ化し、そのハッシュ値もべき等性キーと一緒にデータベースに保存しておく。次に同じべき等性キーでリクエストが来た時、保存されているハッシュ値と、新しく来たリクエストのハッシュ値を比較する。もしハッシュ値が一致しなければ、それは同じべき等性キーを使い回して異なる操作をしようとしていると判断し、エラーを返すことで不正な操作を防ぐことができる。
もちろん、バックエンドでのべき等性による保護は非常に重要だが、フロントエンドでのユーザーエクスペリエンス向上も怠ってはいけない。ユーザーが誤って二重に送信ボタンを押すのを防ぐために、送信中はボタンを無効にしたり、読み込み中の表示をしたりするといった対策は、ユーザーの混乱を防ぎ、システムの無駄な負荷を減らすためにも引き続き必要だ。フロントエンドはユーザー体験を守り、バックエンドはデータの整合性を守る。この両輪が揃ってこそ、堅牢なシステムが構築できるのだ。
べき等性の概念は、予約システム以外にも多くの場面でその真価を発揮する。例えば、オンライン決済の流れでは、「予約作成」→「支払い初期化」→「支払いゲートウェイ」→「支払い成功」→「予約有効化」→「確認メール送信」といったステップがある。もし「支払い成功」の通知が二重にサーバーに届いた場合、べき等性がなければ、二重に支払い情報が更新されたり、二重に請求書が発行されたり、二重に予約が有効化されたり、二重に確認メールが送信されたりといった問題が発生する可能性がある。支払いゲートウェイからのコールバック処理も、べき等性を考慮して設計する必要があるのだ。
また、べき等性とデータベースの制約は、互いに関連するが異なる問題を解決する。データベースのユニーク制約などは、例えば「予約番号が重複しない」といったデータの整合性を保証するものだ。しかし、それは「二重に支払い処理が行われる」ことや「二重に確認メールが送られる」こと、「外部APIが二重に呼び出される」ことといった、ビジネスロジックの重複を防ぐものではない。べき等性は、操作全体が安全に繰り返せることを保証するものであり、データベース制約はそれを支える手段の一つだと言える。良いシステムは通常、この両方を必要とする。
より実践的な予約システムでは、クライアントから届いた予約リクエストは、APIサーバーで受け取られ、まず基本的なバリデーション(入力チェック)が行われる。次に、べき等性キーのチェックが行われ、既に処理済みのリクエストであれば保存されている結果を返し、新規リクエストであればデータベーストランザクションを開始する。このトランザクション内で、部屋の空き状況を確認し、予約を作成し、その予約結果と応答をべき等性キーに紐付けて保存し、最後にトランザクションをコミットする。そして、クライアントに最終的な応答を返すという流れになる。
高トラフィックなシステムでは、一時的なべき等性の状態管理にRedisのような高速なKVS(Key-Value Store)を利用することもある。例えば、「このべき等性キーは現在処理中である」という情報を一時的にRedisに保存することで、同時に複数のリクエストが来た場合に、それらを適切に調整し、一つのリクエストだけを処理に回すことができる。ただし、最終的な「真のデータ源(Source of Truth)」としては、やはり永続性のあるデータベースを使うのが一般的である。適切なアーキテクチャは、システムの要件や規模によって変わってくる。
そして、べき等性を実装したら、必ず徹底的なテストを行う必要がある。ただ成功するケースを試すだけでは不十分だ。同じリクエストを二回送ったときに、本当に一度しか処理されないか。同じリクエストが同時に複数届いたときに、やはり一度しか処理されないか。同じべき等性キーを使って、全く異なる内容のリクエストを送ったときに、正しく拒否されるか。また、データベースへの書き込みが完了した直後、クライアントに応答が返る前にサーバーがクラッシュしたと仮定し、再度リクエストを送ったときに、二重予約が発生しないか、といった様々な「失敗シナリオ」も想定してテストすることが非常に重要だ。
結局のところ、べき等性は単に支払いAPIのためだけの特別な機能ではない。これは、システムエンジニアがバックエンドを設計する上での一般的な原則なのだ。APIがある操作を実行し、その操作が「一度だけ発生すべき」ものであるならば、常に「もしこのリクエストが二回届いたらどうなるか?」と自問する必要がある。「もう一つのレコードが作られてしまう」という答えになるのであれば、そのエンドポイントにはべき等性による保護が足りない可能性が高い。特に、予約、支払い、注文、在庫管理といったSaaSアプリケーションでは、安全にリトライできるAPIを設計することが、後々の深刻なバグを防ぐために不可欠となる。
ユーザーはダブルクリックをする。ネットワークは常に安定しているわけではない。リクエストはタイムアウトすることもあるし、クライアントは再試行する。サーバーだって再起動することはある。これらは決して珍しい「エッジケース」ではない。むしろ、分散アプリケーションにおける「通常の運用条件」と捉えるべきだ。このような現実を考慮して、堅牢なバックエンドは設計されるべきである。べき等性は、APIが繰り返し送られてくるリクエストを安全に処理し、ビジネス上の操作が意図せず繰り返されてしまうのを防ぐための強力な手段だ。ホテル管理プラットフォームのように、たった一つの重複リクエストが予約状況、部屋の空き状況、支払い、顧客とのコミュニケーションに大きな影響を与える可能性があるアプリケーションでは、この小さな設計上の決定が、最終的にシステムの信頼性に大きな違いをもたらすことになるだろう。リクエストは最終的には繰り返される、という前提でAPIを構築するべきだ。