ComportamentalIniciante6 min de leitura6 seções1955, Data aproximada

Máquina de estados

Declare os estados possíveis e as transições permitidas numa tabela explícita: tudo que não está nela é impossível por construção. É a diferença entre 'status é uma string que a gente atualiza' e um ciclo de vida com regras.

Em 10 segundos

Estados possíveis + transições permitidas, declarados numa tabela — e toda mudança de estado passa por ela. 'Cobrança paga não pode ser cancelada' deixa de ser um if perdido num controller e vira uma regra que nenhum caminho do código consegue driblar.

Antes de ter nome

Máquinas de estado são anteriores ao software: relés telefônicos e controladores eletromecânicos já eram máquinas de estado físicas décadas antes.

  • #fsm
  • #ciclo-de-vida
  • #transicoes
  • #status
  • #invariantes
Analogia

A catraca do metrô

A catraca só conhece dois estados: travada e liberada. Moeda na travada → libera; passar na liberada → trava de novo. Empurrar a catraca travada não a abre 'um pouco' — simplesmente não acontece. Ninguém escreveu if (empurrou && !pagou) recusar(): a física da catraca torna o estado inválido inalcançável. É exatamente o que a tabela de transições faz pelo seu domínio.

01Conceito

O problema: o status que aceita qualquer coisa

status: string nasce inocente. Seis meses depois há cinco valores possíveis, quatro lugares que fazem UPDATE, e as regras de ordem — 'só cancela se ainda não pagou' — existem como ifs repetidos, cada um com uma versão levemente diferente.

O bug típico não é um estado errado: é uma transição errada. O job de expiração roda atrasado e expira uma cobrança que foi paga há dez minutos. Cada estado isoladamente é válido; o caminho entre eles é que era proibido — e ninguém tinha escrito a proibição num lugar só.

A versão formal vem da computação teórica: autômatos finitos, com estados, alfabeto de eventos e função de transição. Você não precisa do formalismo — precisa da disciplina que ele impõe: enumerar estados (fechados, não strings livres), enumerar eventos, e tornar a função de transição o único caminho de mudança.

Relação com o padrão State: a máquina de estados é o modelo (quais estados, quais transições); o padrão State é uma forma de implementá-la em OO, com uma classe por estado, útil quando cada estado carrega comportamento complexo. Para a maioria dos ciclos de vida, a tabela declarativa basta — e é mais fácil de ler, testar e desenhar.

Em banco, a transição vira UPDATE condicional: SET estado = novo WHERE id = X AND estado = esperado. De brinde, isso fecha a corrida entre dois processos disputando a mesma transição — o segundo altera zero linhas.

Como se encaixa
  1. Criada
  2. publicarAtiva
  3. pagarPaga
  4. conciliarConciliada
O caminho feliz de uma cobrança. Fora dele, cada estado tem saídas próprias (ativa também expira ou cancela) — e 'paga' não tem seta para cancelada: essa transição não existe.
02Anatomia

As três partes da máquina

Camada 1/3Tabela de transições

Camada 1

Tabela de transições

a especificação executável do ciclo de vida

Estados fechados (enum, não string), eventos fechados, e o mapa estado+evento → próximo. É pequena o bastante para caber numa tela, ser revisada pelo time de produto e virar o desenho da documentação — porque ela É a regra, não uma descrição dela.

No código

ativa: { pagar: "paga", expirar: "expirada" }

Se violar: regras de transição espalhadas em ifs voltam a divergir entre si — a tabela existe, mas na cabeça de cada dev, em versões diferentes.

03Implementação

Código

// A máquina inteira cabe numa tabela: estado atual + evento -> próximo estado.
// Tudo que NÃO está na tabela é impossível por construção.
type Estado = "criada" | "ativa" | "paga" | "expirada" | "cancelada";
type Evento = "publicar" | "pagar" | "expirar" | "cancelar";

const TRANSICOES: Record<Estado, Partial<Record<Evento, Estado>>> = {
  criada:    { publicar: "ativa", cancelar: "cancelada" },
  ativa:     { pagar: "paga", expirar: "expirada", cancelar: "cancelada" },
  paga:      {},          // estado terminal: cobrança paga não expira nem cancela
  expirada:  {},
  cancelada: {},
};

function aplicar(atual: Estado, evento: Evento): Estado {
  const proximo = TRANSICOES[atual][evento];
  if (!proximo) {
    throw new Error(`transição inválida: ${atual} + ${evento}`);
  }
  return proximo;
}

aplicar("ativa", "pagar");    // "paga"
aplicar("paga", "expirar");   // Error — o bug do job de expiração morre AQUI,
                              // não no extrato do cliente três dias depois.

// No banco, a mesma ideia vira UPDATE condicional (e mata a corrida junto):
// UPDATE cobrancas SET estado = 'paga'
//  WHERE id = $1 AND estado = 'ativa'   -- 0 linhas = transição inválida
04No mundo real

Casos de uso

Ciclo de vida de uma cobrança PIX

Cenário

Uma cobrança nasce, fica ativa aguardando pagamento, e pode ser paga, expirar ou ser cancelada. O job de expiração roda em lote e o webhook de pagamento chega quando quer — às vezes os dois no mesmo segundo.

Como o padrão entra

A tabela declara: ativa → paga|expirada|cancelada; paga, expirada e cancelada são terminais. A transição roda como UPDATE condicional, então quando o webhook e o job disputam, o segundo altera zero linhas e desiste — a cobrança paga jamais expira, por construção.

Trade-off: Todo caminho novo de negócio ('reativar cobrança expirada') exige mexer na tabela e pensar nas consequências — o que é mais lento que 'só mudar a string', e é exatamente o ponto.

Onboarding com análise de documentos

Cenário

Cadastro passa por: dados enviados → em análise → aprovado ou reprovado, com reenvio de documentos permitido só após reprovação. Analistas humanos e validação automática atuam no mesmo cadastro.

Como o padrão entra

A máquina impede o analista de aprovar um cadastro que voltou para 'aguardando documentos' e impede o robô de reprovar quem já foi aprovado. Cada transição registra ator e timestamp — o histórico de transições vira a trilha de auditoria do compliance.

Trade-off: Estados demais (análise-fase-1, fase-2, fase-3…) explodem a tabela; agrupar em estados de negócio e guardar o detalhe como dado do estado mantém a máquina legível.

05Cuidado

Armadilhas

  1. Estado implícito em booleans combinados

    isPaid + isCanceled + isExpired = oito combinações, cinco impossíveis — e nada impede paid=true com canceled=true. Booleans que descrevem ciclo de vida são uma máquina de estados desmontada: junte-os num enum e as combinações absurdas deixam de existir.

  2. A máquina que existe só na cabeça

    O time 'sabe' as regras, mas cada regra vive num if diferente, e o UPDATE direto no banco não passa por nenhum deles. Se a tabela de transições não é código executado por toda mudança, ela não é a regra — é folclore.

  3. Esquecer as saídas de erro e de tempo

    Todo estado não-terminal precisa responder: e se der errado? e se ninguém agir? Cobrança 'ativa' sem transição de expiração fica ativa para sempre; análise sem timeout prende o cadastro. Os caminhos infelizes fazem parte da máquina — geralmente são a maioria dela.

06Decisão

Quando usar × quando evitar

Use quando

3
  • Entidades com ciclo de vida e regras de ordem — cobrança, pedido, contrato, entrega.
  • Quando dois atores (jobs, webhooks, humanos) mudam o mesmo status concorrentemente.
  • Quando o histórico de transições tem valor de auditoria.

Evite quando

3
  • Flags simples sem regra de ordem — ativo/inativo não precisa de máquina.
  • Fluxos que mudam toda semana em fase de descoberta — formalizar cedo demais engessa.
  • Modelar como estado o que é dado (o valor da cobrança não é um estado).

Testar o que ficou

3 armadilhas de Máquina de estados viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • XStateA biblioteca de máquinas de estado do front: você declara estados e transições, e o que não está na tabela simplesmente não acontece.
  • PaymentIntent do StripeO status de uma cobrança (requires_payment_method, processing, succeeded) é uma máquina: cada webhook é um evento que só move para os estados permitidos.
  • useReducer do ReactA assinatura (estado, ação) => próximoEstado é exatamente uma função de transição — o reducer é a tabela da máquina escrita em código.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Transições nomeadas; estados ilegais viram erro.
pedido.transicionar("pago"); // só de 'aguardando'

O que ele cobra

Nenhum salto a mais entre a chamada e o efeito.

  • A tabela de transições precisa ser mantida em dia conforme o fluxo ganha estados e eventos
  • Estados e transições demais tornam a tabela grande e difícil de abarcar de uma vez

Não vale se: o fluxo tem um ou dois estados sem transições inválidas plausíveis — aí a máquina explícita é só cerimônia.

Onde isto já custou caro

Incidentes reais que este conceito explica.

Onde isto aparece

Continue por aqui