Configuration

Escopo: transversal. Aplica-se a qualquer linguagem ou stack do projeto.

Configuração é tudo que muda de um ambiente para o outro sem que o código mude: a URL do banco, o tamanho do pool de conexões, o nível de log, a chave da integração. Quando ela está bem organizada, o mesmo artefato compilado sobe em desenvolvimento, em homologação e em produção, e só os valores injetados mudam.

Quando está mal organizada, aparecem linhas de if (env === "prod") espalhadas pelo código. O comportamento em produção passa a ser diferente do comportamento que foi testado, e ninguém consegue prever o resultado do deploy lendo o código.

Esta página cobre estrutura e precedência. Os secrets (rotação, armazenamento, escopo) estão em security.md. As feature flags como mecanismo de release gradual estão em ci-cd.md.

Conceitos fundamentais

ConceitoO que é
Config (configuração)Valor que varia entre ambientes sem mudar o comportamento do código
Secret (segredo)Valor sensível que expõe credencial ou acesso privilegiado; nunca vai para o repositório
Layering (camadas de configuração)Estratégia de resolver configuração por níveis de especificidade, onde cada camada sobrescreve a anterior
Fail-fast (falhar rápido)Validar toda a configuração no startup e encerrar imediatamente se algo estiver ausente ou inválido
Runtime (tempo de execução)Período em que o processo está rodando, após o startup; configurações dinâmicas são avaliadas aqui

O que separa uma config de um secret

Do ponto de vista do código, os dois chegam iguais: uma string injetada no startup. O que muda é o estrago que cada um causa ao vazar, e a frequência com que precisam ser trocados.

EixoConfigSecret
SensibilidadePúblico ou interno não críticoExpõe credencial ou acesso privilegiado
VersionamentoPode ir no repositórioNunca no repositório, inclusive histórico
RotaçãoRaro, acompanha releasePeriódica, independente de release
ExemplosAPI_BASE_URL, PAGE_SIZE, LOG_LEVELDATABASE_PASSWORD, JWT_SECRET, STRIPE_API_KEY

A pergunta que decide: esse valor dá a alguém de fora algum acesso que ele não deveria ter? Se dá, é secret. O PAGE_SIZE pode ser lido por qualquer pessoa sem consequência alguma, e por isso é config.

Guardar os dois no mesmo arquivo obriga o arquivo inteiro a viver sob o regime mais restrito. Mudar o PAGE_SIZE passa a exigir acesso ao cofre de credenciais, e a senha do banco passa a circular junto com um valor que todo mundo precisa editar. Separar as origens deixa cada um no regime que lhe cabe.


Precedência em camadas

A configuração se resolve por camadas, da menos específica para a mais específica. Cada camada sobrescreve a anterior:

1. defaults no código        → última linha de defesa
2. arquivo de config         → valores padrão por ambiente (commitado)
3. variáveis de ambiente     → overrides por host (injetadas no deploy)
4. argumentos de linha de comando / runtime → override pontual
5. secrets manager           → resolvido no startup para chaves sensíveis

A regra que sustenta o modelo: quanto mais perto do host, mais forte o valor. O que está commitado no repositório sempre perde para o que o host injetou no deploy. É essa ordem que permite promover o mesmo artefato entre ambientes.

O default escrito no código serve para o caso em que nenhuma outra camada respondeu. Um servidor que sobe com PORT=8080 fixo no código e ignora o PORT=80 que o host mandou tem a precedência de cabeça para baixo, e nenhum deploy consegue corrigir isso sem alterar o código.


Um arquivo base e um override por ambiente

O arquivo único com if (env === "prod") por dentro cresce sem ordem. Três arquivos completos (config.dev.json, config.staging.json, config.prod.json) repetem os valores que são iguais nos três, e alterar um default comum vira uma edição em triplicata. O formato que se sustenta é base mais override:

config/
  base.json         <- valores válidos em todo ambiente
  dev.json          <- apenas o que muda em dev
  staging.json      <- apenas o que muda em staging
  prod.json         <- apenas o que muda em produção

No startup, o sistema lê base.json, aplica por cima o arquivo do ambiente atual e depois as variáveis de ambiente. O valor que não varia mora em um lugar só.

O sinal de que a divisão está errada é o arquivo de ambiente ter quase o mesmo tamanho do base. Isso indica que valores comuns aos três ambientes foram parar no arquivo específico, e o lugar deles é no base.


Um objeto tipado no lugar da leitura solta

A configuração chega ao código como texto. Ler esse texto direto onde ele é usado espalha conversões (parseInt(PORT), value === "true") por toda a aplicação, e cada ponto de leitura pode converter de um jeito diferente. Renomear uma chave nesse cenário não quebra a compilação, e o defeito só aparece em produção.

Construa um objeto tipado uma vez, no startup:

AppConfig {
  port: number
  database: {
    host: string
    maxPoolSize: number
  }
  features: {
    allowsMultiTenant: boolean
  }
}

O resto do código recebe esse AppConfig como dependência, e o acesso a variável de ambiente fica confinado ao ponto que monta o objeto. A partir daí, renomear uma chave vira erro de compilação, que aparece na build. Testar um cenário com configuração diferente vira construir um AppConfig de teste, sem precisar alterar process.env por baixo dos panos.

Em linguagens com tipagem forte (TypeScript, C#, VB.NET), o objeto de config é uma classe ou um record com propriedades não-nulas. Em linguagens dinâmicas, validar o formato na carga cumpre o mesmo papel: um schema de JSON (JavaScript Object Notation · Notação de Objetos JavaScript), ou uma dataclass com validação.


Validar tudo no startup, antes de aceitar tráfego

O serviço que sobe sem DATABASE_URL e só descobre isso no primeiro request entrega o erro para um usuário real. O que sobe com MAX_POOL_SIZE="abc" quebra minutos depois, quando alguma linha tenta converter o valor, longe da causa.

O AppConfig é montado e validado antes de o servidor abrir a porta:

startup → loadConfig() → validar → [válido] server.start() → aceita tráfego
[inválido] log fatal + exit non-zero
on startup:
  config = loadConfig()
  if not isValid(config):
    log fatal + exit non-zero
  server.start(config)

Três verificações cobrem a maioria dos casos:

VerificaçãoExemploAção na falha
PresençaDATABASE_URL definidoExit imediato, log da chave faltante
FormatoPORT é número, FEATURE_X é booleanoExit imediato, log do valor recebido
ConsistênciaMAX > MIN, URL responde a pingExit imediato, log da regra violada

Falhar no startup sai barato porque o orquestrador (Kubernetes, systemd, PM2) enxerga o processo morrer, marca a instância como não-saudável e continua mandando tráfego para a versão anterior. Falhar no centésimo request sai caro: nesse ponto o serviço já respondeu a noventa e nove, e alguns deles podem ter gravado dados incompletos.


Receber a config como dependência

A configuração é uma dependência do módulo, e a forma de acessá-la define o quanto o módulo fica preso ao ambiente.

FormaAcoplamentoTestabilidade
process.env.X direto no meio da lógicaAlto, globalBaixa, exige monkey-patch
Objeto de config global importadoMédioMédia, requer reset entre testes
Objeto de config injetado via parâmetroBaixoAlta, passa fixture pronta

A injeção explícita é a forma preferida. Quem chama o módulo decide qual config passar. No teste, cada cenário recebe a sua. Em produção, o container de injeção resolve o valor uma vez no startup e distribui para quem precisa.

Ver também a seção Dependências explícitas em principles.md; database.md aplica a mesma regra a conexões.


Mudanças em runtime

Quase toda configuração é estática: o processo lê no startup e o valor fica igual até o próximo deploy. Alguns valores precisam mudar com o processo no ar:

  • Feature flags (granularidade: mudar por usuário, percentual ou geografia, ver ci-cd.md)
  • Limites operacionais (rate limit, tamanho de batch) ajustados durante incidente
  • Níveis de log temporariamente elevados para debugging em produção

Esses valores exigem um mecanismo próprio (serviço de configuração dinâmica, feature flag service, endpoint administrativo autenticado) com três garantias:

  • Auditoria: quem mudou, quando, de que valor para qual.
  • Reversão rápida: voltar ao estado anterior em um clique.
  • Escopo restrito: apenas valores marcados como dinâmicos, nunca config estrutural (connection strings, chaves de criptografia).

Mantenha essa lista curta. Cada valor que pode mudar com o sistema no ar é um valor que ninguém consegue reproduzir a partir do repositório, e um incidente causado por ele fica difícil de investigar depois.


Referência rápida

DecisãoRegra
Valor sensível que vaza e vira notíciaSecret, ver security.md
Valor que varia entre ambientesConfig em camadas, base + override por ambiente
Ordem de precedênciaCódigo < arquivo < ambiente < CLI / runtime < secrets manager
Forma de consumir no códigoObjeto tipado injetado, não leitura direta de env
ValidaçãoFail-fast no startup, antes de aceitar tráfego
Mudança sem restartApenas para flags e limites operacionais, com auditoria
Feature flags como release gradualVer ci-cd.md, seção Feature Flags

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.