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.
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.