Feature flags deixaram de ser luxo de empresas grandes. Em 2026, qualquer time que publica aplicativo Android, API backend ou produto SaaS com Kotlin precisa separar deploy de lançamento. Deploy é colocar código novo em produção. Lançamento é liberar comportamento novo para usuários reais. Quando essas duas coisas acontecem sempre juntas, cada mudança vira aposta: se algo quebra, o rollback precisa desfazer todo o deploy, mesmo que o problema esteja em apenas uma tela, endpoint ou regra de negócio.
Com feature flags, o time consegue publicar código inativo, liberar para um grupo pequeno, medir erro e conversão, desligar rapidamente uma funcionalidade problemática e fazer rollout progressivo sem criar branch eterno. Para quem trabalha com Android offline-first, WorkManager, Kotlin para backend ou CI/CD em Kotlin, esse padrão é especialmente útil porque reduz o risco operacional sem travar a cadência de entrega.
O ponto importante: feature flag não é apenas um if perdido no código. Ela precisa de nome claro, dono, tipo, data de remoção, valor padrão seguro, testes e observabilidade. Caso contrário, o projeto troca risco de deploy por dívida técnica invisível.
O que é uma feature flag?
Uma feature flag é uma decisão configurável que altera o comportamento do software sem exigir novo build ou novo deploy. Em vez de publicar uma versão diferente para cada público, o mesmo código consulta uma configuração e decide qual caminho executar.
Um exemplo mínimo em Kotlin:
interface FeatureFlags {
fun novaTelaCheckout(): Boolean
fun usarGatewayPixV2(): Boolean
}
class CheckoutService(
private val flags: FeatureFlags,
private val checkoutLegado: CheckoutLegado,
private val checkoutNovo: CheckoutNovo,
) {
suspend fun finalizarPedido(pedido: Pedido): ResultadoCheckout {
return if (flags.novaTelaCheckout()) {
checkoutNovo.finalizar(pedido)
} else {
checkoutLegado.finalizar(pedido)
}
}
}
Esse exemplo parece simples, mas já mostra a regra principal: a flag fica atrás de uma interface, não espalhada em chamadas diretas para Firebase Remote Config, LaunchDarkly, Unleash, banco de dados ou arquivo YAML. Essa camada evita acoplamento e facilita testes.
Tipos de feature flags
Nem toda flag tem a mesma finalidade. Misturar todos os casos no mesmo padrão costuma gerar confusão.
Release flag controla uma funcionalidade nova durante rollout. Deve ser temporária. Exemplo: liberar uma tela de checkout redesenhada para 10% dos usuários.
Ops flag permite desligar comportamento em produção sem rollback. Exemplo: desativar geração de relatório pesado quando um fornecedor externo está instável.
Experiment flag divide usuários em variantes para teste A/B. Exemplo: comparar dois textos de onboarding medindo ativação.
Permission flag libera recurso por plano, perfil ou autorização. Exemplo: habilitar exportação CSV apenas para contas Pro.
Migration flag ajuda a mover tráfego de uma implementação para outra. Exemplo: trocar gradualmente um endpoint Java legado por um serviço Kotlin com Ktor ou Spring Boot.
A diferença importa porque cada tipo tem ciclo de vida diferente. Release flags devem morrer rápido. Ops flags podem ficar mais tempo, mas precisam ser bem documentadas. Experiment flags precisam de métrica e amostragem consistente. Permission flags quase sempre viram regra de produto permanente.
Feature flags no Android com Kotlin
No Android, feature flags aparecem em três lugares: configuração remota, cache local e UI. O app precisa funcionar mesmo sem rede, então o valor da flag não pode depender de uma chamada síncrona no momento em que a tela abre. Uma estratégia comum é baixar configurações em background, salvar o último valor conhecido e usar um padrão seguro quando nada foi carregado ainda.
Para flags simples, DataStore Preferences funciona bem como cache local:
class AndroidFeatureFlags(
private val dataStore: DataStore<Preferences>,
) : FeatureFlags {
private val NOVO_CHECKOUT = booleanPreferencesKey("novo_checkout")
private val PIX_V2 = booleanPreferencesKey("pix_v2")
suspend fun atualizar(remote: RemoteFlags) {
dataStore.edit { prefs ->
prefs[NOVO_CHECKOUT] = remote.novoCheckout
prefs[PIX_V2] = remote.pixV2
}
}
override fun novaTelaCheckout(): Boolean {
error("Prefira expor Flow<Boolean> ou snapshot carregado no repository")
}
fun novaTelaCheckoutFlow(): Flow<Boolean> =
dataStore.data.map { prefs -> prefs[NOVO_CHECKOUT] ?: false }
}
Em apps reais, evite bloquear a UI esperando configuração remota. Carregue flags durante inicialização, sincronize com WorkManager quando fizer sentido e trate o valor padrão como decisão de produto. Para uma tela crítica, o padrão deve ser conservador. Se a nova experiência falhar, o usuário deve cair no fluxo estável.
Em Jetpack Compose, conecte a flag como estado vindo do ViewModel:
@Composable
fun CheckoutRoute(viewModel: CheckoutViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
if (uiState.novoCheckoutAtivo) {
NovoCheckoutScreen(uiState)
} else {
CheckoutLegadoScreen(uiState)
}
}
O cuidado aqui é não transformar a árvore Compose em um labirinto de if. Quando a diferença é grande, prefira separar telas ou componentes inteiros. Quando a diferença é pequena, talvez a flag pertença a um parâmetro de configuração, não à composição inteira.
Feature flags no backend Kotlin
No backend, as flags normalmente entram em controllers, services, consumers de fila e jobs. A regra é parecida: use uma interface própria e mantenha o provedor externo isolado.
Em Spring Boot com Kotlin:
@ConfigurationProperties(prefix = "features")
data class FeatureProperties(
val antifraudeV2: Boolean = false,
val relatorioAssincrono: Boolean = false,
)
@Component
class BackendFeatureFlags(
private val properties: FeatureProperties,
) {
fun antifraudeV2(): Boolean = properties.antifraudeV2
fun relatorioAssincrono(): Boolean = properties.relatorioAssincrono
}
@Service
class PagamentoService(
private val flags: BackendFeatureFlags,
private val antifraudeV1: AntifraudeV1,
private val antifraudeV2: AntifraudeV2,
) {
suspend fun autorizar(pagamento: Pagamento): DecisaoAntifraude {
return if (flags.antifraudeV2()) {
antifraudeV2.avaliar(pagamento)
} else {
antifraudeV1.avaliar(pagamento)
}
}
}
Esse modelo com properties resolve casos simples. Para rollout por usuário, empresa, região ou percentual, você provavelmente usará uma plataforma específica ou uma tabela de configuração. Mesmo assim, mantenha a interface do domínio estável. O código de pagamento não precisa saber de onde vem a decisão.
Em Ktor, a mesma ideia pode ser injetada no módulo da aplicação:
fun Application.module() {
val flags = EnvironmentFeatureFlags(environment.config)
routing {
post("/pagamentos") {
val request = call.receive<PagamentoRequest>()
val resposta = if (flags.antifraudeV2()) {
processarComNovoMotor(request)
} else {
processarComMotorAtual(request)
}
call.respond(resposta)
}
}
}
Para endpoints críticos, registre logs e métricas com o nome da flag e a variante escolhida. Isso ajuda a responder perguntas práticas: a variante nova aumentou latência? A taxa de erro subiu em uma região? O comportamento falhou apenas para usuários de um plano específico?
Rollout gradual sem perder controle
Um rollout seguro começa pequeno. Um fluxo comum:
- Publicar o código com a flag desligada.
- Ativar para ambiente interno ou equipe de QA.
- Liberar para 1% dos usuários elegíveis.
- Medir erro, latência, crash rate, conversão e chamados de suporte.
- Aumentar para 5%, 25%, 50% e 100% se os indicadores estiverem saudáveis.
- Remover o caminho antigo quando a nova implementação estiver consolidada.
A etapa 6 é a mais esquecida. Flag temporária que nunca morre vira complexidade permanente. Depois de alguns meses, ninguém sabe se pode remover o fluxo antigo, testes precisam cobrir combinações demais e cada refatoração fica mais arriscada.
Uma prática útil é registrar metadados da flag no próprio código ou em um catálogo:
data class FeatureFlagMetadata(
val key: String,
val owner: String,
val type: FlagType,
val createdAt: LocalDate,
val removeAfter: LocalDate?,
)
enum class FlagType {
RELEASE,
OPS,
EXPERIMENT,
PERMISSION,
MIGRATION,
}
Esse catálogo não precisa ser sofisticado no começo. O importante é responder três perguntas: quem é responsável pela flag, por que ela existe e quando deve ser removida.
Testando código com feature flags
Feature flags aumentam o número de caminhos possíveis. Portanto, teste explicitamente o comportamento ligado e desligado.
class FakeFeatureFlags(
private val novoCheckout: Boolean,
) : FeatureFlags {
override fun novaTelaCheckout(): Boolean = novoCheckout
override fun usarGatewayPixV2(): Boolean = false
}
class CheckoutServiceTest {
@Test
fun `usa checkout novo quando flag esta ligada`() = runTest {
val service = criarService(flags = FakeFeatureFlags(novoCheckout = true))
val resultado = service.finalizarPedido(pedidoValido())
assertEquals("novo", resultado.origem)
}
@Test
fun `mantem checkout legado quando flag esta desligada`() = runTest {
val service = criarService(flags = FakeFeatureFlags(novoCheckout = false))
val resultado = service.finalizarPedido(pedidoValido())
assertEquals("legado", resultado.origem)
}
}
Nem toda combinação precisa de teste end-to-end. O segredo é testar decisões perto da camada onde elas acontecem. Services testam regra de negócio. ViewModels testam estado de UI. Controllers testam contrato HTTP. Testes de interface devem focar nos fluxos mais importantes, não em todas as permutações possíveis.
Também vale automatizar uma verificação de flags vencidas. Um teste simples pode falhar quando removeAfter passou da data atual. Isso força o time a tomar decisão: remover, renovar justificativa ou transformar em permissão permanente.
Boas práticas para 2026
Use nomes explícitos. novoCheckout é melhor que flag1, mas checkoutPixComAntifraudeV2 pode ser ainda mais claro se a mudança for específica. Evite nomes que só fazem sentido para quem participou da reunião original.
Defina valor padrão seguro. Em Android, o app pode abrir sem configuração remota. Em backend, o provedor de flags pode estar indisponível. O sistema precisa saber qual comportamento usar nesses casos.
Não coloque lógica de negócio dentro da plataforma de flags. A plataforma decide variante. A regra do domínio continua no código Kotlin, com testes, revisão e versionamento.
Monitore por variante. Se todos os logs, métricas e traces ignoram a flag, você não consegue saber se o rollout novo está saudável. Combine essa disciplina com observabilidade em Kotlin e, em sistemas distribuídos, com tracing via OpenTelemetry.
Remova flags antigas. Esse é o ponto que separa maturidade de improviso. Feature flag boa é aquela que permite lançar com segurança e depois desaparece quando não é mais necessária.
Erros comuns
O primeiro erro é usar feature flag para esconder código inacabado sem prazo. Isso cria uma falsa sensação de segurança. Código inativo ainda compila, envelhece, interage com dependências e pode confundir futuras refatorações.
O segundo é consultar a flag em lugares demais. Se a mesma decisão aparece em controller, repository, ViewModel, componente Compose e worker, o comportamento pode ficar inconsistente. Centralize a decisão em uma camada clara.
O terceiro é não pensar em dados. Uma flag que muda o formato salvo em banco, o schema de uma mensagem Kafka ou a estrutura de cache precisa de estratégia de migração. Desligar a flag talvez não desfaça dados já gravados.
O quarto é confundir experimento com permissão. Teste A/B mede hipótese temporária. Permissão controla acesso de produto. Misturar os dois dificulta análise e cobrança.
Conclusão
Feature flags são uma das práticas mais úteis para times Kotlin que querem entregar rápido sem transformar produção em roleta. Elas ajudam no Android, no backend, em migrações, em experimentos e em operações críticas. Mas o ganho só aparece quando a implementação vem com disciplina: interface própria, valor padrão seguro, rollout gradual, testes para ligado e desligado, observabilidade por variante e limpeza de flags antigas.
Para começar pequeno, escolha uma funcionalidade nova de risco moderado, coloque atrás de uma release flag, publique desligada, ative para um público interno e acompanhe métricas por alguns dias. Depois avance o rollout e remova o caminho antigo. Esse ciclo simples já muda a cultura do time: deploy deixa de ser evento tenso e vira uma etapa controlada de entrega contínua.
Se você está montando uma stack Kotlin mais madura, combine feature flags com CI/CD, testes automatizados, observabilidade e arquitetura modular. Para comparar práticas de rollout em outras stacks, também vale estudar como comunidades de Go e Python tratam deploy progressivo, automação e operação de serviços em produção.