State Diagram Example: A Subscription Lifecycle as a State Machine

A worked UML state machine diagram for a SaaS subscription: states, events, guards, choice points, a composite state, and the bugs a state diagram catches early.

· Founder & developer

Published

State Diagram Example: A Subscription Lifecycle as a State Machine

Subscription bugs are state bugs. A customer keeps access after canceling. A past-due account pays and stays locked out. A trial ends for someone without a card, and no rule says what happens next. Each of these is a transition nobody drew.

A state machine diagram, usually called a state diagram, lists every condition one object can be in and every event that moves it to another. Missing transitions show up in review instead of in support tickets. This post builds one for a SaaS subscription, using the statuses Stripe Billing documents so you can compare the model with a real system, and covers the notation as it comes up.

When to draw a state machine

Draw one when a single object has a lifecycle with rules: an order, a subscription, a payment, a support ticket, a deployment. The warning signs are familiar:

  • A status column with more than three values, and code that checks it in many places.
  • Boolean flags such as is_canceled and is_paused that can contradict each other.
  • Arguments about whether something "can happen" from a given status.

A state diagram is the wrong picture for a conversation between several participants, where a sequence diagram shows the order of messages, and for a business process with many people and hand-offs, where a BPMN diagram fits better.

The notation

Element Notation Meaning
State Rounded rectangle A condition the object stays in until an event arrives
Initial pseudostate Small filled circle Where the machine starts
Final state Filled circle inside a ring The machine has finished
Transition Arrow labeled event [guard] / action Move when the event happens and the guard is true
Choice Diamond Guards on the outgoing arrows decide the target
Composite state A state that contains states Rules shared by everything inside
Internal behavior entry /, exit /, do / Runs on entering, on leaving, or while in the state

Every part of a transition label is optional. invoice paid is an event on its own, [setting: unpaid] is a guard on its own, and cancel / revoke access is an event with an action. The syntax and the symbols come from the UML 2.5.1 specification.

The subscription statuses

The example uses the statuses from Stripe's subscription overview, with automatic card payments:

Status What it means Access
incomplete The first payment is not done yet; the customer has 23 hours No
incomplete_expired The first payment never succeeded; nothing is billed No
trialing In a trial period Yes
active In good standing Yes
past_due Payment on the latest invoice failed or was not attempted Your choice
unpaid Retries are exhausted and your settings chose this outcome No
paused The trial ended without a payment method, and pause was chosen No
canceled Ended; Stripe describes it as a terminal state No

Many products keep access during past_due while payment retries run, and this example does too. Write that decision on the diagram; it is the one people forget.

Building the transitions

Take each state and ask which events can happen there.

  1. Start. subscribe [trial] leads to trialing; subscribe [no trial] leads to incomplete.
  2. First payment. From incomplete, invoice paid leads to active. If 23 hours pass without payment, the subscription moves to incomplete_expired, and Stripe voids the invoice. That state is final: to try again, the customer starts a new subscription.
  3. Trial end. trial ends goes to a choice. [payment succeeds] leads to active; [no payment method] leads to paused in this example. Stripe lets you choose what happens in that case: create an invoice (the default), cancel, or pause. Draw the setting you actually use, not all three.
  4. Renewals. From active, renewal payment fails leads to past_due, and invoice paid brings it back.
  5. Retries exhausted. Another choice: depending on your settings, the subscription moves to unpaid or canceled, or stays past_due. From unpaid, paying the invoice returns it to active.
  6. Exit. cancel moves active to canceled. Cancel at period end is not a separate state: the subscription stays active with a flag until the period ends, then moves to canceled.

A composite state for access

Three states grant access here: trialing, active and past_due. Drawing them inside one composite state, Access granted, lets you write the rule once: entry / grant access and exit / revoke access. Any transition that leaves the composite revokes access, whichever state it starts from, so no path can forget it. Moving between states inside the composite, such as from trialing to active, does not leave it and does not run those actions.

A start dot leads to trialing or incomplete. Trialing, active and past due sit inside an Access granted frame. Trial end goes through a choice to active or paused. Failed renewals move active to past due; exhausted retries go through a choice to unpaid or canceled, then to a final state.

15 components · 17 connections

Open in Archboard
Diagram contents
  • 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
A state machine for a SaaS subscription using the statuses Stripe Billing documents: the trial and the first payment, a composite state for every status that grants access, failed renewals, and the two ways a subscription ends.

How to read it:

  • Top. The start dot splits on the trial: trialing inside the composite, or incomplete outside it. From incomplete, payment leads to active, and 23 hours without one lead to incomplete_expired and the final state on the right.
  • Middle, Access granted. trial ends reaches a choice: active when the payment succeeds, paused on the left when there is no payment method. A failed renewal moves active to past_due, and payment moves it back.
  • Bottom. When retries are exhausted, a second choice sends the subscription to unpaid or canceled. unpaid can still pay its way back to active; canceled ends in the final state.
  • Edges into the frame grant access, edges out of it revoke access.

From the diagram to code

The diagram maps to a transition table. Anything not in the table is rejected:

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;
}

If the events come from a payment provider's webhooks, do not assume order. Stripe's webhook documentation says it does not guarantee that events arrive in the order they were generated. Treat each event as a reason to fetch the subscription's current status, then apply the transition to that.

Common mistakes

  • Missing events. For every state, check every event. "What happens if the customer cancels while past_due?" is a question the diagram should answer.
  • Flags drawn as states. Cancel at period end, a discount or a seat count are properties of a state, not states.
  • Actions drawn as states. "Send dunning email" is an action on a transition or an entry behavior, not a place the subscription waits.
  • No final state. Show where the lifecycle ends, so nobody writes code that revives a canceled subscription.
  • Guards that overlap or leave gaps. The guards leaving a choice must cover every case exactly once; add [else] if needed.
  • Two lifecycles in one diagram. The subscription and its invoices have separate state machines. Link them with events instead of merging them.

Keep the diagram next to the code that implements it and review both in the same pull request, as described in architecture diagrams that don't rot.

Build it in Archboard

  1. Sign in with Google or GitHub and create a diagram.
  2. From the UML pack, add a State for each status.
  3. Add a Core Frame named Access granted around trialing, active and past_due.
  4. Add a small filled Core Shape set to an ellipse for the initial state, a Shape set to a diamond for each choice, and an Icon of a circle with a dot for each final state.
  5. Connect the states with edges labeled event [guard] / action.
  6. Add a UML Note with the entry and exit actions of the composite state.

The example uses 14 diagram nodes, within the Free plan. With an AI agent connected over MCP, you can paste your status enum and ask it to draft the states, then check every transition yourself.

FAQ

What is the difference between a state diagram and a flowchart? A flowchart shows the steps of a process in order. A state diagram shows the conditions one object can be in and which events move it between them, so it answers what is allowed rather than what happens next.

Should every value of my status column be a state? Every value that changes behavior should be. If two values behave the same in every case, merge them; if one value hides two behaviors, split it.

Where do webhooks fit in a state machine? They are events. Verify them, fetch the current status from the provider, and apply the transition, because events can arrive out of order or more than once.

Do I need a composite state? Use one when several states share a rule, such as access or billing. It turns a rule you would repeat on many arrows into one entry and one exit action.

How detailed should transition labels be? Name the event in domain terms and add a guard only where two arrows leave on the same event. Put the actions that matter for correctness, such as granting access, on the diagram.

Start free — one seat, no credit card.

Related posts

← Back to blog

Open a canvas in a minute

Sign in with Google or GitHub. Free for a single seat. Upgrade when the team joins.