Feature flags

Escopo: transversal. Aplica-se a qualquer linguagem ou stack do projeto.

Uma feature flag (interruptor de funcionalidade) permite que o código esteja em produção com a funcionalidade desligada. O deploy deixa de ser o momento em que o usuário passa a ver a novidade, e esses dois eventos podem acontecer em dias diferentes.

Isso muda três coisas na prática. O código incompleto pode ser integrado sem esperar a feature ficar pronta. A feature pode ser ligada para 1% dos usuários antes de chegar aos 100%. E a feature que deu problema pode ser desligada na hora, sem esperar um rollback de deploy.

Esta página aprofunda o vocabulário que ci-cd.md esboça na seção de deploy vs release. A flag como caso particular de configuração dinâmica aparece em configuration.md.

Conceitos fundamentais

ConceitoO que é
Feature (funcionalidade)Comportamento visível ao usuário, cujo ciclo de vida é independente do ciclo de deploy do código
Deploy (implantação)Ato técnico de colocar o código em produção; não implica que a feature esteja visível ao usuário
Toggle (interruptor)Mecanismo que habilita ou desabilita uma feature sem deploy
Rollout (ativação gradual)Estratégia de ativar uma feature progressivamente para subconjuntos de usuários
Dark launch (ativação invisível)Executar o novo código em produção sem expor o resultado ao usuário
Kill switch (chave de emergência)Flag que desativa uma feature problemática imediatamente, sem deploy ou rollback
Runtime (tempo de execução)Ponto de avaliação da flag a cada requisição, permitindo mudança sem restart
TTL (Time To Live · tempo de validade)Tempo durante o qual o valor de uma flag em cache local é considerado válido

Quatro propósitos diferentes para uma flag

As flags servem a fins distintos, e o mesmo mecanismo acaba abrigando todos eles. A regra de negócio permanente fica na mesma lista do experimento que dura duas semanas, e ninguém consegue mais dizer quais podem ser apagadas. Quatro categorias cobrem a maioria dos casos:

CategoriaPropósitoVida útil
ReleaseEsconder código não terminado em produçãoDias a semanas, removida na release
ExperimentA/B test, medir efeito de variaçãoEnquanto o teste roda, removida na decisão
OpsKill switch, controle de carga, throttling (limitação de taxa)Semi-permanente, usada em incidentes
PermissionHabilitar feature por plano, role (perfil de acesso), entitlement (permissão por plano)Permanente, parte do modelo de negócio

Cada categoria tem prazo de validade próprio. A flag de release existe para ser apagada assim que o rollout termina. A de permission faz parte do modelo de negócio e fica para sempre. Registrar a categoria no momento da criação é o que torna a limpeza possível depois.


Ativar aos poucos, de ninguém até todos

A flag aceita mais estados que ligado e desligado. O rollout controla quem vê a feature, em que proporção e em que ordem:

off              →  ninguém vê
internal         →  apenas contas do time e QA
beta             →  segmento opt-in (adesão voluntária) de clientes
% gradual        →  1%10%50%100%
segmentado       →  por plano, geografia, role, feature entitlement
on               →  todos veem

A progressão usual é interno → beta → gradual → total. Cada degrau é uma oportunidade de olhar as métricas (taxa de erro, latência, conversão) com um público pequeno antes de ampliar. Quem pula de 0% direto para 100% abre mão dessa observação, e a flag vira um if a mais no código sem nenhum benefício de release controlada.


Rodar o código novo sem o usuário ver

O dark launch coloca o código novo em produção e esconde o resultado. A lógica executa de verdade, com tráfego de verdade, e o que ela devolve fica em log, vai para comparação ou é descartado.

Três formas típicas:

  • Shadow: a requisição vai para o novo código e para o antigo; a resposta retornada ao usuário é a do antigo. O time compara os dois outputs offline.
  • Silent metrics: o novo código emite métricas em produção (tempo, taxa de erro) sem afetar o fluxo. Valida performance sob carga real.
  • Write-to-shadow: a escrita acontece no sistema antigo e em um buffer (área temporária) do novo; consistência é verificada antes de promover.

Vale o esforço quando o caminho é crítico: migração de banco, reescrita de cálculo financeiro, troca de provedor de pagamento. Implementar o dark launch custa tempo de engenharia, e esse tempo é menor que o de um incidente com os dados dos clientes.


Desligar a feature em segundos durante um incidente

O kill switch desativa uma feature na hora, sem abrir PR (Pull Request · Pedido de Integração), sem rebuild (recompilação) e sem deploy. Ele existe para o momento em que a taxa de erro dispara, a latência estoura o limite ou uma métrica de negócio despenca.

Ele se comporta de forma diferente de uma flag de release em três pontos:

  • Vive para sempre como parte do código operacional, e continua no lugar depois que o rollout termina.
  • É acionado pelo time de operação, no meio do incidente.
  • Cai em um comportamento seguro: voltar ao caminho antigo, devolver um erro explícito ou servir uma resposta reduzida. A requisição sempre recebe uma resposta.

Toda feature nova de alto risco (escrita, cobrança, integração externa) nasce com kill switch no primeiro deploy. Escrever o switch durante o incidente adiciona um ciclo de build e deploy a um sistema que já está quebrado.


Onde a flag é avaliada

São três momentos possíveis, e cada um troca latência por consistência de um jeito diferente:

PontoLatênciaConsistênciaExemplo de uso
Build-time (em tempo de compilação)Zero (código morto removido)TotalFeatures experimentais que nunca entram em produção
Startup (na inicialização)Zero em runtimePor instânciaFeatures de infraestrutura que não mudam durante a vida do processo
Runtime (em tempo de execução)Custo por avaliaçãoPor requisiçãoRollout gradual, kill switch, experimentos

A maioria das flags úteis é avaliada em runtime, porque é o único ponto em que o valor muda sem reiniciar o processo. É o que torna possível ajustar o rollout de 10% para 50% e acionar o kill switch enquanto o incidente acontece.

O custo por avaliação importa. Consultar um serviço externo a cada chamada de função adiciona uma ida à rede dentro do caminho quente. O padrão é o cache local com TTL (Time To Live · tempo de validade) curto: o cliente da flag sincroniza com o backend a cada poucos segundos e responde ao código a partir da memória. Ver performance.md, seção Cache.


Como a flag aparece no código

A forma do condicional decide se a flag vai ser fácil de remover depois. Três padrões, do pior ao melhor:

1. inline (embutido no fluxo) espalhado (pior)

Duplica o fluxo inteiro dentro de um if, e remover a flag depois exige reler as 160 linhas para decidir o que fica.

if (flags.isNewCheckoutEnabled) {
  ... 80 linhas do novo fluxo ...
} else {
  ... 80 linhas do antigo ...
}

2. isolamento por função (aceitável)

Cobre a maior parte dos casos. A flag escolhe qual implementação usar, e as duas são módulos independentes com a mesma interface.

const checkout = flags.isNewCheckoutEnabled ? newCheckout : legacyCheckout;
checkout.process(order);

3. strategy (estratégia) com registro (escalável)

Vale quando há mais de duas variantes, ou quando a escolha depende de vários fatores combinados.

const strategy = checkoutStrategies[flags.checkoutVariant];
strategy.process(order);

A regra vale para os três: a flag escolhe o caminho, e o caminho escolhido ignora que uma flag existe. Quem chama escreve checkout.process(order) sem if em volta. Quando a nova implementação vence, remover a flag é apagar a linha da escolha e manter a implementação inteira.


Toda flag tem prazo de validade

A flag que ninguém remove deixa três coisas para trás: o condicional no código, a entrada no painel de configuração e a dúvida de quem lê o arquivo e precisa descobrir por que aquele caminho alternativo existe. Depois de alguns meses, o time não consegue mais responder quantas flags estão ativas nem quais são seguras de apagar.

Três práticas seguram isso:

  • Prazo explícito: no momento de criar, registrar a data de remoção esperada. Release flag: dias. Experimento: duração do teste. Ops: permanente, registrar como tal.
  • Inventário auditável: listar todas as flags vivas, dono, propósito, última mudança. Sem inventário, ninguém sabe o que pode sair.
  • Cleanup como tarefa explícita: no momento que o rollout chega a 100%, a remoção da flag entra no backlog (lista de tarefas pendentes) com prioridade. Adiar é criar lixo de longo prazo.

O sinal de que a gestão saiu do controle é uma lista com dezenas de flags ligadas em 100% há meses. Elas guardam decisões que já foram tomadas, e o sistema de flags parou de servir para controlar release.


Flags e testes

O código com flag tem pelo menos dois caminhos, e os dois precisam de teste. Dois padrões funcionam:

  • Teste por variante: cada teste roda com uma configuração de flag específica, passada via fixture (dado de teste pré-definido). O flag service em teste é injetado com valores fixos.
  • Teste do caminho novo em isolamento: a nova implementação é testada sem flag, como qualquer outra unidade. O teste da escolha (qual caminho é ativado) é um teste separado, menor.

Ler o flag service real dentro do teste quebra os dois padrões. O resultado do teste passa a depender de uma configuração que vive fora do repositório, e alguém mudando o rollout em produção faz a suíte falhar sem que nenhuma linha de código tenha mudado.


Referência rápida

DecisãoRegra
Esconder código não terminado em produçãoRelease flag, remover após rollout
Medir variação de comportamentoExperiment flag, remover após decisão
Desligar feature em incidenteKill switch, permanente, acionado por ops
Habilitar por plano/rolePermission flag, permanente
Progressão de rolloutInterno → beta → % gradual → total
Validar feature de risco antes do usuário verDark launch (shadow, silent metrics)
Avaliação da flagRuntime com cache local, TTL curto
Estrutura no códigoFlag escolhe implementação, implementação não sabe
Flag ligada em 100% há tempoAgendar remoção, manter inventário auditável
Como testarFixture com valores fixos; nunca ler flag service em teste
Deploy vs release, visão geralVer ci-cd.md
Flag como config dinâmicaVer configuration.md seção "Mudanças em runtime"

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.