Bugs de assinatura são bugs de estado. Um cliente continua com acesso depois de cancelar. Uma conta com pagamento atrasado paga e continua bloqueada. Um teste termina para alguém sem cartão, e nenhuma regra diz o que acontece depois. Cada um desses casos é uma transição que ninguém desenhou.
Um diagrama de máquina de estados, normalmente chamado de diagrama de estados, lista cada estado em que um objeto pode estar e cada evento que o leva para outro. Transições que faltam aparecem na revisão, e não nos tickets de suporte. Este artigo constrói um para uma assinatura SaaS, com os status documentados pelo Stripe Billing para que você possa comparar o modelo com um sistema real, e explica a notação à medida que ela aparece.
Quando desenhar uma máquina de estados
Desenhe uma quando um único objeto tiver um ciclo de vida com regras: um pedido, uma assinatura, um pagamento, um ticket de suporte, um deploy. Os sinais de alerta são conhecidos:
- Uma coluna
statuscom mais de três valores e código que a verifica em muitos lugares. - Flags booleanas como
is_canceledeis_pausedque podem se contradizer. - Discussões sobre se algo "pode acontecer" a partir de um determinado status.
Um diagrama de estados é a imagem errada para uma conversa entre vários participantes, em que um diagrama de sequência mostra a ordem das mensagens, e para um processo de negócio com muitas pessoas e repasses, em que um diagrama BPMN se encaixa melhor.
A notação
| Elemento | Notação | Significado |
|---|---|---|
| Estado | Retângulo com cantos arredondados | Uma condição em que o objeto fica até chegar um evento |
| Pseudoestado inicial | Pequeno círculo preenchido | Onde a máquina começa |
| Estado final | Círculo preenchido dentro de um anel | A máquina terminou |
| Transição | Seta com o rótulo event [guard] / action |
Muda quando o evento acontece e a guarda é verdadeira |
| Escolha | Losango | As guardas nas setas de saída decidem o destino |
| Estado composto | Um estado que contém estados | Regras compartilhadas por tudo o que está dentro |
| Comportamento interno | entry /, exit /, do / |
Executa ao entrar, ao sair ou enquanto está no estado |
Cada parte do rótulo de uma transição é opcional. invoice paid é um evento sozinho, [setting: unpaid] é uma guarda sozinha e cancel / revoke access é um evento com uma ação. A sintaxe e os símbolos vêm da especificação UML 2.5.1.
Os status da assinatura
O exemplo usa os status da visão geral de assinaturas do Stripe, com pagamentos automáticos com cartão:
| Status | O que significa | Acesso |
|---|---|---|
incomplete |
O primeiro pagamento ainda não foi feito; o cliente tem 23 horas | Não |
incomplete_expired |
O primeiro pagamento nunca foi concluído; nada é cobrado | Não |
trialing |
Em período de teste | Sim |
active |
Em dia | Sim |
past_due |
O pagamento da última fatura falhou ou não foi tentado | Você decide |
unpaid |
As novas tentativas se esgotaram e suas configurações escolheram este resultado | Não |
paused |
O teste terminou sem forma de pagamento, e a pausa foi escolhida | Não |
canceled |
Encerrada; o Stripe a descreve como um estado terminal | Não |
Muitos produtos mantêm o acesso durante past_due enquanto as novas tentativas de pagamento acontecem, e este exemplo também. Escreva essa decisão no diagrama; é a que as pessoas esquecem.
Construindo as transições
Pegue cada estado e pergunte quais eventos podem acontecer nele.
- Início.
subscribe [trial]leva atrialing;subscribe [no trial]leva aincomplete. - Primeiro pagamento. De
incomplete,invoice paidleva aactive. Se 23 horas passarem sem pagamento, a assinatura vai paraincomplete_expirede o Stripe anula a fatura. Esse estado é final: para tentar de novo, o cliente inicia uma nova assinatura. - Fim do teste.
trial endsvai para uma escolha.[payment succeeds]leva aactive;[no payment method]leva apausedneste exemplo. O Stripe deixa você escolher o que acontece nesse caso: criar uma fatura (o padrão), cancelar ou pausar. Desenhe a configuração que você realmente usa, não as três. - Renovações. De
active,renewal payment failsleva apast_due, einvoice paida traz de volta. - Tentativas esgotadas. Outra escolha: dependendo das suas configurações, a assinatura vai para
unpaidoucanceled, ou continua empast_due. Deunpaid, pagar a fatura a devolve paraactive. - Saída.
cancellevaactiveacanceled. Cancelar no fim do período não é um estado à parte: a assinatura continuaactivecom uma flag até o período acabar e então vai paracanceled.
Um estado composto para o acesso
Aqui, três estados dão acesso: trialing, active e past_due. Desenhá-los dentro de um único estado composto, Access granted, permite escrever a regra uma vez só: entry / grant access e exit / revoke access. Qualquer transição que saia do composto revoga o acesso, seja qual for o estado de origem, então nenhum caminho pode esquecer disso. Mudar de estado dentro do composto, por exemplo de trialing para active, não sai dele e não executa essas ações.
Conteúdo do diagrama
- 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
Como ler:
- No alto. O ponto inicial se divide conforme o teste:
trialingdentro do composto ouincompletefora dele. Deincomplete, o pagamento leva aactive, e 23 horas sem pagamento levam aincomplete_expirede ao estado final à direita. - No meio, Access granted.
trial endschega a uma escolha:activequando o pagamento é concluído,pausedà esquerda quando não há forma de pagamento. Uma renovação que falha levaactiveapast_due, e o pagamento a traz de volta. - Embaixo. Quando as tentativas se esgotam, uma segunda escolha manda a assinatura para
unpaidoucanceled.unpaidainda pode voltar aactivepagando;canceledtermina no estado final. - As conexões que entram na moldura dão acesso, e as que saem o revogam.
Do diagrama ao código
O diagrama vira uma tabela de transições. Tudo o que não está na tabela é rejeitado:
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;
}
Se os eventos vêm dos webhooks de um provedor de pagamentos, não conte com a ordem. A documentação de webhooks do Stripe diz que ele não garante que os eventos cheguem na ordem em que foram gerados. Trate cada evento como um motivo para buscar o status atual da assinatura e aplique a transição sobre ele.
Erros comuns
- Eventos faltando. Para cada estado, verifique cada evento. "O que acontece se o cliente cancelar enquanto está em
past_due?" é uma pergunta que o diagrama deve responder. - Flags desenhadas como estados. Cancelar no fim do período, um desconto ou um número de assentos são propriedades de um estado, não estados.
- Ações desenhadas como estados. "Enviar e-mail de cobrança" é uma ação em uma transição ou um comportamento
entry, não um lugar onde a assinatura espera. - Nenhum estado final. Mostre onde o ciclo de vida termina, para que ninguém escreva código que ressuscite uma assinatura cancelada.
- Guardas que se sobrepõem ou deixam lacunas. As guardas que saem de uma escolha devem cobrir cada caso exatamente uma vez; adicione
[else]se precisar. - Dois ciclos de vida em um diagrama. A assinatura e suas faturas têm máquinas de estados separadas. Ligue-as por eventos em vez de juntá-las.
Mantenha o diagrama ao lado do código que o implementa e revise os dois no mesmo pull request, como descrito em diagramas de arquitetura que não ficam desatualizados.
Monte no Archboard
- Entre com Google ou GitHub e crie um diagrama.
- Do pack UML, adicione um Estado para cada status.
- Adicione uma Moldura do pack Básico chamada Access granted em volta de
trialing,activeepast_due. - Adicione do pack Básico uma pequena Elipse preenchida para o estado inicial, um Losango para cada escolha e um Ícone de círculo com ponto para cada estado final.
- Conecte os estados com conexões rotuladas
event [guard] / action. - Adicione uma Nota UML com as ações de entrada e saída do estado composto.
O exemplo usa 14 nós de diagrama e fica dentro do plano Free. Com um agente de IA conectado via MCP, você pode colar o enum de status e deixar que ele esboce os estados, e depois conferir cada transição você mesmo.
Perguntas frequentes
Qual a diferença entre um diagrama de estados e um fluxograma? Um fluxograma mostra os passos de um processo em ordem. Um diagrama de estados mostra as condições em que um objeto pode estar e quais eventos o movem entre elas, então ele responde o que é permitido, e não o que vem a seguir.
Cada valor da minha coluna de status deve ser um estado? Cada valor que muda o comportamento, sim. Se dois valores se comportam igual em todos os casos, junte-os; se um valor esconde dois comportamentos, divida-o.
Onde os webhooks entram numa máquina de estados? Eles são eventos. Verifique-os, busque o status atual no provedor e aplique a transição, porque os eventos podem chegar fora de ordem ou mais de uma vez.
Preciso de um estado composto? Use um quando vários estados compartilham uma regra, como acesso ou cobrança. Ele transforma uma regra que você repetiria em muitas setas em uma ação de entrada e uma de saída.
Quão detalhados devem ser os rótulos de transição? Nomeie o evento em termos do domínio e adicione uma guarda só quando duas setas saírem com o mesmo evento. Coloque no diagrama as ações que importam para a correção, como conceder acesso.