【AI時代の個人開発】9. Stripe実装を「壊れない設計」に作り直した話
課金機能は、すでに動いていました。
テスト決済に成功し、契約状態もデータベースへ反映される。利用者が料金を支払い、サービスを使い始めるまでの流れは完成しています。
それでも私は、一度作ったStripe実装を、ほぼ設計から見直すことにしました。
理由は単純です。
お金を扱う仕組みは、「成功したとき」よりも、「想定どおりに進まなかったとき」に本当の品質が見えるからです。
処理の途中で失敗したらどうなるのか。Stripeと自分のサービスで契約状態がずれたら、元へ戻せるのか。
そこまで考えると、必要だったのは部分的なコード修正ではありませんでした。
決済できる仕組みを、安心して運用できる仕組みへ変える。
今回は、動いていたStripe実装を「壊れない設計」へ作り直した過程を振り返ります。
動いていた課金機能を、なぜ作り直したのか
最初に実装した課金機能でも、正常な流れは問題なく動いていました。
決済画面を開ける
テストカードで支払いを完了できる
Stripeから通知を受け取れる
契約状態をデータベースへ保存できる
契約済みの利用者に機能を提供できる
一通り動作した時点では、私自身も課金機能が完成に近づいたと考えていました。
ところが、コードレビューを続けるうちに、確認している範囲が正常系に偏っていることに気づきます。
同じ通知が2回届いたらどうなるのか。通知は受け取れたのに、データベース更新だけが失敗したらどうなるのか。利用者の実際の契約状況と、サービス上の表示が食い違ったらどうするのか。
作り直すきっかけになったのは、大きな不具合が発生したことではありません。
設計の前提が、うまくいく場合に寄りすぎていると気づいたことでした。
Webhookは「順番どおりに一度だけ届く」とは限らない
Webhookとは、あるサービスで起きた出来事を、別のシステムへ自動で知らせる仕組みです。
決済サービスで支払いが完了したとき、その結果をWebサービスへ通知し、通知を受け取った側が契約状態の更新などを行います。
Stripeでも、支払いの成功や失敗、契約内容の変更、解約などがWebhookで通知されます。
Stripeは、課金処理を扱う堅牢な決済サービスです。
それでもなお、外部サービスと連携する以上、通信や自サービス側の処理まで常に成功するとは限りません。
同じ通知が再送される
想定した順番とは異なる順序で届く
Webhookは届いたが、データベース更新に失敗する
Stripe側と自サービス側で状態がずれる
実装時には想定していなかったイベントが届く
Stripeの信頼性とは別に、外部システムとの連携では、一時的な通信障害や処理失敗を前提に設計する必要があります。
「正しく通知されるはず」と期待するだけでは、利用者のお金を扱う仕組みとして十分ではないと考えました。
イベントごとの処理から、「状態遷移」の設計へ

最初の実装では、Stripeから届いたイベントに応じて、必要な値を書き換える発想が中心でした。
支払いが成功したら利用可能にする。失敗したらエラー状態にする。解約されたら利用を停止する。
一つひとつを見ると、自然な処理です。
ただ、課金機能全体を考えると、「どのイベントを受け取ったか」だけでは、利用者の現在状態を正確に表せません。
たとえば、契約は存在していても、支払いだけが失敗している場合があります。解約手続きを終えていても、契約期間の終了までは利用できる場合もあります。
そこで、課金状態を1つの値だけで表すのではなく、契約状態と支払い状態に分けました。
■ 契約状態
trialing(トライアル中)
active (契約中)
canceled(解約済み)
■ 支払い状態
current(支払い正常)
pending(支払い確認中)
past_due(支払い遅延・失敗)
「契約が存在すること」と「支払いが正常であること」は、同じではありません。
この2つを分けることで、利用者が現在どの状態にいるのかを、以前より正確に表現できるようになりました。
Stripeからイベントを受け取ったときも、単純に値を書き換えるのではなく、次のように考えます。
現在はどの状態か
どのイベントを受け取ったか
次にどの状態へ移るべきか
その状態変更を許可してよいか
これらを確認してから更新します。
イベントを個別に処理する発想から、課金機能全体の状態遷移を管理する発想への変更です。
「壊れない」とは、失敗しないことではなかった
通信を伴うシステムから、あらゆる失敗を完全になくすことはできません。
今回の再設計で目指した「壊れない」は、エラーが一度も発生しないことではありませんでした。
同じ通知が届いても二重処理しない
想定外の状態変更を許さない
一部の処理に失敗しても再試行できる
Stripeとの状態のずれを検出できる
問題が起きた経緯を追跡できる
必要であれば正しい状態へ戻せる
壊れない設計とは、失敗が起きても、状態を見失わずに復旧できる設計です。
そのために見直したのが、重複処理への対策、復旧経路、ログの3点でした。
同じ通知が届いても、二重処理しない
Webhookでは、同じイベントが再送される可能性があります。
1件の支払い成功通知が2回届いたとしても、データベース更新やメール送信を2回実行してはいけません。
そこで必要になるのが、冪等性(べきとうせい)です。
冪等性とは、同じ処理を複数回実行しても、結果が1回実行した場合と変わらない性質を指します。
今回の実装では、単に「そのイベントを受け取ったことがあるか」だけではなく、処理によって作られる状態にも注目しました。
どの利用者に対する処理か
契約状態がどうなるか
支払い状態がどうなるか
どのイベントによる処理か
これらを組み合わせて作った識別情報が、stateHashです。
たとえば、ある利用者について、すでにactive(契約中)かつcurrent(支払い正常)への更新が完了しているとします。
その後、同じ結果を生む通知が再び届いても、stateHashを照合すれば、同じ状態変更を繰り返そうとしていることを判定できます。
イベントが同じかどうかだけでなく、処理後の状態が重複していないかまで見る設計です。
これにより、通知が再送されても、完了済みの状態変更を繰り返さないようにしました。
Webhookだけを、唯一の正解にしない
一方で、Webhookの処理が一時的に失敗すると、自サービス側のデータが古いまま残る可能性があります。
Stripeでは支払いが成功しているのに、自サービスでは未契約と表示される。反対に、Stripeでは契約が終了しているのに、自サービスでは利用可能な状態が残る。
こうした食い違いが起きたとき、次のWebhookを待つだけでは、能動的に復旧できません。
そこで、Stripe側の現在状態を取得し、自サービスのデータベースを同期し直すための復旧経路を用意しました。

◇再設計前
Webhookが正しく届き、正しく処理されることを期待する。
◇再設計後
Webhookを通常の更新経路として使いながら、状態がずれた場合にはStripeを正として同期し直す。
実際の契約と決済を管理しているのはStripeです。
そのため、自サービスとの食い違いが見つかった場合は、Stripe側の情報を正として状態を復旧します。
自動処理を信頼することと、自動処理だけに依存することは違うと思います。
通常の処理だけでなく、失敗した場合の戻り道まで作ることで、課金機能は「動く仕組み」から「運用できる仕組み」へ近づきました。
「ログ」はエラーが起きてから足すものではなかった
復旧手段を用意しても、何が起きたのかわからなければ、正しい判断はできません。
そこで、Webhookを受け取った記録だけでなく、処理結果まで追跡できるようにログを見直しました。
どのWebhookイベントを受け取ったか
処理前はどの状態だったか
処理後にどの状態へ変わったか
重複処理としてスキップしたか
未対応イベントとして記録したか
どの段階で処理に失敗したか
手動同期によって何が修復されたか
以前の私は、ログを「エラーが起きた後に、原因を調べるための記録」と捉えていました。
しかし、課金機能では、状態が正しく移動したことを確認するためにもログが必要です。
テストですべての未来を予測することはできません。だからこそ、本番運用が始まった後に、何が起きたのかを追えるようにしておく必要があります。
ログは、運用可能なシステムを作るための設計の一部でした。
AIにはコードではなく「設計の穴」を探してもらった
これまでの個人開発でも、AIには実装方法を質問したり、コードを書いてもらったりしていました。
今回の再設計では、その使い方が変わりました。
想定していない失敗経路を洗い出す
状態遷移に矛盾がないか確認する
正常系と異常系のテスト観点を整理する
実装が要件から外れていないかレビューする
Webhook失敗後の復旧手段が不足していないか確認する
ログだけで原因を追跡できるか検証する
完成したコードを書いてもらうこと以上に、私が見落としている設計上の穴を探してもらうことに価値がありました。
ただし、AIに質問すれば、自動的に安全な設計が完成するわけではありません。
どの状態を正とするのか。どこまでを自動で復旧するのか。どの状態なら利用を許可するのか。
最終的な判断は、サービスを運営する人間が行う必要があります。
AIは、実装者にもレビュワーにもなります。
その力を引き出すためには、人間側が「何を守りたいのか」を明確にしなければなりませんでした。
AI時代だからこそ、リリース前に診ておく
今回の再設計には、当然ながら時間がかかりました。
まだ顧客がいない段階で、実際に発生するかわからない問題まで考える必要があるのか。まずリリースして、問題が起きてから直せばよいのではないか。
個人開発のすべての機能に対して、リリース前から完璧を目指す必要はありません。
それでは、いつまでも公開できなくなります。
ただし、課金のように、問題が起きたときの影響が大きい領域は別だと思います。
課金トラブルが発生すると、修正作業だけでは終わりません。
顧客から状況を聞き取る
Stripeと自サービスの状態を照合する
誤った契約表示を修正する
二重処理や利用停止の有無を確認する
必要に応じて返金や個別連絡を行う
原因を調査し、再発防止の修正を進める
顧客が少ない運営初期だからこそ、一件の問い合わせに運営者自身の時間が大きく奪われます。
そして、課金に関する問題は、機能上の不具合だけでは終わりません。顧客がサービスへ寄せる安心感や信頼にも影響します。
契約状態が正確に反映され、問題が起きてもすぐに確認・復旧できることは、サービス品質の土台になると思います。
その土台があるからこそ、料金表示や申込画面でも迷いのない案内ができ、問い合わせにも一貫した対応ができます。結果として、利用を検討する人が安心して申し込める環境にもつながります。
さらに大きいのが、運営開始後の保守対応コストを抑えられることです。
今は、AIに設計レビューを依頼し、失敗パターンを列挙させ、テスト観点を整理してもらえます。
リリース後に顧客対応と原因調査を同時に行うより、リリース前にAIと一緒に課金設計を診ておく。
AIを味方につけられる今だからこそ、公開前に堅牢性へ投資する価値は大きくなっています。
「決済できる」から、「運用できる」へ
今回の再設計では、利用者から見える新機能が増えたわけではありません。
決済画面の見た目も、大きくは変わりません。
それでも、システムの内側では、課金機能に対する考え方が大きく変わりました。

課金機能を作り直したことで、増えたのはコードの量だけではありませんでした。
失敗を前提に考える視点と、運用まで含めて設計する基準が、自分の中に残りました。
決済できる仕組みから、安心してお金を預けてもらえる仕組みへ。
それが、今回の再設計で目指したものです。
Stripe実装を作り直し、課金機能の設計にはひとまず区切りがつきました。
それでも、本番リリースにはまだ進めませんでした。
最後に残っていたのは、画面幅による表示崩れや、本番環境でしか見つからない挙動など、一つひとつは小さく見える課題です。
次回は、リリース直前まで残った問題と、それらをどのように潰していったのかを振り返ります。
AIを使う側から、使いこなす側へ。
関連情報
次回:シリーズ10(完結編)
前回:シリーズ8
【AI時代の個人開発】シリーズを最初から読む
創作大賞2026の選考期間中は、創作大賞2026 ビジネス部門 応募作品|AI時代の個人開発 にも全10話をまとめています。
いいなと思ったら応援しよう!
いただいたお気持ちは執筆用のチョコ代に当てさせていただきます!