Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】My referral codes are just user IDs. The hard part was the order transaction.

2026年10月14日に「Dev.to」が公開したITニュース「My referral codes are just user IDs. The hard part was the order transaction.」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

紹介コードはユーザーIDを基にシンプル実装。注文処理ではデータ一貫性のため、複数書き込みをトランザクションで管理する。二重決済やクーポン競合対策として、PayPal注文IDをドキュメントIDとし、トランザクション内で存在確認や利用状況再チェックが重要となる。

ITニュース解説

システム開発において、ユーザーに提供する「紹介コード」や、その紹介コードが利用される「注文処理」は、一見単純に見えても、多くの考慮点や潜在的な課題を抱えている。特に、複数のユーザーが同時に操作する「競合状態」は、予期せぬ問題を引き起こすことがある。この記事では、AIヘッドショットアプリの開発者が、これらの課題にどのように直面し、解決したかの具体的な経験が語られている。

まず、「紹介コード」の実装方法について解説する。一般的な紹介コードは、ランダムな文字列を生成し、専用のデータベーステーブルで管理することが多い。しかし、筆者はより簡潔なアプローチを採用した。それは、紹介コードを「REF-」という固定のプレフィックスと、紹介者の「ユーザーID」を組み合わせるというものだ。例えば、ユーザーのIDが「user123」であれば、紹介コードは「REF-user123」となる。この方法の利点は、特別なコード生成ロジックや、紹介コードを保存するための専用データベースコレクションが不要になることだ。コードの有効期限を管理する必要もなく、紹介者がアカウントを削除しても、そのコードは単に存在しないユーザーを指すだけなので、データベースに不要なデータが残る心配もない。この設計は、ユーザーIDが公開情報であるため、紹介コードも推測可能になるという側面を持つ。しかし、紹介コードは共有されることを目的としているため、筆者はこれをセキュリティ上の問題とはみなしていない。これは、システムの要件に応じて、シンプルさと効率性を優先した優れた設計例と言えるだろう。

次に、この紹介コードが適用される「注文処理」の複雑さについて掘り下げる。注文処理は、割引適用、決済、そしてデータベースへの情報書き込みといった複数のステップから構成される。ここで最も重要なのは、これらのステップが「アトミック(不可分)」であるべきという原則だ。これは、購入者へのクレジット付与、紹介者へのボーナス付与、注文記録のデータベース書き込み、これら全ての処理が完全に成功するか、さもなければ全てが失敗して元に戻されるかのどちらか一方である必要があるという意味だ。もし一部の処理だけが成功し、他が失敗すると、システム内でデータの不整合が生じ、ユーザーからの問い合わせにつながる。このアトミック性を保証するために、「データベーストランザクション」という機能が利用される。トランザクションを使用することで、一連のデータベース操作を一つのまとまりとして扱い、その全体が成功した場合にのみ変更を永続化し、途中でエラーが発生した場合は全ての変更を取り消すことが可能になる。

注文処理におけるもう一つの大きな課題は、「リプレイバグ」だ。これは、同じ決済リクエストが何らかの理由でシステムに複数回送られてくる場合に発生する問題である。例えば、ユーザーが誤って決済ボタンを二度クリックしたり、ネットワークの不具合でリクエストが再送されたり、あるいは悪意を持って同じリクエストが繰り返されたりすることが考えられる。もしシステムがこの重複を適切に処理できなければ、購入者には二重にクレジットが付与され、紹介者にも二重のボーナスが与えられてしまうなど、深刻な問題に発展する。

当初、筆者はこの問題に対し、注文処理の直前にデータベースを検索し、同じPayPal注文IDを持つ記録が既に存在しないかを確認するという対策を講じた。しかし、この方法は「競合状態」には対応できなかった。もし、ごく短時間の間に同じ決済リクエストが二つ同時にシステムに到着した場合、両方のリクエストが同時にデータベースを検索し、どちらも「まだ記録がない」と判断してしまう可能性がある。その結果、両方のリクエストがそれぞれ独立して注文処理を進め、最終的に二重のクレジット付与が生じてしまうのだ。

この問題に対する確実な解決策は、PayPalの注文IDをデータベースの「注文ドキュメントのID」として直接使用し、さらにその存在チェックを「トランザクションの中」で行うことだった。データベースサービスであるFirestoreのトランザクションは、複数の同時実行されるトランザクションが同じドキュメントに対して書き込もうとした場合、片方だけを成功させ、もう片方を失敗させる仕組みを持つ。失敗したトランザクションは自動的に再試行されるが、その際にはすでに注文ドキュメントが存在するため、「既に処理済み」というエラーを返して処理を中断する。これにより、同じPayPal注文IDによる二重処理が確実に防止される。また、トランザクションの外側で行っていた最初の存在チェックも完全に削除せず残している。これは、安価な処理で早期に重複リクエストを排除し、PayPalへのAPI呼び出しといったコストのかかる処理をスキップできるため、全体のパフォーマンス向上に貢献する。

さらに、通常のクーポン利用に関する課題も、この注文処理内で対処する必要があった。紹介コードとは異なり、一般的なクーポンには「利用回数制限」があることが多い。例えば、「先着100名限定」といったクーポンだ。もし、割引が適用された価格でPayPal決済が完了した後に、別の注文がそのクーポンの最後の利用枠を使い果たしてしまい、結果的にそのユーザーが使ったクーポンが利用不可能になってしまう、という状況が発生する可能性がある。システムとしては割引済みの金額で決済を受け付けてしまっているため、このような状況は対応が非常に難しい。

この問題も、クーポン利用に関するチェックとクーポンの使用回数更新を「トランザクション内」に含めることで解決された。トランザクション内で注文がキャンセルされた場合、クーポンの使用回数も元に戻されるため、無駄にクーポン枠を消費することはない。決済後に注文を拒否することはユーザー体験として好ましくないが、根拠のない割引を提供し続けるよりは、システムとしての整合性を保つことがより重要である。Firestoreのトランザクションは、書き込みを行う前に全ての読み込みを完了させる必要があるため、このクーポンチェックは注文ドキュメントの存在チェックの直後に行われるよう設計された。

これらの経験から得られた大きな教訓は、複数の操作が関連し合うシステムにおいて、「競合状態」を設計段階から考慮し、テストでその堅牢性を検証することの重要性だ。筆者は、この競合状態によるバグをコードを注意深くレビューすることで発見したが、本来であれば、このような同時実行性の問題を検出するためのテストを事前に記述すべきだったと述べている。システム開発においては、機能が正しく動作することだけでなく、予期せぬ状況、例えば多数のユーザーが同時にアクセスするような高負荷時でも、システムが安定して堅牢に動作するように設計し、徹底的にテストすることが極めて重要となる。これらの実践的な学びは、Stature AIというサービスにおける注文フローの実装を通じて得られた貴重な知見である。

関連コンテンツ

関連IT用語

関連ITニュース