Engenharia, arquitetura e performance — direto de quem constrói
início/ blog/ artigo
Feature Flags · 5 min de leitura

Feature Flags e Multi-Tenancy: Como Testo em Produção Sem Quebrar Clientes

Como uso feature flags para mandar código não finalizado para produção sem PRs gigantes, e como a arquitetura multi-tenant me deixa ativar features só em um tenant de teste — testando em produção sem afetar os outros clientes.

TT
TryTechDesenvolvimento de Software

O problema: PRs gigantes e branches que apodrecem

Todo mundo que já trabalhou em uma feature grande conhece a sensação. Você abre uma branch, começa a implementar, e três semanas depois tem um PR com 4.000 linhas alteradas em 60 arquivos. Ninguém revisa aquilo com atenção. A branch já divergiu tanto da main que fazer merge virou um exercício de resolver conflitos por horas.

Pior ainda: enquanto a feature não está pronta, ela vive isolada numa branch. Ela não integra com o resto do sistema, não roda no ambiente de produção, não recebe dados reais. Você só descobre que quebrou alguma coisa no dia do merge — o pior dia possível para descobrir.

A raiz do problema é uma confusão comum: fazer deploy de código e liberar uma feature são coisas diferentes. A gente trata as duas como se fossem a mesma coisa, e é aí que mora o sofrimento.


A ideia: desacoplar deploy de release

Feature flags (ou feature toggles) resolvem exatamente isso. A ideia é simples: você envolve o código novo em um condicional que consulta uma flag. Se a flag está desligada, o código novo não roda — mesmo estando em produção.

if (features.isEnabled("novo-checkout")) {
  return renderNovoCheckout(cart);
}
return renderCheckoutAntigo(cart);

Com isso, eu consigo mandar código não finalizado para a main e para produção com a flag desligada. O código está lá, deployado, convivendo com o resto do sistema — mas invisível para os usuários. Isso muda tudo:

  • PRs pequenas e frequentes. Em vez de um PR de 4.000 linhas, faço dez PRs de 400 linhas ao longo de duas semanas. Cada um passa por review de verdade, faz merge rápido e nunca diverge muito da main.
  • Integração contínua de verdade. O código novo já está integrado com o sistema desde o primeiro commit. Não existe o “dia do merge” traumático.
  • Kill switch instantâneo. Se algo der errado com a feature já ativa, eu desligo a flag. Não preciso de rollback, redeploy, nada. Um toggle e voltou ao estado anterior.

Onde a flag mora

A implementação pode ser tão simples ou sofisticada quanto você quiser. No começo, uma flag pode ser só uma variável de ambiente ou uma linha numa tabela de configuração:

type FeatureContext = {
  tenantId: string;
  userId?: string;
};

class FeatureFlags {
  async isEnabled(flag: string, ctx: FeatureContext): Promise<boolean> {
    const config = await this.loadFlag(flag);
    if (!config) return false;

    // Globalmente ligada
    if (config.enabledGlobally) return true;

    // Ligada para tenants específicos
    if (config.enabledTenants?.includes(ctx.tenantId)) return true;

    return false;
  }
}

O ponto importante é que a decisão é feita em runtime, consultando um estado que eu controlo sem precisar fazer deploy. Ligar ou desligar uma flag é uma operação de dados, não de código.


O superpoder do multi-tenant: testar em produção de verdade

Aqui entra a parte que, para o meu caso, é a mais valiosa. Meu sistema é fortemente multi-tenant — vários clientes compartilham a mesma aplicação e a mesma infraestrutura, isolados logicamente por um tenantId.

Isso significa que a feature flag não precisa ser um interruptor global de liga/desliga. Ela pode ser segmentada por tenant. E é exatamente isso que me permite fazer algo que soa perigoso, mas na prática é muito seguro: testar em produção.

Eu mantenho um tenant de teste — um cliente fictício que vive no mesmo banco, na mesma aplicação, nos mesmos servidores que os clientes reais. Quando termino uma feature, eu ativo a flag apenas para esse tenant:

// Flag ligada só para o tenant de teste
{
  flag: "novo-checkout",
  enabledGlobally: false,
  enabledTenants: ["tenant-qa-interno"]
}

Agora a feature roda em produção — infraestrutura real, dados reais no formato real, latência real, integrações reais com gateways de pagamento e serviços externos. Mas ela só está visível para o tenant de teste. Os outros clientes continuam vendo exatamente o que viam antes, sem saber que existe código novo rodando ao lado deles.

graph TB
  subgraph prod["Produção — mesma aplicação, mesmo banco"]
    F{{"feature flag: novo-checkout"}}
    T1["Tenant A (cliente real)"]
    T2["Tenant B (cliente real)"]
    T3["Tenant C (cliente real)"]
    QA["Tenant QA (teste)"]
  end

  F -->|"desligada"| T1
  F -->|"desligada"| T2
  F -->|"desligada"| T3
  F -->|"ligada"| QA

Quando eu ganho confiança de que a feature está sólida, faço um rollout gradual: ativo para um cliente piloto que topou testar, depois para 10% dos tenants, depois para todos. Se algum problema aparecer no meio do caminho, eu desligo a flag para aquele segmento e investigo — sem afetar quem já estava feliz.


Por que isso é melhor que um staging tradicional

Um ambiente de staging tenta imitar produção, mas nunca chega lá. O volume de dados é menor, as integrações externas costumam estar em modo sandbox, a configuração diverge com o tempo. Bugs que só aparecem com dados reais e escala real passam despercebidos.

Testar em produção com um tenant de teste elimina essa distância. Não tem “imitação”: é o ambiente real, com um blast radius controlado por design. O isolamento por tenant, que a aplicação já garante para separar os clientes, vira também a fronteira de segurança dos meus testes.

Claro, isso exige disciplina. Algumas regras que sigo:

  • O tenant de teste nunca dispara efeitos colaterais reais para terceiros — e-mails, cobranças de verdade, webhooks para produção alheia. Uso contas de sandbox nos serviços externos para esse tenant.
  • Toda flag tem dono e data de validade. Feature flag esquecida vira dívida técnica. Depois que a feature está 100% liberada, eu removo a flag e o código morto do caminho antigo.
  • A flag é observável. Logs e métricas registram qual caminho de código rodou, para eu saber o que aconteceu quando algo dá errado.

O resultado

Trocar “branch longa + PR gigante + dia do merge” por “PRs pequenas + código atrás de flag + teste em tenant real” mudou completamente o meu ritmo. Eu entrego mais rápido, com menos medo, e com um mecanismo de reversão que custa um clique em vez de um redeploy.

Feature flags não são mágica — elas transferem parte da complexidade do processo de branch para o runtime da aplicação. Mas quando o sistema já é multi-tenant, você ganha de graça a peça mais valiosa: um lugar seguro dentro da própria produção para ver o código novo funcionando de verdade, antes de qualquer cliente real encostar nele.