ステートマシン図の例:サブスクリプションのライフサイクルを状態遷移で表す

SaaS のサブスクリプションを題材にした UML ステートマシン図の例です。状態、イベント、ガード、選択、複合状態と、図で早期に見つかるバグを解説します。

· 創業者・開発者

公開日:

ステートマシン図の例:サブスクリプションのライフサイクルを状態遷移で表す

サブスクリプションのバグは状態のバグです。解約した顧客がアクセスを持ち続ける。支払いが遅れていたアカウントが支払ったのに、ロックされたままになる。カードを登録していない人のトライアルが終わり、その後どうなるかを決めたルールがない。どれも、誰も描かなかった遷移です。

ステートマシン図(状態遷移図とも呼ばれます)は、1 つのオブジェクトが取りうるすべての状態と、それを別の状態へ移すすべてのイベントを並べたものです。足りない遷移は、サポートチケットではなくレビューの段階で見つかります。この記事では、SaaS のサブスクリプションを例にステートマシン図を作ります。実際のシステムと比べられるよう Stripe Billing が文書化しているステータスを使い、記法は出てきた時点で説明します。

ステートマシン図を描くべきとき

1 つのオブジェクトにルールのあるライフサイクルがあるなら、描く価値があります。注文、サブスクリプション、支払い、サポートチケット、デプロイなどです。次のような兆候に覚えがあるはずです。

  • 値が 3 つより多い status 列があり、コードのあちこちでそれを確認している。
  • is_canceled と is_paused のように、互いに矛盾しうるブール値のフラグがある。
  • あるステータスから何かが「起こりうるか」で議論になる。

複数の参加者の間のやり取りには、メッセージの順番を示すシーケンス図が向いています。多くの人や引き継ぎを含む業務プロセスには、BPMN 図のほうが合います。どちらもステートマシン図の出番ではありません。

記法

要素 記法 意味
状態 角の丸い長方形 イベントが来るまでオブジェクトがとどまる状態
開始疑似状態 小さな塗りつぶしの円 ステートマシンの開始点
終了状態 輪の中の塗りつぶしの円 ステートマシンが終了した
遷移 event [guard] / action とラベルを付けた矢印 イベントが起き、ガードが真なら遷移する
選択 ひし形 出ていく矢印のガードで行き先が決まる
複合状態 状態を含む状態 内部のすべてに共通するルール
内部の振る舞い entry /、exit /、do / 入るとき、出るとき、その状態にいる間に実行される

遷移のラベルはどの部分も省略できます。invoice paid だけでイベント、[setting: unpaid] だけでガード、cancel / revoke access はアクションを伴うイベントです。構文と記号は UML 2.5.1 仕様に基づいています。

サブスクリプションのステータス

この例では、カードによる自動支払いを前提に、Stripe のサブスクリプションの概要にあるステータスを使います。

ステータス 意味 アクセス
incomplete 初回の支払いがまだ済んでいない。顧客には 23 時間の猶予がある なし
incomplete_expired 初回の支払いが一度も成功しなかった。何も請求されない なし
trialing トライアル期間中 あり
active 正常な状態 あり
past_due 最新の請求書の支払いが失敗したか、試行されなかった 自分で決める
unpaid 再試行が尽き、設定でこの結果が選ばれている なし
paused 支払い方法がないままトライアルが終わり、一時停止が選ばれている なし
canceled 終了。Stripe は終端の状態と説明している なし

多くのプロダクトは、支払いの再試行が続いている間は past_due でもアクセスを残します。この例もそうしています。この判断は図に書いておきましょう。忘れられがちなのはこの点です。

遷移を組み立てる

状態を 1 つずつ取り上げ、そこでどんなイベントが起こりうるかを考えます。

  1. 開始。 subscribe [trial] で trialing へ、subscribe [no trial] で incomplete へ進みます。
  2. 初回の支払い。 incomplete から invoice paid で active に進みます。支払いがないまま 23 時間が過ぎると、サブスクリプションは incomplete_expired に移り、Stripe は請求書を無効にします。この状態は最終です。やり直すには、顧客が新しいサブスクリプションを始めます。
  3. トライアルの終了。 trial ends で選択に進みます。[payment succeeds] なら active へ、この例では [no payment method] なら paused へ進みます。この場合に何が起きるかは Stripe の設定で選べます。請求書を作成する(デフォルト)、解約する、一時停止する、の 3 つです。3 つすべてではなく、実際に使っている設定を描きましょう。
  4. 更新。 active から renewal payment fails で past_due に移り、invoice paid で戻ります。
  5. 再試行が尽きたとき。 もう 1 つの選択です。設定に応じて、サブスクリプションは unpaid か canceled に移るか、past_due のまま残ります。unpaid からは、請求書を支払えば active に戻ります。
  6. 終了。 cancel で active から canceled に移ります。期間末での解約は独立した状態ではありません。期間が終わるまでフラグ付きの active のままで、終わった時点で canceled に移ります。

アクセスのための複合状態

ここでアクセスを与える状態は trialing、active、past_due の 3 つです。これらを 1 つの複合状態 Access granted の中に描けば、ルールを 1 度書くだけで済みます。entry / grant access と exit / revoke access です。複合状態から出る遷移は、どの状態から始まってもアクセスを取り消すので、どの経路でも取り消し忘れは起きません。trialing から active のように複合状態の中で状態を移っても、複合状態から出たことにはならず、これらのアクションは実行されません。

開始点から trialing か incomplete に進みます。trialing、active、past due は Access granted の枠の中にあります。トライアル終了は選択を経て active か paused へ。更新の失敗で active から past due に移り、再試行が尽きると選択を経て unpaid か canceled に進み、終了状態に至ります。

15 個のコンポーネント · 17 個の接続

Archboard で開く
ダイアグラムの内容
  • Access granted
  • trialing
  • active
  • past_due
  • paused
  • incomplete
  • incomplete_expired
  • unpaid
  • canceled
  • subscribe [trial]
  • subscribe [no trial]
  • invoice paid
  • after 23 h / void invoice
  • trial ends
  • [payment succeeds]
  • [no payment method]
  • resume [payment succeeds]
  • renewal payment fails
  • retries exhausted
  • [setting: unpaid]
  • [setting: cancel]
  • cancel
Stripe Billing が文書化しているステータスを使った、SaaS サブスクリプションのステートマシン。トライアルと初回支払い、アクセスを与えるすべてのステータスをまとめた複合状態、更新の失敗、そしてサブスクリプションが終わる 2 つの道筋を示しています。

図の読み方は次のとおりです。

  • 上部。 開始点はトライアルの有無で分かれます。複合状態の中の trialing か、外の incomplete です。incomplete からは支払いで active に進み、支払いのないまま 23 時間が過ぎると incomplete_expired を経て右側の終了状態に至ります。
  • 中央は Access granted。 trial ends は選択に至ります。支払いが成功すれば active へ、支払い方法がなければ左側の paused へ進みます。更新が失敗すると active から past_due に移り、支払いで戻ります。
  • 下部。 再試行が尽きると、2 つ目の選択でサブスクリプションは unpaid か canceled に送られます。unpaid は支払えばまだ active に戻れますが、canceled は終了状態で終わります。
  • 枠に入るエッジ はアクセスを与え、枠から出るエッジはアクセスを取り消します。

図からコードへ

この図は遷移表に対応します。表にないものはすべて拒否します。

type Status = 'incomplete' | 'incomplete_expired' | 'trialing' | 'active'
  | 'past_due' | 'unpaid' | 'paused' | 'canceled';

const transitions: Record<Status, Partial<Record<string, Status>>> = {
  incomplete: { invoice_paid: 'active', expired: 'incomplete_expired' },
  trialing: { trial_ended_paid: 'active', trial_ended_no_method: 'paused' },
  active: { payment_failed: 'past_due', cancel: 'canceled' },
  past_due: { invoice_paid: 'active', retries_exhausted: 'unpaid' },
  unpaid: { invoice_paid: 'active' },
  paused: { resumed: 'active' },
  incomplete_expired: {},
  canceled: {},
};

function next(status: Status, event: string): Status {
  const target = transitions[status][event];
  if (!target) throw new Error(`${event} is not allowed in ${status}`);
  return target;
}

イベントが決済事業者の Webhook から届く場合は、順番を前提にしてはいけません。Stripe の Webhook のドキュメントには、イベントが生成された順に届くことは保証しないと書かれています。各イベントはサブスクリプションの現在のステータスを取得するきっかけとして扱い、その状態に遷移を適用しましょう。

よくある間違い

  • イベントの抜け。 状態ごとに、すべてのイベントを確認します。「past_due のときに顧客が解約したらどうなるか」は、図が答えるべき問いです。
  • フラグを状態として描く。 期間末での解約、割引、シート数は状態の属性であって、状態ではありません。
  • アクションを状態として描く。 「督促メールを送る」は遷移のアクションか entry の振る舞いであり、サブスクリプションがとどまる場所ではありません。
  • 終了状態がない。 ライフサイクルがどこで終わるかを示し、解約済みのサブスクリプションを復活させるコードを誰も書かないようにします。
  • 重なったり穴があったりするガード。 1 つの選択から出るガードは、すべてのケースをちょうど 1 回ずつカバーする必要があります。必要なら [else] を加えます。
  • 1 つの図に 2 つのライフサイクル。 サブスクリプションとその請求書は、別々のステートマシンを持ちます。混ぜずに、イベントでつなぎましょう。

古くならないアーキテクチャ図で説明しているとおり、図はそれを実装するコードのそばに置き、同じプルリクエストで両方をレビューします。

Archboard で作る

  1. Google または GitHub でサインインし、図を作成します。
  2. UML パックから、ステータスごとに 状態 を追加します。
  3. trialing、active、past_due を囲むように、Access granted という名前の基本パックの フレーム を追加します。
  4. 基本パックから、開始状態用に小さく塗りつぶした 楕円 を、選択ごとに ひし形 を、終了状態ごとに点の入った円の アイコン を追加します。
  5. 状態どうしを event [guard] / action とラベルを付けたエッジでつなぎます。
  6. 複合状態の入場と退場のアクションを書いた UML の ノート を追加します。

この例で使うダイアグラムノードは 14 個で、Free プランの範囲に収まります。MCP で接続した AI エージェントを使えば、ステータスの enum を貼り付けて状態の下書きを任せ、遷移は自分で 1 つずつ確認できます。

よくある質問

ステートマシン図とフローチャートの違いは何ですか? フローチャートはプロセスの手順を順番に示します。ステートマシン図は、オブジェクトが取りうる状態と、その間を移すイベントを示します。つまり、次に何が起きるかではなく、何が許されるかに答える図です。

ステータス列の値はすべて状態にすべきですか? 振る舞いを変える値はすべて状態にすべきです。2 つの値がどの場合でも同じように振る舞うならまとめ、1 つの値が 2 つの振る舞いを隠しているなら分けます。

Webhook はステートマシンのどこに位置しますか? Webhook はイベントです。検証し、事業者から現在のステータスを取得してから遷移を適用します。イベントは順不同で届いたり、複数回届いたりするからです。

複合状態は必要ですか? アクセスや請求のように、複数の状態が 1 つのルールを共有しているなら使いましょう。多くの矢印に繰り返し書くはずのルールを、入場アクションと退場アクションの 1 組にまとめられます。

遷移のラベルはどこまで詳しく書くべきですか? イベントはドメインの言葉で名付け、同じイベントで 2 本の矢印が出るときだけガードを加えます。アクセスの付与のように、正しさに関わるアクションは図に書きましょう。

無料で始める — 1 シート、クレジットカード不要。

関連記事

← ブログに戻る

1分でキャンバスを開く

Google または GitHub でサインイン。1シートは無料で、チームが加わったらアップグレード。