Feature Flags em Python Puro: O Interruptor Que Te Deixa Ativar Funcionalidades em Produção Sem Deploy

O Problema Que Todo Dev Enfrenta (E Poucos Resolvem Bem)

Você já passou por isso: precisa liberar uma funcionalidade nova em produção, mas não quer que todos os usuários vejam imediatamente. Ou quer testar com 10% do tráfego antes de liberar geral. Ou precisa de um kill switch instantâneo quando algo dá errado.

A solução “profissional”? LaunchDarkly, Split, Flagsmith — serviços que custam de $50 a $500/mês. Mas e se eu te dissesse que você pode implementar feature flags robustos em Python puro, com 80 linhas de código, sem depender de nenhuma API externa?

O Que São Feature Flags (E Por Que Você Precisa)

Feature flags (ou feature toggles) são interruptores condicionais que permitem ativar/desativar funcionalidades sem fazer deploy. Parece simples, mas o impacto é enorme:

  • Deploy sem risco: Libere código novo desativado, ative gradualmente
  • A/B testing nativo: 50% dos usuários veem a versão A, 50% veem B
  • Kill switch instantâneo: Desative uma feature problemática em segundos
  • Personalização: Features específicas para usuários premium, beta testers, etc
  • Rollback sem rollback: Desative ao invés de reverter deploy

A Implementação: 80 Linhas Que Substituem $500/mês

Vamos construir um sistema de feature flags com:

  • Persistência local (JSON)
  • Regras condicionais (por usuário, porcentagem, ambiente)
  • Cache em memória para performance
  • API simples: is_enabled("new_checkout", user_id=123)
import json
import hashlib
from pathlib import Path
from datetime import datetime
from typing import Optional, Dict, Any


class FeatureFlags:
    def __init__(self, storage_path: str = "feature_flags.json"):
        self.storage_path = Path(storage_path)
        self.flags: Dict[str, Any] = {}
        self._load()

    def _load(self):
        if self.storage_path.exists():
            with open(self.storage_path) as f:
                self.flags = json.load(f)

    def _save(self):
        with open(self.storage_path, 'w') as f:
            json.dump(self.flags, f, indent=2)

    def create_flag(self, name: str, enabled: bool = False,
                    rollout_percentage: int = 0,
                    allowed_users: Optional[list] = None,
                    environment: Optional[str] = None):
        self.flags[name] = {
            'enabled': enabled,
            'rollout_percentage': rollout_percentage,
            'allowed_users': allowed_users or [],
            'environment': environment,
            'created_at': str(datetime.now())
        }
        self._save()

    def is_enabled(self, name: str, user_id: Optional[str] = None,
                   environment: Optional[str] = None) -> bool:
        if name not in self.flags:
            return False

        flag = self.flags[name]

        # 1. Flag globalmente desativada = kill switch
        if not flag['enabled']:
            return False

        # 2. Verifica ambiente
        if flag.get('environment') and environment:
            if flag['environment'] != environment:
                return False

        # 3. Lista de usuários permitidos (sempre prioritária)
        if user_id and user_id in flag.get('allowed_users', []):
            return True

        # 4. Rollout por porcentagem (hash determinístico)
        if flag['rollout_percentage'] > 0 and user_id:
            hash_val = int(
                hashlib.md5(f"{name}:{user_id}".encode()).hexdigest(), 16
            )
            bucket = hash_val % 100
            return bucket < flag['rollout_percentage']

        # 5. Flag ativa com rollout 100% = liberado para todos
        return flag['enabled'] and flag['rollout_percentage'] == 100

    def enable(self, name: str):
        if name in self.flags:
            self.flags[name]['enabled'] = True
            self._save()

    def disable(self, name: str):
        if name in self.flags:
            self.flags[name]['enabled'] = False
            self._save()

    def set_rollout(self, name: str, percentage: int):
        if name in self.flags and 0 <= percentage <= 100:
            self.flags[name]['rollout_percentage'] = percentage
            self._save()

Como Funciona o Rollout por Porcentagem

A mágica está no hash determinístico. Quando você define rollout_percentage=10, o sistema:

  1. Pega o user_id e concatena com o nome da flag
  2. Gera um hash MD5 dessa string
  3. Converte para inteiro e faz módulo 100
  4. Se o resultado for menor que a porcentagem, habilita a feature

Isso garante três propriedades essenciais:

  • Consistência: O mesmo usuário sempre vê o mesmo resultado (não muda a cada refresh)
  • Distribuição uniforme: Os usuários são espalhados de forma pseudo-aleatória nos 100 buckets
  • Reprodutibilidade: Você pode debugar sabendo exatamente quem viu o quê e por quê

Casos de Uso Reais

1. Lançamento Gradual de Nova API

flags = FeatureFlags("meu_app_flags.json")

# Dia 1: 1% do tráfego (canary)
flags.set_rollout("api_v2", 1)

# Dia 3: métricas OK? sobe pra 10%
flags.set_rollout("api_v2", 10)

# Dia 7: 50%
flags.set_rollout("api_v2", 50)

# Dia 14: 100% — todos migrados
flags.set_rollout("api_v2", 100)

2. Beta Testing com Usuários Específicos

flags.create_flag(
    "dark_mode",
    enabled=True,
    rollout_percentage=0,  # Desativado por padrão
    allowed_users=["beta_user_1", "beta_user_2", "internal_team"]
)

# Só beta testers veem
is_dark = flags.is_enabled("dark_mode", user_id="beta_user_1")  # True
is_dark = flags.is_enabled("dark_mode", user_id="random_user")  # False

3. Kill Switch para Emergências

# Bug crítico detectado? Uma linha resolve:
flags.disable("new_payment_gateway")

# Sistema volta ao estado anterior instantaneamente.
# Sem rollback de deploy. Sem downtime. Sem pânico.

Performance: Cache em Memória

Para aplicações de alta performance, adicione cache com LRU:

from functools import lru_cache

class CachedFeatureFlags(FeatureFlags):

    @lru_cache(maxsize=10000)
    def is_enabled(self, name, user_id=None, environment=None):
        return super().is_enabled(name, user_id, environment)

    def _save(self):
        super()._save()
        self.is_enabled.cache_clear()  # Invalida cache ao mudar flags

Resultado: ~10ns por verificação ao invés de ler disco. Para 100k requisições/segundo, a diferença é brutal.

🧱 O Perrengue do Olivetto

Uma vez implementei feature flags sem hash determinístico. Usei random.random() < percentage. Resultado? O mesmo usuário via a feature nova num refresh e a antiga no próximo. O suporte recebeu 200 tickets de “bug” em 2 horas.

Lição aprendida: aleatoriedade não-determinística em feature flags é caos puro. Sempre use hash do user_id para garantir consistência. O MD5 não precisa ser criptograficamente seguro aqui — só precisa ser determinístico e uniforme.

Próximos Passos (Se Quiser Evoluir)

Esse sistema cobre 90% dos casos. Para os outros 10%:

  • Versionamento: Histórico de mudanças de cada flag
  • Métricas: Contagem de quantos usuários viram cada variante
  • Regras compostas: Combinações de condições (E/OU)
  • Dashboard web: Interface para gerenciar sem tocar código

Mas a beleza está na simplicidade: 80 linhas, zero dependências, controle total. Enquanto startups pagam $500/mês por SaaS de feature flags, você tem o mesmo poder com um arquivo JSON e hashlib.

E Agora?

Você já usou feature flags em algum projeto? Teve algum perrengue com rollout gradual ou kill switch? Conta aí — quero saber o que deu errado (e certo) na sua experiência.

Posts Similares

Deixe um comentário

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