Feature flags em produção: como evitar que uma flag esquecida vire dívida técnica permanente

Escrito por

em

Toda equipe que adota feature flags começa pela mesma promessa: deploy independente de release, rollback sem reverter commit, teste em produção com tráfego real. Três meses depois, o inventário de flags passou de seis para setenta, ninguém lembra por que metade delas existe, e o código está cheio de if aninhados que ninguém tem coragem de apagar. O problema não é a técnica — é a ausência de um ciclo de vida definido para cada flag desde o momento em que ela nasce.

O que uma flag esquecida custa de verdade

Uma flag ativa por tempo indeterminado não é neutra. Ela dobra o número de caminhos possíveis em cada função que a consulta, o que significa dobrar a superfície de teste — na prática, ninguém testa os dois lados depois da segunda semana, e o caminho “desligado” apodrece sem que ninguém perceba. Quando finalmente alguém tenta removê-la, encontra três serviços diferentes lendo o mesmo nome de flag com lógicas de fallback incompatíveis entre si, porque cada time implementou a checagem do seu jeito, em momentos diferentes, sem revisar como o vizinho tinha feito.

Há também o custo de runtime, geralmente ignorado: cada avaliação de flag é uma chamada de rede ou uma leitura de cache distribuído, dependendo do provedor. Em um endpoint chamado alguns milhares de vezes por segundo, isso aparece no p99 de latência antes de aparecer em qualquer dashboard de “dívida técnica” — e quando alguém finalmente investiga a lentidão, a causa raiz é uma flag de experimento que devia ter sido removida há oito meses, ainda sendo avaliada em todo request.

O terceiro custo, mais silencioso, é cognitivo: cada nova pessoa que entra no time precisa aprender o significado de setenta nomes de flag antes de entender o comportamento real do sistema em produção. O código deixa de ser a fonte da verdade sobre o que o usuário vê — vira só metade da história, com a outra metade espalhada num painel de configuração que ninguém documentou.

Se a equipe não consegue responder, sem consultar ninguém, “que decisão essa flag ainda está esperando para ser tomada”, ela já devia ter sido removida.

Categorizar antes de criar

Nem toda flag tem o mesmo ciclo de vida, e tratar todas como a mesma coisa é a raiz do acúmulo. Vale separar por categoria já na criação, antes mesmo de escrever a primeira linha de código que a consulta:

  • Release toggle — esconde uma funcionalidade incompleta. Vida útil: dias a poucas semanas. Sempre removida depois do lançamento, sem exceção, independente de quem pediu pra “deixar por garantia”.
  • Experiment toggle — sustenta um teste A/B. Vida útil: até o experimento fechar com significância estatística. Tem data de expiração desde o dia em que nasce, e essa data entra no mesmo pull request que cria a flag.
  • Ops toggle — kill switch para desligar uma dependência instável em produção. Vida útil: permanente, mas exige alerta automático quando fica ligada por mais de algumas horas, porque ligada significa que algo está degradado ali fora.
  • Permission toggle — libera recurso por plano ou por cliente. Vida útil: permanente, mas isso é uma regra de negócio, não uma flag de deploy — e deveria migrar para uma tabela de configuração assim que o número de variações passar de duas ou três.

Confundir “ops toggle” com “permission toggle” é o erro mais comum: a equipe deixa um recurso condicionado a uma flag booleana no código porque “por enquanto só um cliente pede isso”, e um ano depois há onze clientes, cada um com sua combinação de flags, e ninguém consegue prever o comportamento do sistema para um cliente novo sem ler o código de trás para frente. Nesse ponto, a flag deixou de ser uma ferramenta de deploy e virou uma tabela de permissões disfarçada de booleano espalhado pelo código — o pior dos dois mundos.

Sinais de que uma flag já devia ter sido removida

Antes de chegar no processo formal de revisão, alguns sinais aparecem cedo e costumam ser ignorados justamente porque parecem pequenos demais para justificar uma tarefa própria no board:

  • Ninguém no time lembra, sem olhar o código, se a flag está ligada ou desligada em produção hoje
  • O nome da flag não aparece em nenhuma conversa há mais de um mês, nem no Slack nem em reunião
  • Os dois caminhos (ligado e desligado) fazem exatamente a mesma coisa há semanas, porque o rollout já chegou a 100%
  • A flag aparece em mais de um serviço, e cada serviço a interpreta de um jeito ligeiramente diferente

Qualquer um desses sinais, isolado, já é motivo suficiente para abrir a remoção como tarefa própria — não como algo que “cabe” dentro de outra entrega, porque é exatamente esse tipo de tarefa que nunca ganha prioridade quando compete com uma feature nova.

O código que sobrevive à remoção

A prática mais eficaz para não acumular flags mortas é escrever o código já pensando em como vai ser a remoção, não apenas em como vai ser a ativação. Uma checagem espalhada em cinco arquivos é uma flag que ninguém vai remover, porque remover significa caçar cada ocorrência e confiar que nenhuma foi esquecida — e confiança não é uma estratégia de engenharia confiável.

Centralizar em um ponto único de decisão facilita tanto a leitura quanto a remoção futura:


function podeUsarNovoCheckout(usuario) {
  return flags.ativa("checkout_v2", usuario);
}

// uso, em qualquer lugar do sistema
if (podeUsarNovoCheckout(usuario)) { ... }


Remover a flag vira: apagar a função, trocar as chamadas pelo branch vencedor, apagar a configuração no provedor. Três passos mecânicos, não uma caçada por dezenas de arquivos.

Outra decisão que evita acúmulo: nunca aninhar uma flag dentro de outra sem um motivo explícito documentado ali mesmo, no comentário acima da condição. Duas flags cruzadas multiplicam os cenários possíveis por quatro, e a terceira pessoa que mexer nesse trecho não vai adivinhar por que aquela combinação específica existe nem se é segura de remover isoladamente.

Comparando os três jeitos comuns de gerenciar flags

Abordagem Onde vive a regra Facilidade de remover Quando compensa
Variável de ambiente Deploy/infra Alta — some no próximo deploy Kill switch simples, poucos ambientes
Config em banco próprio Tabela relacional Média — exige migração para limpar Regras de negócio por cliente/plano
Provedor de feature flag (SaaS ou self-hosted) Serviço externo com SDK Baixa se não houver disciplina — alta se houver processo de expiração Times grandes, experimentos, rollout gradual por percentual

Um provedor dedicado resolve o problema de visibilidade — mostra todas as flags ativas em um painel único, com quem criou e quando — mas não resolve o problema de disciplina sozinho. Ele só transforma “flag esquecida no código” em “flag esquecida no painel”, o que é mais fácil de auditar, não impossível de acontecer. A ferramenta ajuda a enxergar o problema; não substitui o processo que decide o que fazer com o que foi visto.

O processo que mantém o inventário sob controle

A prática que resolve o problema na origem é simples de descrever e difícil de manter sem ferramenta: toda flag nasce com uma data de revisão e um responsável nomeado, não só um nome técnico solto no código. Um job semanal — ou uma linha fixa no ritual de planejamento — lista as flags com mais de trinta dias ativas e cobra decisão explícita: vira permanente e documentada como configuração de negócio, ou é removida esta semana, sem meio-termo indefinido.

Equipes que resolvem isso de verdade costumam ter uma regra dura: nenhuma flag nova é aprovada em revisão de código sem a condição de remoção escrita no mesmo pull request — não “depois eu documento”, mas a frase exata que vai justificar apagar aquele código quando a hora chegar. Quando a condição se cumpre, a remoção é uma tarefa mecânica de baixa prioridade, não uma decisão nova que exige investigar o histórico inteiro da flag pra entender se ainda é segura de tirar.

Comentários

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *