Sistema de Feature Flags — Documentação Técnica
Documentação em construção. Público-alvo: devs do time (uso técnico/API).
Sistema avançado: rollout percentual, segmentação de usuários e A/B test.
Índice
- Visão geral
- Core de flags
- Targeting e segmentação
- Rollout progressivo
- A/B testing / Experimentação
- SDKs e integração técnica
- Autenticação (SDK Key)
- Sincronização em tempo real
- Eventos de conexão
- Webhooks (integração externa)
- Fluxo completo de evaluate()
- Decisões de arquitetura
- Modelo de dados
- Em aberto
Visão geral
Sistema de feature flags construído do zero, com suporte a:
- Flags booleanas e multivariáveis
- Segmentação por atributos de usuário
- Rollout percentual com bucketing consistente
- A/B testing com múltiplas variantes
- SDKs client-side e server-side
Core de flags
- Tipos de flag: boolean (on/off) e multivariável (string/número/JSON), permitindo retornar variantes diferentes, não só ligar/desligar
- Ambientes: dev, staging, produção — cada um com seu próprio estado de flag
- Kill switch: desligar uma flag instantaneamente em produção, sem deploy
Targeting e segmentação
- Regras de segmentação: por atributo do usuário (país, plano, versão do app, device, etc.)
- Targeting individual: ligar a flag para usuários específicos (por ID, email)
- Segments: listas nomeadas de usuários (ex: beta testers, funcionários internos). Terminologia alinhada ao padrão de mercado (LaunchDarkly usa "segments"; "cohort" é o termo equivalente do lado de ferramentas de analytics como Amplitude, e os dois conceitos costumam ser sincronizados entre as duas categorias de ferramenta)
Rollout progressivo
- Rollout percentual: liberar gradualmente para uma fatia da base (5%, 10%, 50%...)
- Sticky bucketing: garante que o mesmo usuário sempre caia na mesma variante entre sessões
-
Hash consistente: normalmente hash do
user ID + flag keypara decidir o bucket -
Bucketing, definição: mecanismo de dividir a base de usuários em fatias numeradas (0-99) de forma determinística e recalculada a cada requisição — não há cadastro prévio de "usuário está no grupo X" guardado em banco. A mesma entrada (
userId + flagKey) sempre produz a mesma saída, o que gera o efeito "sticky" sem precisar guardar estado por usuário. Incluir aflagKeyno hash (e não só ouserId) evita correlação indesejada entre o bucket de um usuário em flags diferentes
A/B testing / Experimentação
- Múltiplas variantes com pesos configuráveis (ex: A 33%, B 33%, C 34%)
- Integração com analytics: disparar eventos de exposição (quem viu qual variante)
- Significância estatística geralmente delegada a uma ferramenta de análise externa, mas o sistema de flags precisa expor os dados de exposição
SDKs e integração técnica
SDK client-side vs server-side
A diferença não é só "onde roda o código" — é uma questão de segurança e vazamento de informação.
Server-side SDK
- Roda em ambiente confiável (backend)
- Pode ter acesso a todas as flags, incluindo as não lançadas, regras de segmentação completas e segredos usados no targeting
- Comunica-se direto com a API central ou via um Relay Proxy (serviço intermediário que reduz chamadas repetidas)
Client-side SDK (browser, mobile, apps)
- Roda em ambiente não confiável — qualquer um pode inspecionar o payload
-
Nunca deve receber a lista completa de flags. O backend avalia a flag para aquele usuário específico e envia só o payload já resolvido (ex:
flag X = true,flag Y = variante B) - Isso é feito via endpoint do tipo
/sdk/eval?context={user}, retornando um payload minimalista - Regra crítica: nunca usar a SDK key server-side (completa) dentro de um app client — isso vazaria todas as flags e regras internas
Avaliação local vs remota
| Abordagem | Latência | Frescor dos dados |
|---|---|---|
| Remota (thin client) | Alta — chamada de rede a cada checagem | Sempre atualizado |
| Local (thick client, recomendada) | Baixa — avaliação em memória | Depende da estratégia de sync |
Estratégias de atualização do cache local:
- Polling: SDK pergunta "tem mudança?" a cada N segundos
- Streaming: servidor empurra updates em tempo real assim que uma flag muda
Muitos SDKs combinam os dois: streaming como canal principal + polling como fallback caso a conexão caia.
Fallback / default values
- Todo flag check exige um valor default obrigatório na chamada (nunca deixar o SDK "adivinhar")
-
Hierarquia de fallback:
- Valor em cache local (último snapshot conhecido)
- Se nunca conectou → default fornecido no código de chamada
- Falha do serviço de flags nunca deve travar a aplicação — deve ser silenciosa e logada
- Timeout de inicialização: tempo máximo de espera no boot para o primeiro fetch (ex: 5s); depois disso, segue com defaults e atualiza quando possível
- Emitir métrica/log quando o SDK cai em modo fallback, para sinalizar degradação do serviço
Autenticação (SDK Key)
A SDK key funciona como um bearer token:
Authorization: Bearer sdk-a1b2c3d4...
| Elemento | Papel |
|---|---|
| SDK key (bearer token) | Autentica + resolve escopo (projeto/ambiente) — não participa da decisão de segmentação |
| Context (key, plan, country...) | Fornece os dados para a decisão de segmentação e rollout |
Exemplo de request:
POST /evaluate
Headers: { "Authorization": "Bearer sdk-a1b2c3d4..." }
Body: {
"flagKey": "cancelar-nota-fiscal",
"context": {
"key": "user-12345",
"plan": "enterprise",
"country": "BR"
}
}
Por que não expor projectId/environment como parâmetros públicos
Alternativa descartada: passar ?project=app-mobile&env=production abertamente, sem token.
Problemas dessa abordagem:
- Enumeração — projectId previsível permite testar/descobrir outros projetos/ambientes
- Sem autenticação — impossível distinguir chamada legítima de bisbilhotagem
- Vazamento de lógica de negócio — testar contexts publicamente permite reconstruir regras de segmentação por engenharia reversa
- Sem revogação/rotação — sem token não há "chave" para cortar acesso; seria preciso mudar a própria estrutura da API
- Sem rate limiting/billing por cliente — impossível medir uso ou isolar consumidores
- DoS mais fácil — sem identificação, não dá para aplicar throttling seletivo
- Mistura acidental de ambientes — sem credencial vinculante, é mais fácil um client de produção acessar staging por engano
Sincronização em tempo real
Por que webhook não serve como mecanismo de sync do SDK
Webhook exige que o servidor inicie uma conexão de saída até o cliente — o que não funciona para a maioria dos consumidores reais de SDK:
- Frontend/browser: sem endereço público
- App mobile: sem IP público, sem porta aberta
- Backend atrás de NAT/firewall corporativo: porta não exposta
- Serverless/Lambda: função só existe durante a execução
- Múltiplas réplicas atrás de load balancer: ambíguo para qual réplica enviar
Por isso, o padrão adotado (como na maioria dos sistemas de feature flag do mercado) é o inverso: o cliente inicia a conexão de saída (streaming ou polling) e mantém ela aberta.
SSE como alternativa ao WebSocket
WebSocket é bidirecional e mais pesado que o necessário — o caso de uso aqui é apenas server → client.
SSE (Server-Sent Events) cobre esse caso:
- Conexão HTTP simples, unidirecional
- Reconexão automática nativa
- Funciona sobre HTTP/1.1 comum, sem upgrade de protocolo
- Mais simples de implementar mantendo tempo real
Fluxo de autenticação SSE:
SDK Client → API (Auth): POST /auth (org, app, ambiente)
API (Auth) → SDK Client: SDK Key (bearer token)
SDK Client → Sync Service (SSE): GET /stream (Authorization: Bearer ...)
Sync Service → SDK Client: snapshot inicial de flags
[flag alterada no painel]
Sync Service → SDK Client: evento SSE (update de flag)
SDK Client: atualiza cache local
Eventos de conexão
SdkConnected
Dispara quando o SDK abre a conexão de streaming (SSE/WebSocket) e o token é validado com sucesso. Não é só "TCP conectou" — é "conexão autenticada e pronta para receber updates daquele escopo (org/app/ambiente)".
SdkDisconnected
Dispara quando a conexão cai, por um de três motivos (importante diferenciar no payload):
- Fechamento gracioso — shutdown, deploy, scale-down
- Timeout/heartbeat perdido — SDK parou de responder ao keep-alive
- Erro de rede — queda abrupta
Diferenciar o motivo importa para observabilidade: uma queda maciça de conexões pode ser "todo mundo fez deploy ao mesmo tempo" (normal) ou "o serviço de streaming caiu" (crítico).
Pontos de atenção:
- Reconexões automáticas com backoff podem gerar ruído (
SdkDisconnected → SdkConnectedrepetidos) — considerar debounce/agregação nas métricas - Incluir um
connectionIdúnico por sessão de streaming para correlacionar conexão/desconexão e calcular duração
Usos práticos:
- Observabilidade — quantos SDKs ativos por ambiente
- Detecção de degradação — queda anormal indica problema no Sync service
- Billing/capacity planning — se cobrança for por conexões simultâneas
Payload — SdkConnected
{
"eventType": "SdkConnected",
"connectionId": "conn_9f8a3b2c",
"organizationId": "org_123",
"applicationId": "app_456",
"environment": "production",
"sdkKeyId": "sdkkey_789",
"sdkVersion": "js-server@2.4.1",
"transport": "sse",
"connectedAt": "2026-07-16T14:32:10Z"
}
-
connectionId: chave de correlação entre o evento de conexão e o de desconexão — sem ele não dá pra calcular duração nem saber a qual conexão um disconnect se refere -
sdkKeyId: identificador do token, nunca a SDK key crua — logar o token completo num evento de observabilidade seria vazamento de credencial em qualquer pipeline de logs/analytics -
sdkVersion: linguagem + versão do SDK — essencial pra saber quais versões estão em uso ao investigar bugs ou depreciar releases antigas -
transport: canal usado (sse,websocket,polling-fallback) — ajuda a identificar se alguma fatia de clients caiu pra polling por problema de rede
Payload — SdkDisconnected
{
"eventType": "SdkDisconnected",
"connectionId": "conn_9f8a3b2c",
"organizationId": "org_123",
"applicationId": "app_456",
"environment": "production",
"sdkKeyId": "sdkkey_789",
"disconnectedAt": "2026-07-16T15:10:42Z",
"durationSeconds": 2312,
"reason": "heartbeat_timeout",
"reasonDetail": "no keep-alive response in 30s"
}
-
durationSeconds: calculado no momento do evento (disconnectedAt - connectedAt) — poupa quem consome o evento de cruzar os dois eventos manualmente -
reason: enum fechado (graceful,heartbeat_timeout,network_error) — nunca texto livre, pra permitir agregação confiável em dashboards -
reasonDetail: campo livre opcional, só pra debug humano — nunca usado em lógica/agregação
Agregação de reconexões: se um novo SdkConnected chegar com o mesmo sdkKeyId dentro de uma janela curta (ex: 5s) após um SdkDisconnected, o pipeline de métricas pode agregar os dois como "reconexão" em vez de "queda + nova sessão" — evita inflar o contador de desconexões em cenários normais de instabilidade de rede.
Decisão — sem IP nos eventos: os eventos de conexão não carregam IP do client. Segmentação por IP, se necessária no futuro, é responsabilidade do client incluir como atributo no context da chamada de evaluate() — igual qualquer outro atributo (país, plano, device). O sistema de flags não infere nem captura dado de rede por conta própria; só decide com base no que recebe explicitamente.
Webhooks (integração externa)
Webhook não é o mecanismo de sincronização do SDK, mas é útil como feature de integração opcional, quando o receptor é um serviço backend do próprio cliente, controlado por eles, com endpoint público de propósito.
Exemplos de uso:
- Notificar o sistema de CI/CD do cliente quando uma flag mudar em produção
- Sincronizar com um sistema de config interno do cliente
Modelo proposto: URL de webhook registrada no momento da autenticação (junto com organização/aplicação/ambiente), salva em uma tabela de webhooks associada à chave/token.
Escopo do webhook
O webhook usado aqui serve exclusivamente para notificar que uma flag mudou — não carrega o novo valor no payload, apenas o sinal de que algo mudou naquela flag/ambiente. O cliente, ao receber o evento, decide se e quando revalidar o estado real via evaluate()/sdk/eval.
Escopo do registro: Application + Environment (não atrelado a uma SdkKey específica, já que a chave de SDK tem ciclo de vida de rotação/revogação próprio, diferente da vida útil de uma integração de webhook).
Webhook {
id
applicationId
environmentId
url
status // active | disabled
createdAt
}
Payload
{
"eventType": "FLAG_CHANGED",
"applicationId": "app_456",
"environmentId": "env_789",
"flagKey": "cancelar-nota-fiscal",
"changedAt": "2026-07-17T10:15:00Z"
}
Sinal genérico, sem o valor da flag — o cliente sempre revalida o estado real depois de receber o evento; o webhook nunca é a fonte de verdade do novo valor.
Por que não usar assinatura (HMAC/secret)
Como o payload não carrega o valor da flag, forjar essa notificação não tem efeito real além de fazer o cliente revalidar seu estado à toa (uma chamada extra desnecessária, não uma injeção de dado falso). Diferente de um webhook que carregasse o valor diretamente — nesse caso a assinatura seria obrigatória. Aqui, o cliente é livre para descartar/ignorar disparos que considerar excessivos.
Rate limiting por webhook — com colapso de eventos
Rate limit aqui não é sobre validade temporal da requisição — é um teto de frequência de disparo, para impedir que uma rajada de edições na mesma flag vire uma rajada de chamadas HTTP contra a url cadastrada (o que tornaria o próprio serviço de flags um vetor de flood/DoS contra terceiros, caso alguém cadastre a URL de uma vítima).
Estratégia adotada: dentro de uma janela de tempo (ex: 1 evento a cada N segundos por webhook), múltiplas mudanças na mesma flag são colapsadas em um único disparo — já que o payload não informa o valor, não há perda de informação relevante em notificar uma vez só "essa flag mudou" em vez de uma vez por edição.
Validação de URL no cadastro (anti-SSRF)
Ao registrar um webhook, validar que a url não aponta para endereços internos (localhost, ranges de IP privado, endereço de metadata de nuvem como 169.254.169.254) — evita que o cadastro de um webhook seja usado para fazer o próprio serviço de flags acessar rede interna que não deveria alcançar.
Entrega e retry — WebhookDelivery
Chamada HTTP para endpoint de terceiro pode falhar (timeout, 5xx, DNS fora do ar), então cada tentativa de entrega é registrada separadamente da configuração do webhook:
WebhookDelivery {
id
webhookId
payload // snapshot do corpo enviado
attempt // número da tentativa (1, 2, 3...)
httpStatus
success
deliveredAt
}
Composição com Webhook (a entrega nasce vinculada ao webhook que a gerou), mas como registro histórico segue o mesmo critério do ExposureEvent/ConnectionSession: pode ser retido para fins de auditoria mesmo que o webhook seja depois editado ou desativado.
Política de retry (fase inicial): backoff exponencial com limite de tentativas (ex: 3 tentativas — 1min, 5min, 30min), depois marca como failed definitivamente e loga para investigação manual. Sem fila de retry infinita.
Fluxo completo de evaluate()
Pipeline de resolução de token → busca da flag → segmentação → rollout → resultado.
Visão geral do pipeline
Request → Auth (SDK key) → Resolve escopo (org/app/env)
→ Buscar definição da flag (cache local ou remoto)
→ Flag existe e está ativa?
→ Avaliar regras (ordem de precedência)
→ Bucketing (rollout % / variante A-B)
→ Montar resultado + reason
→ Emitir evento de exposição (async)
→ Retornar resposta
1. Autenticação e resolução de escopo
- Validar o SDK key (bearer token) contra o serviço de auth/cache de tokens
- Token inválido/revogado →
401, encerra o pipeline aqui (sem chegar a tocar em dados de flag) - Token válido → resolve
{ organizationId, applicationId, environment }vinculados àquele token - Esse escopo é o que delimita quais flags podem ser buscadas no próximo passo — o context da request nunca escolhe o ambiente, só o token
2. Busca da definição da flag
- Chave de busca:
(organizationId, applicationId, environment, flagKey) - Fonte: cache local em memória (thick client) → se miss, busca remota → se a flag não existir, cai no fallback (ver seção de Fallback / default values)
- Flag encontrada, mas kill switch ligado → resultado imediato = valor "off" configurado,
reason: "OFF". Nenhuma regra é avaliada depois disso — ver Decisões de arquitetura sobre precedência do kill switch
3. Ordem de precedência das regras
| Ordem | Regra | Comportamento |
|---|---|---|
| 1 | Kill switch / flag off | Curto-circuito — retorna valor off, ignora tudo abaixo |
| 2 | Targeting individual | Se context.key (ou email) está na lista de override individual → retorna o valor daquele override |
| 3 | Segments | Se o usuário pertence a um segment com regra própria → aplica o valor definido para aquele segment |
| 4 | Regras de segmentação | Avaliadas em ordem definida na flag (primeira regra que casar com os atributos do context vence) |
| 5 | Rollout percentual (fallthrough) | Se nenhuma regra acima casou, aplica o bucketing por hash consistente |
| 6 | Default da flag | Se nem o rollout resolver, usa o default definido na própria flag — não confundir com o default passado pelo SDK na chamada, que só entra em jogo se a flag inteira não for alcançável |
Cada resultado carrega um campo reason (ex: TARGET_MATCH, RULE_MATCH, FALLTHROUGH, OFF, DEFAULT) — essencial para debugar "por que esse usuário caiu nessa variante".
4. Bucketing (rollout % e variantes A/B)
- Hash consistente:
hash(userId + flagKey) % 100→ determina o bucket 0-99 - Rollout percentual: compara o bucket contra o threshold configurado (ex: 30% → buckets 0-29 entram)
- A/B com múltiplas variantes: o range 0-99 é dividido proporcionalmente aos pesos (A 33% = buckets 0-32, B 33% = 33-65, C 34% = 66-99)
- Usar sempre o mesmo algoritmo de hash em todos os SDKs (client e server) — se um SDK usa hash diferente do outro, o mesmo usuário pode cair em variantes diferentes dependendo de onde a avaliação rodou, quebrando a sticky bucketing
5. Fallback (quando a busca da flag falha)
- Cache local (último snapshot conhecido) — se a flag existe no cache mas o serviço remoto está fora, usa o cache
- Se nunca conectou (sem cache algum) → default fornecido no código de chamada,
reason: "ERROR"ou"CLIENT_NOT_READY" - Logar/emitir métrica sempre que cair aqui, para não mascarar degradação do serviço
6. Montagem do resultado
{
"value": true,
"variant": "control",
"reason": "RULE_MATCH",
"ruleId": "rule_beta_countries_br"
}
-
value: o valor resolvido (bool, string, número ou JSON) -
variant: nome da variante (relevante em multivariável/A-B) -
reason: motivo da decisão (debugging) -
ruleId: qual regra específica decidiu (opcional, mas ajuda muito em suporte/debug)
Exposição desse payload completo é restrita — ver Decisões de arquitetura.
7. Evento de exposição
- Disparado de forma assíncrona (não pode bloquear a resposta do
evaluate()) - Contém:
flagKey,variant,context.key,timestamp,reason - Vai para o pipeline de analytics — é o dado bruto que alimenta significância estatística de A/B test (calculada fora do sistema de flags)
8. Diferença entre evaluate() single e /sdk/eval bulk
-
evaluate()single: avalia uma flag específica para um context — típico de uso server-side, ou de debug/teste manual -
/sdk/eval?context=: avalia todas as flags do ambiente para aquele context de uma vez, retornando o payload minimalista já resolvido — é o que o client-side SDK chama no boot e a cada refresh, evitando N chamadas (uma por flag) - Internamente,
/sdk/evalroda o mesmo pipeline acima em loop para cada flag do ambiente, mas com uma otimização: busca todas as definições de flag de uma vez (evita N round-trips ao cache/DB)
Decisões de arquitetura
1. Exposição de reason / ruleId no payload
Decisão: o payload completo (reason, ruleId) só é exposto em evaluate() server-side / telas de debug autenticadas. O endpoint client-side (/sdk/eval?context=) devolve só value + variant.
Motivo: expor ruleId/reason no client permite que qualquer um inspecionando o response reconstrua a lógica de segmentação interna (ex: descobrir que existe uma regra específica por país) — o mesmo tipo de vazamento de lógica de negócio que já motivou a decisão de não expor project/env como parâmetros públicos.
2. Kill switch — precedência absoluta
Decisão: o kill switch não é uma regra na lista de precedência de targeting/segmentação/rollout. É um curto-circuito que roda antes de qualquer avaliação de regra, e vence sempre, incondicionalmente — inclusive sobre targeting individual (ex: um QA com override pessoal não continua vendo a flag "on" se o kill switch foi acionado).
Motivo: kill switch é a válvula de emergência do sistema. Se pudesse ter exceções, deixaria de cumprir sua função de parar tudo, sem exceção, sem deploy.
Mecânica:
- No painel de controle, apertar o botão é uma escrita simples no mesmo lugar de outras configs da flag (
status: "killed") - A diferença está na prioridade de leitura:
evaluate()checa isso primeiro; sekilled, nem entra no loop de regras - Propagação depende do canal de sync: SDKs com streaming (SSE) ativo recebem o update quase instantaneamente; SDKs em modo polling ou desconectados (fallback em cache local) só verão o kill switch na próxima reconexão/poll — limitação conhecida, não um bug
3. Versionamento de algoritmo de hash — não implementar agora
Decisão: não implementar um registry de múltiplas versões de hash (hashVersion por flag) neste momento. Se o algoritmo de hash for trocado no futuro, os buckets de usuários existentes vão simplesmente mudar — novas decisões serão produzidas a partir da nova conta, sem tentar preservar as antigas.
Motivo: o sistema ainda está em fase de construção, sem testes A/B ativos que dependam de continuidade estatística entre trocas de algoritmo. Implementar registry versionado agora (com todo SDK de toda linguagem precisando reimplementar todas as versões de hash) é custo de engenharia prematuro para um problema que ainda não existe. Trocar hash é evento raro e deliberado — deve ser comunicado como breaking change, não silenciosamente absorvido.
Nota de escopo: essa decisão vale para o estágio atual do projeto. Se no futuro o sistema amadurecer com testes A/B rodando continuamente, essa decisão deve ser revisitada.
4. Verificação client-side nunca é autoritativa
Cenário motivador: uma interface desktop exibe um botão condicionado a uma flag. Se a conexão de streaming cair e o client não for notificado de um kill switch, o botão continua visível com a última informação em cache — até o momento em que o usuário efetivamente submete a ação.
Decisão: toda flag que guarda uma ação com efeito real no backend deve ser re-verificada no servidor no momento da ação, independente do que o client mostrou. A avaliação client-side (cache local) serve só para UX responsiva (mostrar/esconder elementos); nunca é a fonte de verdade.
Fluxo:
Client (cache desatualizado) → mostra botão → usuário clica
↓
API recebe a submissão → roda evaluate() fresco, contra o estado atual
↓
Kill switch ativo agora → API rejeita/ignora a operação
↓
API responde ao client (ex: 403 / "feature indisponível")
↓
Client pode usar essa resposta como sinal indireto para invalidar seu cache local,
sem esperar a próxima reconexão de streaming
Motivo: mesmo princípio de nunca confiar em validação só no front-end — o client pode estar desatualizado, ou ser manipulado. A flag no client é sugestão de experiência; a flag no server é o portão real.
Formato da resposta de rejeição
Código HTTP 403 Forbidden — a requisição está bem formada e autenticada, só não é permitida pelo estado atual da flag (não é 400, não é erro de input; não é 404, o recurso existe).
{
"error": "FEATURE_UNAVAILABLE",
"reason": "RULE_MATCH",
"flagKey": "cancelar-nota-fiscal"
}
-
error: código fixo e genérico (FEATURE_UNAVAILABLE) — permite o client reconhecer programaticamente que a rejeição foi por causa de uma flag, diferenciando de outros motivos de 403 que a API possa ter (ex: permissão de usuário) -
reason: reaproveita o mesmo enum já usado no resultado deevaluate()(OFF,TARGET_MATCH,RULE_MATCH,FALLTHROUGH) — sem vocabulário novo só pra esse caso. Expor a categoria da decisão é seguro e útil:OFFsugere que é sistêmico/temporário (kill switch geral), enquantoRULE_MATCH/FALLTHROUGHsugere que o usuário não é elegível agora — o client pode adaptar a UX de acordo (tentar de novo mais tarde vs não insistir) -
flagKey: qual flag causou a rejeição — permite o client invalidar exatamente essa flag no cache local, sem esperar a próxima reconexão de streaming
O que fica de fora, deliberadamente: ruleId, attribute, operator, value — o client sabe que categoria de decisão aconteceu (ex: foi uma regra de segmentação), mas não sabe qual atributo ou valor causou isso (ex: não sabe que foi porque country != BR). Isso preserva a distinção entre categoria da decisão (seguro expor) e conteúdo da regra de negócio (não deve vazar), mantendo a mesma postura da Decisão 1.
5. ruleId sem versionamento
Decisão: ruleId não é versionado. É uma referência estável à regra, cujo conteúdo pode ser editado ao longo do tempo sem gerar um novo identificador. ExposureEvent e o resultado de evaluate() guardam apenas o ruleId, não um snapshot da condição avaliada naquele momento.
Limitação conhecida e aceita: se uma regra for editada depois de gerar exposições, o histórico de auditoria não reflete com precisão o que a regra dizia no momento da avaliação — o ruleId de um evento antigo passa a apontar para o conteúdo atual da regra, não para o que ela era quando o evento aconteceu.
Motivo: mesma lógica já aplicada à decisão de não versionar o algoritmo de hash — construir uma trilha de auditoria histórica precisa (versionamento de regra estilo git, ou snapshot embutido no evento) é engenharia adicional para um problema que, no estágio atual do projeto, ainda não se manifestou como necessidade real. Aceitar a limitação agora é mais barato do que resolver um requisito que ainda não apareceu.
Resumo das decisões
| Item | Decisão |
|---|---|
reason/ruleId
|
Só em evaluate() server-side/debug; nunca em /sdk/eval client |
| Kill switch | Curto-circuito com precedência absoluta, fora da lista de regras |
| Hash versionado | Não implementar agora; troca de hash = breaking change documentado |
| Verificação client vs server | Toda ação com efeito real precisa de evaluate() fresco no servidor no momento da ação |
Versionamento de ruleId
|
Não implementado; histórico de auditoria pode ficar impreciso após edição de regras — limitação aceita |
Modelo de dados
Entidades do domínio e suas relações, em terminologia UML (associação, composição, classe de associação).
Organization
Tenant raiz — representa o cliente/empresa que usa a plataforma.
-
Composição com
Application(1 organização possui N aplicações; aplicação não existe fora de uma organização)
Application
Um produto/app específico do cliente (ex: "app mobile", "backend web").
-
Composição com
Environment(ambiente não existe fora de uma aplicação) -
Composição com
Segment(segments são definidos no escopo da aplicação) -
Composição com
FlagDefinition(a flag, como conceito, pertence à aplicação)
Environment
Instância de ambiente (dev/staging/produção) dentro de uma aplicação.
-
Composição com
SdkKey(chave é emitida para um ambiente específico e não existe fora dele) - Participa como um dos lados da classe de associação
FlagEnvironmentConfig
SdkKey
Token de autenticação (bearer) que resolve o escopo org/app/ambiente.
-
Composição com
Webhook(webhook é registrado atrelado a uma chave; não existe solto) -
Associação com
ConnectionSession(a sessão referencia a chave, mas é um registro de log — pode ser retida mesmo após a chave ser revogada)
Webhook
URL de integração externa registrada no momento da autenticação.
- Existe apenas como parte de uma
SdkKey(composição, conforme acima)
FlagDefinition
A flag em si: chave, tipo (boolean/multivariável), descrição. Conceito estável entre ambientes.
-
Composição com
Application - Participa como um dos lados da classe de associação
FlagEnvironmentConfig
FlagEnvironmentConfig
Classe de associação entre FlagDefinition e Environment. Representa "esta flag, neste ambiente", carregando os atributos que variam por ambiente (status/kill switch, defaultValue, hashVersion). É o motivo de uma flag poder estar "on" em staging e "off" em produção sem duplicar a definição da flag.
-
Composição com
Variant,TargetingRule,IndividualOverride(só existem no contexto de uma config específica) -
Associação com
SegmentTargetRuleeExposureEvent
Variant
Uma variante de valor/peso dentro de uma config de flag (ex: "A", 33%).
-
Composição com
FlagEnvironmentConfig
TargetingRule
Regra de segmentação ordenada, avaliada dentro de uma config.
-
Composição com
FlagEnvironmentConfig -
Composição com
RuleCondition(uma regra é feita de condições; sem elas não tem sentido)
RuleCondition
Condição atômica de uma regra (atributo, operador, valor — ex: country equals "BR").
-
Composição com
TargetingRule
Operadores suportados (fase inicial — conjunto fechado, sem comparações como maior/menor):
| Operator | Formato esperado (máscara) | Exemplo de value
|
|---|---|---|
equals |
escalar único (string, número ou bool) | "BR" |
in |
array de escalares do mesmo tipo | ["BR", "PT", "AO"] |
operatorSchema = {
equals: { shape: "scalar" },
in: { shape: "array" }
}
- O
operatorfunciona como chave que resolve a máscara de validação (shape) esperada paravalue— o motor de avaliação consulta essa máscara antes de parsear a condição, em vez de umif/elsefixo por operador - Validação na criação da regra: o backend rejeita uma condição malformada (ex:
equalscom um array) já no momento do cadastro, sem depender do motor de avaliação pra detectar o erro depois - Extensível sem quebra: operadores futuros (
greater_than,contains, etc.) só precisam declarar sua própria entrada emoperatorSchema— a estrutura de validação não muda, só cresce o dicionário
RuleCondition {
id
attribute // ex: "country", "plan"
operator // "equals" | "in" — chave que resolve a máscara de shape
value // shape validado contra operatorSchema[operator].shape
}
IndividualOverride
Override de valor para um usuário específico (por ID/email).
-
Composição com
FlagEnvironmentConfig
Segment
Lista/grupo nomeado de usuários (ex: beta testers, funcionários internos). Terminologia alinhada ao padrão de mercado (LaunchDarkly).
-
Composição com
Application -
Composição com
SegmentMember(a lista de membros só existe dentro do segment) - Participa como um dos lados da classe de associação
SegmentTargetRule
SegmentMember
Um usuário (userKey) pertencente a um segment.
-
Composição com
Segment
SegmentTargetRule
Classe de associação entre Segment e FlagEnvironmentConfig: representa "este segment direciona para este valor, nesta flag/ambiente". Necessário porque o mesmo segment pode ser usado em várias flags, e uma flag pode usar vários segments — é N:N com atributo próprio (resultVariantId).
ExposureEvent
Evento de telemetria: qual usuário viu qual variante, quando, por qual motivo.
-
Associação (não composição) com
FlagEnvironmentConfig— registro de log que referencia a config no momento da avaliação, mas com retenção/ciclo de vida independente (a exposição histórica pode ser mantida mesmo que a flag seja depois removida)
ConnectionSession
Registro de uma sessão de streaming (SdkConnected → SdkDisconnected), com connectionId, timestamps e motivo de desconexão.
-
Associação com
SdkKey(mesmo raciocínio doExposureEvent: é log, não parte estrutural)
Critério geral de composição vs associação
- Composição: quando o filho não tem razão de existir sem o pai e seu ciclo de vida está atrelado (regra sem flag, variante sem config, membro sem segment)
-
Associação: para registros de telemetria/log (
ExposureEvent,ConnectionSession) — devem sobreviver independentemente do objeto que os originou, pois são dado histórico, não estrutura viva -
Classe de associação: quando a relação em si carrega atributos próprios e é N:N entre duas entidades que existem de forma independente (
FlagEnvironmentConfig,SegmentTargetRule)
Diagrama de classes
Em aberto
Sem itens pendentes no momento.

Top comments (0)