en
· 3 min de leitura

Máquinas de estado para tudo que importa

Sopa de flags esconde contradições. Estados explícitos, tabela de transições e log de transições deixam bugs e relatórios óbvios.

system-designengineering

Todo sistema que audito tem um pedido, uma assinatura, um documento, um job ou um pagamento que passa por estados. E a maioria representa esse ciclo de vida como uma pilha de colunas booleanas. is_paid, is_shipped, is_cancelled, is_refunded. Chamo isso de sopa de flags, e é onde mora uma parcela surpreendente dos bugs de produção.

O problema das flags é que todas podem ser verdadeiras ao mesmo tempo. Um pedido que está enviado, cancelado, reembolsado e não pago não é um estado. É uma contradição que o banco armazena feliz e que algum código, em algum lugar, precisa interpretar. Cada relatório, cada tela, cada job passa a carregar sua própria teoria particular sobre o que essas combinações significam.

Deixe os estados explícitos

Uma máquina de estado substitui a sopa por duas coisas: uma lista finita de estados e uma lista finita de transições permitidas. Um pedido está em exatamente um destes: draft, pending_payment, paid, shipped, delivered, cancelled, refunded. A tabela de transições diz que paid pode ir para shipped ou refunded. Ela não diz que cancelled pode ir para shipped.

A aplicação impõe essa tabela. Qualquer tentativa de transição inválida é rejeitada com erro, não aplicada em silêncio. Essa regra única elimina uma classe inteira de bugs, porque os estados impossíveis passam a ser impossíveis de fato, não só na intenção.

Na prática, isso é uma coluna status mais uma pequena função de transição que é o único código autorizado a mudá-la. Nada de UPDATE orders SET status direto de serviços aleatórios. Uma porta só.

Registre cada transição

A coluna status diz onde o pedido está. Não diz como chegou ali. Então, ao lado dela, mantenha uma tabela de transições: order_id, from_state, to_state, timestamp, ator, motivo. Só inserção, nunca alteração.

Essa é sua trilha de auditoria, e ela se paga na primeira vez que um cliente pergunta por que o pedido foi cancelado. Também dá métricas de graça. Quanto tempo os pedidos ficam em pending_payment? Que fração vai de shipped para refunded? São só queries na tabela de transições. Com sopa de flags, as mesmas perguntas exigem arqueologia.

Estados terminais e timeouts

Alguns estados são terminais. Delivered, refunded, cancelled: nada sai deles. Diga isso na tabela. Código que tenta "reativar" um pedido reembolsado deve falhar alto.

Timeouts também devem ser transições. Um pedido em pending_payment por mais de 30 minutos vai para expired. Não por uma flag virando em algum canto, mas por um job agendado que executa a mesma transição pela mesma porta que todo o resto. Assim o timeout é registrado, auditado e reportado exatamente como uma ação humana.

Por que isso deixa os bugs óbvios

Quando o ciclo de vida é explícito, os bugs aparecem como erros de transição inválida nos seus logs, em vez de linhas estranhas descobertas meses depois. Um relatório agrupado por status é confiável, porque status significa uma coisa só. Um engenheiro novo lê a tabela de transições e entende o negócio em dez minutos.

Também muda a conversa com produto. "Um pedido entregue pode ser cancelado?" vira uma pergunta sobre uma linha em uma tabela, não uma investigação em cinco serviços.

TypeScript combina naturalmente

Em TypeScript, uma discriminated union é uma máquina de estado esperando para acontecer. Cada estado é uma variante com seus próprios campos: um pedido shipped carrega um código de rastreio, um pedido refunded carrega um id de reembolso e um motivo. O compilador se recusa a deixar você ler o código de rastreio em um draft.

Junte isso a uma função de transição cuja assinatura diz quais estados ela aceita, e a switches exaustivos que quebram a compilação quando você adiciona um estado e esquece um branch. O sistema de tipos vira a camada de enforcement em build time, a tabela de transições em runtime, e o log de transições depois.

Sempre que algo no seu domínio tiver um ciclo de vida, e quase tudo que importa tem, modele como máquina de estado. Custa uma tabela e um pouco de disciplina. A sopa de flags custa toda semana, para sempre.