ADR (Architectural Decision Record) costuma fechar com uma linha que soa definitiva, e é aí que nasce a dívida de reversibilidade:
O risco é baixo e a decisão é reversível. Se o fornecedor não entregar, trocamos por outro em poucos dias.
Minha leitura: essa frase costuma envelhecer como leite. Ela cria uma dívida de reversibilidade silenciosa, porque vale na data em que foi escrita e ninguém revisa mais.
Onde a dívida cresce
Suponha um time que adota um sistema de feature flags. No código é uma função que devolve boolean, trivial de trocar.
Depois de alguns meses, a app usa flags para controlar planos e dados, e as usa em testes A/B. O script de release lê a API do fornecedor. Um dashboard consulta diretamente o estado das flags.
Trocar no código continua trivial. Trocar no sistema como um todo, não.
O código nunca foi o problema. A dívida de reversibilidade cresceu fora dele.
Reversibilidade é uma opção, e opção tem preço
Uma decisão reversível é uma opção que você tem o direito de trocar depois, quando souber mais. O erro é achar que esse direito se mantém pra sempre.
A analogia financeira só vai até certo ponto, e vale dizer onde ela para. Uso três elementos da opção como aproximação, prêmio, custo de exercício e expiração, embora na arquitetura eles não sejam contratuais nem fixos.
| Parte da opção | Em finanças | Na decisão de arquitetura |
|---|---|---|
| Prêmio | pago uma vez, na compra | pago continuamente: wrapper, disciplina, revisão, drill |
| Strike | fixo no contrato | move nas duas direções conforme o sistema muda |
| Vencimento | data definida em contrato | não existe data; a opção some quando o prêmio deixa de ser pago |
O prêmio contínuo é a lacuna da análise: reversibilidade não se compra, paga-se. Parou de pagar, perdeu. Chamo de dívida de reversibilidade o prêmio que ninguém pagou.
Sobre o strike, minha tese é mais profunda que “só sobe”. Cada consumidor novo pendurado no fornecedor eleva o custo de sair. Cada engenheiro que aprende a query language do fornecedor eleva o custo. Cada relatório montado em cima da API eleva o custo.
Mas o custo cai em pelo menos três casos, e ignorar isso enfraquece o argumento:
- Padrão aberto adotado depois da decisão. OpenFeature se apresenta como uma especificação aberta de feature flags, com SDKs e providers para backends diferentes.
- Commoditização da API. Quando fornecedores convergem para a mesma superfície, a tradução de um para outro fica menor.
- Queda de uso. Se o time para de usar metade dos recursos, a superfície a portar encolhe sem ou com pouco refactor.
Então a tese que defendo não é “o strike só sobe”. É, sem prêmio pago de propósito, o strike sobe por padrão, e as quedas dependem de eventos que não estão sob seu controle. Você não planeja um padrão aberto aparecer.
A pergunta que interessa não é “essa decisão é reversível?“. É “quanto custa reverter hoje, e quanto custava seis meses atrás?”. A distância entre as duas respostas é a dívida de reversibilidade.
Duas coisas diferentes: medida e inventário
A lista abaixo mede dívida de reversibilidade de seis jeitos diferentes. Dois itens são medida automática. Um item é experimento. Os três restantes são inventário manual e estimativa, que é uma coisa útil e diferente.
| Item | Tipo | Quem produz |
|---|---|---|
| Acoplamento no código | medida | comando no CI |
| Superfície de SDK em uso | medida | comando no CI |
| Consumidores fora do código | inventário | pessoas, na revisão do ADR |
| Portabilidade dos dados | inventário | resposta a três perguntas |
| Custo de saída em dias | estimativa | a mesma pessoa, do mesmo jeito |
| Drill de saída | experimento | uma execução real, por ano |
Chamar tudo de métrica seria vender inventário como instrumento. Só as duas primeiras linhas rodam sozinhas.
1. Acoplamento no código, com as devidas ressalvas
A questão central é identificar quantos arquivos estão associados a cada fornecedor. A abordagem ingênua falha em múltiplos aspectos:
rg -l 'launchdarkly' --glob '!node_modules' | wc -l
Problemas concretos com essa linha:
- É case-sensitive. Perde
LaunchDarklyeLAUNCHDARKLY. - Conta comentário, teste e mock junto com código de produção.
- Perde alias local e caminho de import que não repete o nome do fornecedor.
Versão que corrige o que dá para corrigir:
rg -l -i 'launchdarkly|ld-node-server-sdk' \
--glob '!node_modules' \
--glob '!**/*.test.*' \
--glob '!**/*.spec.*' \
--glob '!**/__mocks__/**' \
-t js -t ts \
| wc -l
O que sobra de erro, e não some com flag: alias local (import ld from './flags') e acesso dinâmico continuam invisíveis. Quem quer contagem confiável precisa de análise de imports no AST, não de regex. Trate o número como piso, não como total.
Um número sozinho não diz nada. O que diz é a combinação: a mesma contagem, no mesmo repo, medida trimestre a trimestre. Se ela sobe, há indício de aumento do acoplamento direto; o impacto no custo de saída precisa ser confirmado pelo inventário e pelo drill.
Um aviso importante sobre a curva: aqui o conselho e a medida brigam. Se o time seguir a regra “tudo passa pelo wrapper“, a contagem trava num valor baixo e nunca mais se move. Nesse caso, a curva plana é o resultado esperado do time acertando, e não um sinal de saúde contínua. Quando ela sobe, é isso que indica que o wrapper vazou.
Por isso a contagem de arquivos serve como detector de vazamento do wrapper, e não como termômetro de acoplamento. Quem adota wrapper deve medir o tamanho da interface do wrapper, que é o que realmente precisa ser reimplementado numa troca.
Gate de CI, com a mesma ressalva:
#!/usr/bin/env bash
# Fails the build when files outside the wrapper touch the vendor SDK.
set -uo pipefail
VENDOR="${1:?uso: exit-cost.sh {padrao-rg} {limite} [prefixo-do-wrapper]}"
LIMIT="${2:?informe o limite}"
WRAPPER="${3:-src/flags/}"
REPO_ROOT=$(git rev-parse --show-toplevel)
cd "$REPO_ROOT"
# rg exits 1 on no match; a repo with zero vendor files must pass, not fail.
FILES=$(rg -l -i "$VENDOR" \
--glob '!node_modules' \
--glob '!**/*.test.*' \
--glob '!**/*.spec.*' \
--glob '!**/__mocks__/**' \
-t js -t ts || true)
if [ -z "$FILES" ]; then
COUNT=0
else
COUNT=$(printf '%s\n' "$FILES" | grep -cv "^$WRAPPER" || true)
fi
echo "arquivos fora do wrapper associados ao fornecedor: $COUNT (limite: $LIMIT)"
[ "$COUNT" -le "$LIMIT" ] || {
echo "ERRO: orçamento de reversibilidade estourado. Revise o ADR."
exit 1
}
Três detalhes que o script precisa acertar, e que a versão ingênua erra:
rgsai com código 1 quando não acha nada. O|| truenormaliza o status 1 dorgquando não há resultados, e o teste de string vazia zera a contagem.- O
grepde exclusão do wrapper só funciona com caminho relativo à raiz. Ocdnogit rev-parse --show-toplevelgarante isso onde quer que o CI chame o script. - O padrão do fornecedor entra por argumento e precisa incluir o alias de pacote. Para o exemplo deste artigo, a chamada é
exit-cost.sh 'launchdarkly|ld-node-server-sdk' 2.
Limite conhecido: ele conta arquivos. Cinquenta chamadas novas dentro de um arquivo que já estava na lista passam verdes. Para isso serve a próxima medida.
2. Superfície de SDK em uso
Quantos métodos distintos do SDK o repo chama. Poucos métodos indicam superfície pequena a reimplementar numa troca. Muitos indicam o contrário. Não tenho número que separe “pouco” de “muito”, e qualquer corte que eu chutasse aqui seria invenção. O limiar útil é o que o time escreve no próprio ADR e depois defende na revisão.
rg -o 'ldClient\.\w+' | sort -u
Saída de exemplo:
ldClient.allFlagsState
ldClient.close
ldClient.identify
ldClient.track
ldClient.variation
ldClient.waitForInitialization
O que esse comando não pega, e você precisa saber antes de confiar nele:
- Destructuring:
const { variation } = ldClientsome da contagem. - Acesso dinâmico:
ldClient[methodName]some. - Outra instância:
client.variationouflags.variationnão casa com o padrãoldClient. - Comentário e teste entram na contagem igual.
O número vira um piso, de novo. Contagem exata exige AST.
E aqui está o risco de virar teto no CI. Um time pode baixar a contagem trocando ldClient.variation por const { variation } = ldClient, sem tirar nenhuma dependência do fornecedor. O número melhora e o acoplamento fica igual. Regex mede texto, não dependência.
Minha posição: essa contagem serve para conversa na revisão do ADR, e não para reprovar build. Para evitar manipulação de variáveis, é preciso medir a interface do wrapper, código próprio incompatível com destructuring, ou recorrer ao AST.
Se você quer um número que também responde ao caso “50 chamadas novas num arquivo existente”, conte ocorrências em vez de métodos distintos:
rg -o -i 'ldClient\.\w+' \
--glob '!node_modules' \
--glob '!**/*.test.*' \
-t js -t ts \
| wc -l
Esse é o número que o gate de arquivos não vê. Ele tem o mesmo problema de jogos, com a mesma resposta: acompanhe, não reprove.
3. Consumidores que não aparecem no grep
Isto é inventário, não medida. Nenhuma ferramenta gera essa lista, pois os consumidores estão fora do repositório: dashboard, planilha, webhook, job e automação.
Mantenha a lista no próprio ADR e revise junto com o resto. O valor está no exercício de completar a lista, não no tamanho dela. Consumidor invisível é dívida de reversibilidade que ninguém contabiliza. Se ninguém consegue listar de cabeça quem consome aquilo, a estimativa de saída no ADR está desatualizada por construção.
4. Dados presos lá dentro
Também inventário. Três perguntas objetivas:
- Existe export completo, ou só da configuração atual?
- O histórico vem junto?
- O formato é importável em outro lugar sem um script de tradução?
Três “não” são forte indício de que a saída exigirá trabalho de migração ou perda de dados, não apenas a troca do fornecedor.
5. Custo de saída em dias, revisado
Estimativa. O número que estava no ADR original contra o número de hoje. O crescimento entre as duas datas é dívida de reversibilidade pura. Estimativa grosseira serve, contanto que a mesma pessoa estime do mesmo jeito.
O rodapé do ADR usa uma linha por trimestre com data, custo em dias e o que mudou; a terceira coluna justifica o salto, sem a qual a série vira ruído. Não uso exemplos fictícios, pois isso passaria a impressão de referência.
6. O teste de saída
Nada disso substitui tentar. Uma vez por ano, escolha o fornecedor mais crítico e faça o drill de verdade.
O drill só produz fato sobre o que ele exercita. Um drill que exporta a configuração e reimporta num ambiente paralelo mede o item 4, portabilidade dos dados, e mais nada. Dashboard, script de release e gate de plano continuam sem teste, e são exatamente os consumidores do item 3.
Então o escopo do drill precisa incluir cada consumidor da lista do item 3, um por vez:
para cada consumidor no inventario do item 3:
aponte esse consumidor para o ambiente paralelo
rode o caminho real dele (query, job, deploy, checagem de plano)
registre: funcionou sem mudanca / precisou de tradução / não há caminho
registre as horas gastas
O que cada parte do drill corrige:
| Parte do drill | Corrige o item | Fato que produz |
|---|---|---|
| export e import num ambiente paralelo | 4, portabilidade | o export cobre o que o ADR supunha, ou não |
| apontar cada consumidor da lista para o ambiente paralelo | 3, inventário | consumidor que ninguém tinha listado aparece aqui |
| somar as horas gastas em cada etapa | 5, custo em dias | número medido substitui o estimado |
Sem a segunda linha da tabela, o drill não toca o item 3, e a estimativa do item 5 continua chute. Um drill parcial vale mais que nenhum, e vale menos do que o texto anterior deste artigo dava a entender.
Este é o único item que mede diretamente uma tentativa de saída; os dois primeiros produzem fatos sobre o código, não sobre a migração completa. Por isso ele fica por último e vale mais que os cinco anteriores juntos.
A ficha que vai no ADR
A ficha transforma a dívida de reversibilidade em tetos escritos e revisáveis. Cole isso no template e o resto vira rotina:
## Reversibilidade
Custo de saída estimado: __ dias
Medido em: AAAA-MM-DD
Revisar em: AAAA-MM-DD (um trimestre depois)
O que mantem essa porta aberta:
- todo acesso ao SDK passa pelo wrapper em src/flags/
- teto de arquivos fora do wrapper que tocam o SDK, no CI: __
(comece pelo número de hoje; o teto existe para travar o crescimento,
não para representar um valor saudável que alguem mediu)
- métodos do SDK expostos pela interface do wrapper: acompanhar, sem teto
- chamadas ao SDK no repo inteiro: acompanhar, sem teto
Como leio os números:
- arquivos fora do wrapper acima do teto = wrapper vazou, e o alarme é esse
- interface do wrapper crescendo = superfície a reimplementar cresceu
- contagem de chamadas cresce sem novos arquivos = concentração, não vazamento
- contagem de métodos caindo sem remoção de dependência = destructuring,
não melhora
Consumidores fora do código (inventário manual, revisar junto com o ADR):
- dashboard de analytics (time de dados)
- script de release
Último drill de saída: nunca
Próximo drill: AAAA-MM-DD
Escopo do próximo drill: export e import, mais cada consumidor da lista acima
Quando aceitamos fechar a porta:
- se o custo passar do limite que combinamos sem alguém contestar, viramos
decisão de mão única e assumimos isso por escrito
Os tetos ficam em branco de propósito. Qualquer número que eu colocasse aqui viraria referência sem nada por trás. O teto útil é o valor medido no primeiro dia, com a regra de que subir exige alguém defendendo a subida na revisão.
A parte mais útil é “como leio os números”. Sem ela, o time olha uma curva plana e comemora, quando a curva plana pode significar duas coisas opostas: o wrapper está segurando, ou ninguém mexeu no projeto.
Nem toda porta merece ficar aberta
Manter opcionalidade custa, e agora dá pra nomear o custo: é o prêmio da opção, pago todo mês. Wrapper interno, disciplina pra não usar metade da plataforma, revisão trimestral, drill anual. Trabalho real que não entrega feature nenhuma.
Às vezes o certo é deixar a opção expirar, e dizer isso em voz alta:
Vamos all-in nesse fornecedor. Ele vai virar parte da plataforma. Sair vai ser caro e a gente aceita o risco em troca de velocidade.
Escrever isso no ADR não reduz o custo de sair em um dia sequer. O custo continua real e continua crescendo. O que muda é quem se surpreende. A decisão foi tomada com o preço na mesa, em vez de descoberta na hora em que alguém tenta a porta.
Por isso não digo que a dívida assumida deixa de ser dívida. Ela deixa de ser dívida escondida, e essa é uma diferença menor. O problema nunca foi fechar a porta. O problema é achar que ela continua aberta enquanto ela range (igual filme de terror) fechando.
O que fica
“Depois a gente migra” não é propriedade do sistema. É prêmio de opção que alguém precisa pagar, e que aparece na conta quando não é pago. Chamo essa conta de dívida de reversibilidade.
Porta de mão dupla não fica aberta sozinha. Ou você segura, ou ela fecha nas suas costas.





Deixe um comentário