inSmartBank

B/43を運営する株式会社スマートバンクのメンバーによるブログです

サブスクリプション課金システム開発ケーススタディ

世はまさに大サブスクリプション時代。この潮流の中で弊社スマートバンクもまた、去る2023年7月12日にB/43プラスというサブスクリプションサービスをリリースしました。

サブスクリプションといえばユーザーに提供されるコンテンツや機能といった直接的な価値に焦点が当たりがちですが、その土台にはサブスクリプションビジネスを成立させるための課金システムがあります。本記事では筆者が行った課金関連の開発を振り返ってみて重要だったポイントや工夫点を伝えてみたいと思います。

すでに世に多くのサブスクリプションサービスがある中で、課金システムの実装はコモディティ化した単純な作業に思えるかもしれません。しかしながら自社サービスにてゼロから実現するとなると、想像よりも多くの思考と意思決定が必要とされる、エンジニアリング観点ではとても奥深い題材といえます。いち開発プロジェクトのケーススタディ、あるいはいちプログラマの思考過程のログとしても参考になる点があれば幸いです。

かなりのボリュームになったため、目次を見て興味のあるトピックを拾い読みしていただくだけでも嬉しいです 🙏 *1

まえおき

先述の通り弊社は2023年7月12日にB/43プラスというサービスをリリースしました。限定カードを初めとした様々な特典が利用可能になるサブスクリプション型のビジネスです。

サービス自体の説明やB/43プラスをつくった背景、"Why"の部分は@BNBNが『コアユーザーが本当に欲しかったものを探してB/43プラスを作った話』としてまとめておりますのでそちらを参照していただけると嬉しいです。この記事は何をどう作るか、といった"What"と"How"にフォーカスしたものなので合わせて読むと面白いかもしれません。

なお、この記事は2023年8月3日に開催した『B/43 Tech Talk 〜 Fintech×サブスクリプションサービス立ち上げの裏側〜』における筆者の発表を下敷きにしておりますが、同スライドよりもかなり詳細に立ち入った内容となっております。

想定読者

  • サブスクリプションサービスの課金システムに関するプラクティスを学びたいエンジニア
  • 新規にサブスクリプション開発を担当することになったエンジニア
  • 既存の課金システムを改善または拡張するためのアイデアを探しているエンジニア
  • サブスクリプションビジネスを運営している、または立ち上げを検討しているプロダクトマネージャーやデザイナーのような方々

1. サブスクリプションとB/43の特性について

まずは一般的なWebサービスとB/43における課金機能の実現方法について簡単に説明します。すでに深く知っている方や開発の具体の話が気になる方は「2. つくるものを決めるために(要求・要件定義)」まで飛ばしていただいても 🆗 です。

サブスクリプションとは「一定期間利用できる権利に対して料金を支払うビジネスモデル」であり、当然ながら定期的に料金をユーザーからお金を支払ってもらう仕組み、いわゆる課金機能が必要となります。

一般的なWebサービスにおける課金機能の実現方法

Webサービスにおけるユーザーからの課金は多くの場合は決済ゲートウェイアプリ内課金を用いて行われます。これらの手段は安全性と利便性を両立しつつさまざまな法令遵守を助けるという大きなメリットがあり一般的に採用されています。

大変便利な決済ゲートウェイやアプリ内課金ですが、利用するにあたっては受け入れなければいけないコストもあります。

  • 開発コスト。外部システムとネットワークを介して接続する以上、分散システムとしての複雑性を避けられない。ネットワークエラーを考慮したリトライの実装や冪等性の担保、不整合がないかをチェックするリコンサイルなど、持ち込まれた複雑性への対処が求められる。
  • 障害リスク。外部システムに障害が発生すると自社も影響を受ける。
  • 手数料。売上の数%から30%をプラットフォーム側に支払わなければならない。

📝 後述する「B/43の特性」のためにあえてdownsideを書いていますが、それでもなお自前で決済プラットフォームを構築する途方も無いコストに比べれば支払うべきものだと筆者は考えます。*2

決済ゲートウェイやアプリ内課金に関する深い説明は本旨から逸れるため省略しまして、次はB/43においてどのように課金を実現するのかを説明します。

B/43の特性

弊社の提供するB/43はプリペイドカードとモバイルアプリが一体となったプロダクトです。プリペイドなのでユーザーは事前にお金を入金し、その残高から引き落とされる形でカード決済を行います。

ユーザーが入金した残高があるので、システムの外側でユーザーに決済をさせるまでもなく、残高から料金を引き落とすことで定期的な課金処理を実現できます。ここで注目すべきは、決済ゲートウェイやアプリ内課金といった外部システムを利用せずに課金システムを構築でき、先述したdownsideの課題を丸ごとクリアできるということです。これは開発観点だけでなくビジネス的にもとても嬉しいポイントです。

また、自前で課金システムを構築するので外部システムから受ける制約が少なく、サブスクリプションの仕様の自由度が高くなります。もちろんあらゆる仕様を許容する設計にするわけにはいかないので、事前にしっかりと制約を決めて仕様や実装に落とし込むことは重要ですが、比較的高い柔軟性を維持することができます。

B/43は特殊だから参考にならない?

ここまでの説明で「B/43は特殊な例であり課金システムを構築するための参考にならないのでは?」と思ったかもしれません。しかし、待ってください!それは誤りです。この記事では、課金機能の実現方法に関係なく自社でサブスクリプションサービスを立ち上げ/開発するときに考えるべき普遍的なトピックを扱います。

筆者は過去にApp Storeや決済ゲートウェイと連携したサブスクリプションシステムの開発・運用を経験していますが、サブスクリプションにおける状態管理・データモデリング・仕様策定・日付と時刻の扱い・お金を扱うための堅牢な設計や実装….等、開発において重要なポイントや勘所は、外部システムを用いないB/43においてもさほど変わらないと感じています。

つまり、どのような実現方法で課金システムを構築するにせよ同じような課題に直面すると考えていますので、サブスクリプションに関わる方にはぜひ参考にしてもらえると嬉しいです。


前提を共有したところで、ここからは開発フェーズごとにエンジニアとして考えたことや工夫したことを振り返っていきます。

2. つくるものを決めるために(要求・要件定義)

課金システムを開発することが決まった後、開発の最初のステップとして我々は「つくるもの」を明確に定義するための調査を始めました。その過程で行った主要な課金プラットフォームの仕様調査状態遷移の考察という2つのアプローチについて説明します。

2-1. 主要な課金プラットフォームの仕様調査

まず、主要な課金プラットフォームであるApp Store、Google Play、Stripeの仕様を調査しました。これらのプラットフォームは世界中で広く利用されており、意識していないにせよ多くのユーザーが既に慣れ親しんだ体験を提供しています。これらに近しい仕様を定めればユーザーにとって理解しやすく、使いやすい課金システムになると考えたからです。

それぞれのプラットフォームでは仕様の大部分がdeveloper guideとして公開されています。これらを読むことで内部実装が透けて見えてきたり、運用フェーズにて運営として可能なアクションや機能(ユーザーの購読状態を管理機能で判定するなど)もわかってきました。

サブスクリプションの概念・用語は自明ではない

一方、この調査を進めるうち、当初の想定よりもサブスクリプションの概念・用語は自明ではないという発見がありました。それぞれのプラットフォームで用語の定義や使用方法が微妙に異なるのです。プラットフォームを利用する各社のサービスを見ても同様でした。

たとえばサブスクリプションの定期更新を止めるのは解約でしょうか、キャンセルでしょうか。また、定期更新を止めるアクションを解約と呼ぶのでしょうか、それとも定期更新を止めたあとに期限切れを迎えたときが解約なのでしょうか。

せっかくだから、俺はこの赤の用語を選ぶぜ!

ドメイン用語辞書を作成し、チームメンバーで読み合わせ

概念や用語が曖昧なままではチーム内のコミュニケーションに支障をきたすだけでなく、ユーザーに提示するUIやヘルプにおいても不都合が生じます。そこでB/43プラス開発チームではドメイン用語辞書を作成し、チームメンバーで読み合わせを行いました。一度の読み合わせではなかなか揃いませんでしたがプロジェクト中盤以降ではほぼ同じ語彙で会話できていたかと思います。

また、主要プラットフォームの仕様や用語を整理する過程で、システムへの具体的な要求を引き出す問いが見えてきました。以下はその例です。

  • 無料で使える期間はある?
  • 更新時の支払いに失敗したらすぐに解約となる?それとも猶予期間はある?
  • 解約しても有効期限まで特典は使える?
  • 期限前に再申し込みできる?
  • 運営として必要な機能は?
  • 申し込んだユーザーに紐づく別のユーザーも利用できる?

これらを1つずつ潰していくことで要件が固まっていきます。

2-2. 状態遷移の考察

もう一つのアプローチが状態遷移の考察です。このアプローチの目的は、サブスクリプションが取りうる状態や発生可能なイベントを整理し、システムの複雑性を把握することです。

状態遷移の考察には様々な視点があります。例えば、ユーザーやサービス運営といったアクターが各状態でどのアクションを行えるのか、状態間の遷移を起こすトリガーは何なのか、実際には同一とみなせる状態はあるのか等々。これらの視点を意識しつつBPMNなどでフロー形式の図を書いてみることで、全体像を把握しやすくなります。

トリガーがアクターによるアクションであれば、アクションを実行する機能が必要となります。一方、時間経過がトリガーであればバッチ処理などが必要となります。

状態やイベントを洗い出したあとには、同一とみなせる状態があるかを検討するのも重要です。状態を減らすことでシステムの複雑性を減らせるからです。

また、システムで実現する要件によって状態数が増減することもありえます。各要件を採用する/しないを組み合わせたパターンを並べ、ありえそうなパターンの状態遷移図を作成し、要件によりどれだけ複雑性が変化するのかを見極めていきました。

2つのアプローチの結果

これら2つのアプローチの結果、「概念・用語」と「取りうる状態・イベント」が整理できました。それにより、ユーザーストーリーやプロダクト要求仕様書(以下、PRD)を作成することが可能となり、UIに落とし込むべき要素も明確になっていきました。

まとめると、2つのアプローチを通じて我々が構築するべきサブスクリプションシステムの全体像が明らかになりました。

  1. 主要な課金プラットフォームの調査
    • 概念・用語の整理に寄与
    • 一般的な仕様や議論すべきポイントを明確にできた
    • 内製する課金システムが他の一般的なシステムと同様の仕様となることを確認した
  2. 状態遷移の考察
    • とりうる状態と起きるイベントの整理に寄与
    • 設計と実装以前の段階でシステムの複雑性をコントロールできた
    • 状態の考慮漏れ・機能の不足といった潜在的な問題を開発初期段階で予見して対処できた

加えて、ここまでの過程で作成したドメイン辞書、状態遷移図、PRD、UIモックアップなどは具体的な設計と実装に進む前の重要な成果物であったといえます。開発の進行とともに現れる課題の対処や要求の変更・調整を行うたびに立ち返る基盤となったためです。

3. どうつくるか(設計・実装)

ここからは設計・実装における工夫や考慮した点について記述します。スライドでは泣く泣く大部分を割愛してしまったので、ここで供養したく思います。

データモデリングが最重要

サブスクリプションシステムを構築するにあたって最も重要なのはデータモデリングです。イミュータブルデータモデルを意識しつつ、かいつまむと以下のようなステップで進めていきました。

エンティティの抽出

  1. PRDをベースにエンティティを洗い出します。「ユーザーは任意のプランを選択してサブスクリプション申し込みを行う」という要件にあったなら、太字表記にした4つのエンティティが存在すると判断します。
  2. 洗い出したエンティティをイベントとリソースに分類していきます。属性に日時があり「〜する」と表現できるものはイベントと識別しています。上記の例であれば申し込みがイベントにあたります。
  3. 各エンティティ間の関連を整理します。サブスクリプションの状態遷移を起こすイベントは何か、イベントによって生成されるリソースは何か、のように考えていきます。「ユーザー(リソース)が申し込み(イベント)することでサブスクリプション(リソース)が生成される」といった具合です。

イミュータブルデータモデルについてさらに詳しく知りたい方はぜひ下記リンクもご覧ください。

物理設計

エンティティが整理できたら永続化すべき対象(テーブル)と属性(カラム)を決めていきます。エンティティの中には永続化が不要なものもある点に注意します。

このプロセスでもStripe APIが参考になりました。ドキュメントにあるのは物理レベルのデータではなくexposeされるリソース表現ですが、裏側のデータ構造をある程度は想像することができます。Subscriptionオブジェクトだけでattributesが数十個ありますが、1つ1つ意味や用途を見つつ必要な属性を取捨選択しました。

モデリングの検証とレビュー

ここまでで物理レベルのER図やテーブル設計を書いてみたら、すべての要件を満たせるかを確認します。イメトレでも構いませんが、コードを一気に殴り書きして妥当性を確認する方法がおすすめです(筆者はスケッチと呼んでいます)。

「そんな設計で大丈夫か?」「大丈夫だ、問題ない」

もしモデリングが甘い場合に起きる弊害を今回の具体例で見てみましょう。

たとえば「ユーザーは任意のプランを選択してサブスクリプションの申し込みを行う」という仕様から、イベントエンティティの申し込みを見落としたとします。すると「申し込んだけど残高不足など何らかの理由でサブスクリプションが開始しなかった」ようなケースではデータベースに事実が記録されず、以下のような弊害が起きえます*3

  • 申し込みに失敗したユーザーを対象とする機能・施策が実現できない
  • 申し込みの成功に至るまでのファネル分析ができない

データモデリングは最重要と考えているので、チームメンバー全員を集めて設計の説明会を開き、広くレビューをしてもらうことで様々な観点のフィードバックをもらいました。

Web API設計、モバイルエンジニアとのコミュニケーション

データモデリングやテーブル物理設計の次はAPIの設計と実装に移ります。データモデリングとUIからWeb APIの大まかな方向性は自然と導かれました。

Stripe APIドキュメントをここでも参考にしつつリソース表現を決めていきます。

サブスクリプションリソースが取りうる状態は複数あるので、どの状態のときにはどのフィールドがどのような値を取るか、などを細かく認識合わせする必要があります。今回は以下の記事のようにWeb API (以下、API) が呼び出される画面・タイミングの想定、レスポンスの使われ方の想定などをUIのスクショとともに記述することでコミュニケーションの円滑化を図りました。

ohbarye.hatenablog.jp

日付と時刻の扱いに気を使う

実装の詳細において最も気を使った点の一つは日付と時刻の扱いです。サブスクリプションとは切っても切り離せないものですが、ソフトウェアでの扱いがなかなか厄介なものです。

例えば、サブスクリプションの期限が切れた直後にユーザーから見える表示が変わるか、バッチが実行されなくてもシステムが異常な状態にならないかなど、具体的な事象についても考慮する必要があります。

この他にも現在日時の取得箇所をコードベース中で局所化しておくなど、一般的な工夫も行っています。モデル内にてTime.currentを頻出させず引数で受け取るようにするとかそういう話です。詳しくは@t-wadaさんの在時刻が関わるユニットテストから、テスト容易性設計を学ぶをどうぞ。

拡張性・変更容易性を検討しておく

システムを設計・実装する際には、将来の変更を予測することも大切です。

例えば、トライアルの導入、年間プランの提供、割引・クーポン・オファーの実装、価格の変更、支払い手段の変更、プランのアップグレードなど、サービスの成長や市場の変化に対応するための機能拡張を見越すことが重要です。

これらの予測は、主要な課金プラットフォームの仕様を参考にすることで、より具体的かつ現実的なものとなります。事実、これらのうちの一部の開発はリリース後のグロースフェーズですでに検討され始めています。

メタデータを整備して分析工程に役立てる

サブスクリプションのデータ分析は事業運営において非常に重要な役割を果たします。リリース後に様々な観点でデータ分析が行われることは予測できていたため、データベースにメタデータ(コメント)を適切に残しておくことを強く意識しました。結果、今回のプロジェクト中で追加したテーブルやカラムにはすべてコメントを付与しています。

この施策はリリース直後から成果を上げており、実装したエンジニア以外のプロダクトマネージャーたちが自分自身で分析を行い意思決定できるようになりました。

メタデータ整備の工夫については筆者の過去のブログもぜひご参照ください。

blog.smartbank.co.jp

Deep Moduleを意識し、誤った使われ方をしにくくする

具体的なコーディングレベルの設計では"A Philosophy of Software Design"で述べられているDeep Moduleを意識しました。ざっくり言えば「インターフェースが狭くて実装が深い」モジュールを良しとしました。

たとえば今回はサブスクリプションというエンティティに相当するSubscriptionクラスを実装しています。このクラスのpublicメソッドを極限まで減らし、それらの中で事前条件のチェックといったビジネスロジックを始めとし、トランザクション管理等も含めてたくさんの仕事をさせています。

# (ここまで書いてませんでしたがついにRubyが登場)
# 以下のコードはすべてイメージです。実際のコードとは異なります。
class Subscription < ApplicationRecord
  # 自動更新処理を完璧にやり遂げる君
  def renew!(renewed_at:)
    # 事前条件のチェック
    # 口座のロックを取得して引き落としたり
    # 関連レコードを作成したり
    # 失敗したら記録したり諸々やる
  end

  # 解約処理を完璧にやり遂げる君
  def cancel!(canceled_at:)
  end
end

こうすることで呼び出す側が考えることを減らし、誤った使われ方をしにくくします。Subscriptionが余計なメソッドを露出していないので今後の拡張時の方針もクリアになったと感じています。

# 自動更新バッチがサブスクリプションを更新する箇所の雰囲気です
Subscription.past_due.find_each do |subscription|
  # 呼び出し側は基本的にこれだけで良くする
  subscription.renew!(renewed_at:)
end

Subscription.past_due.find_each do |subscription|
  # もし呼び出し側がこんな風になっていたら設計がおかしい。今後の拡張時もこんな風にしたくない
  if subscription.renewable?
    subscription.payment_method.lock do
      invoice = subscription.create_invoice!
      invoice.charge!
      subscription.update!(status: :active)
    end
  end
end

一見public methodの中身がダーティになりそうな設計ですが、ActiveSupportのConceringを用いて関心事をまとめたり、小さいPOROを作って処理を分割していくことでリーダビリティ・テスタビリティにも配慮しています。

# サブスクリプションの期間計算だけをやってくれるPORO
# 期間計算に関するユニットテストも手厚くやる
class SubscriptionTermCalculator
  def initialize(current_period_end_at)
    @current_period_end_at = current_period_end_at
  end

  def next_period_end_at
    # @current_period_end_at から次の期間の終了日を計算
  end
end

class Subscription < ApplicationRecord
  # 自動更新処理に関するメソッドのみをまとめたモジュールをクラス内で定義できる
  concerning :Renewable do
    def renew!(renewed_at:)
      return unless renewable?
      # term_calculator.next_period_end_at を使う処理などが続く
    end

    # private methodも近いところに書きたいですよね
    private

    def renewable?
      # 条件チェック
    end

    def term_calculator
      SubscriptionTermCalculator.new(self.current_period_end_at)
    end
  end
end

4. つくったものを検証する(テスト)

システムを設計・実装した後は、その正確性を確認するためのテストフェーズになります。

一般的なユニットテストや自動テストの話は省略しまして、ここでは手動テスト、とりわけサブスクリプションシステムにおいて肝心だった日付・時刻に関するテスト状態遷移テストの工夫について触れます。

日付・時刻に依存する機能のテスト容易性を高める

日付・時刻に依存する機能のテストでは、テスト容易性を高めるための取り組みが必要です。たとえば今回実装したサブスクリプションの自動更新は購読開始から1ヶ月後に更新処理が行われますが、更新処理のテストのために1ヶ月も待つわけにはいきません。

主要な課金プラットフォームたちはこの問題を、App Store sandboxやStripe Billing test clockといったテスト機能で解決しています。これらを参考にして我々も任意のサブスクリプションの有効期限を短縮したり、状態を更新するテストツールを充実させ、簡易に検証できるようにしました。

状態遷移テストを行う

次に状態遷移テストについてです。この手法はテスト設計の一般的な書籍でよく説明されており、インターネットにも詳しい説明が多くあるので詳細は割愛します。具体的なステップは以下です。

  1. 状態遷移図を作成します。
  2. 状態遷移図から状態遷移表を作成します。
  3. 状態遷移表からテストケースを書き起こします。実際には起き得ない無効遷移のテストも含めてやっておくのも良いでしょう。

1と2のステップは設計段階でやっても良いものですね。ケースができたらあとは実行していくのみです。

(余談)表作成やケースの書き起こしあたりは機械的な作業ですので、ChatGPTを活用して図から表への変換を試みたり、テキストを整形したりして効率を上げました。

プロジェクトの中盤と終盤で2回テストを行う

課金機能はリリースよりだいぶ前に開発が終わったため、手動テストをプロジェクトの中盤と終盤で2回実施しました。

「なぜ2回も?」の問いに対する回答は「全機能を対象とした大規模なテストフェーズを大きめのプロジェクトの終盤に置くのはリスクだと考えていたから」です*4。対処する時間や予算が限られているプロジェクト終盤にて発覚した問題に対処できない…という事態は避けたいものです。

そこで我々はステークホルダーやプロジェクトメンバーで合意しつつ、テストを実施できる機能をある程度まとめ、都度テストを行うようにしました。結果として課金機能を2回テストすることができ、自信をもってリリースに至ることができました。

安全なリリースのためのテストビルド配布とDarkLaunch

最後に、サブスクリプションとは直接関係ないものの安全なリリースのために行った2つの取り組みも少しだけ共有します。

Firebase App Distributionを利用して社内メンバーにのみサブスクリプション申し込みができるバージョンの本番環境用アプリを配布し、サブスクリプション申し込みをしてもらいました。これにより当日リリース直後に「本番環境でだけ申し込みができない!」のようなリスクを回避しました。

そして、リリース当日はデプロイをせずともサブスクリプション機能を開放できるよう、元々B/43で実装していたDark Launchの仕組みを活用しました。

これらの工夫のおかげもあり、当日はかつてなく穏やかにリリースを完遂することができました 🎉

おわりに

以上が、私たちがサブスクリプションサービスの課金システムを開発する際に考えたことです。もちろんこれらが全てではないですが、この記事では課金システム開発を振り返って特に重要だと考えたポイントに焦点を当てて説明しました。いずれの点も私たちのサービスB/43の特性に依存しない、他のサブスクリプションサービスにおいても重要な事柄やノウハウだと考えています。

これから我々はサービスのグロースに力を入れていくことになりますので、その過程で得た新たな知識や経験があれば、またこのような形で共有していきたいと思います。

本記事が皆さんの参考になれば幸いです。


本記事は@ohbaryeが執筆しました。

SmartBankではサブスクリプションサービスの開発・運用をしてみたいエンジニアを募集しています!

smartbank.co.jp

応募はしないけどサブスクリプションや課金の開発についてもっと聞いてみたいなという方には、カジュアル面談も公開しておりますのでご検討ください。

smartbank.co.jp

*1:"あとで読む"ブックマークをしていただくのも大歓迎です 👍

*2:B/43も入金機能においては決済ゲートウェイを利用しています

*3:もちろんこれで要件を満たせるシステムであれば、この設計でもOKです

*4:もちろん、対象とするソフトウェアやチームによって異なる考え方があると思います

We create the new normal of easy budgeting, easy banking, and easy living.
In this blog, engineers, product managers, designers, business development, legal, CS, and other members will share their insights.