Viper — Architecture#
Architectural style#
Layered Registry Library with Strategy Pattern for Source Backends
Viper is a pure library (no binaries, no HTTP server) whose architecture centres on a single composable struct (Viper) that acts as a prioritised configuration registry. Its core structural pattern is a layered waterfall lookup: when a key is requested, the registry walks a fixed six-level precedence stack and returns the first match. Each layer is implemented as a plain map[string]any field on the struct, populated independently by separate subsystems (file, env, flags, k/v store, etc.). Codec support and remote providers are plugged in via interface registries — classic strategy pattern — allowing the core to stay dependency-light while supporting arbitrary formats and backends.
Component diagram (textual)#
┌────────────────────────────────────────────────────────────────────┐
│ Public API (package viper) │
│ Global functions: Get / Set / ReadInConfig / BindEnv / … │
│ Instance-based: v := viper.New() / viper.NewWithOptions(…) │
└───────────────────────────────┬────────────────────────────────────┘
│ delegates to
┌──────▼──────┐
│ Viper{} │ core struct (viper.go)
│ (registry) │
└──┬──┬──┬───┘
┌─────────────┘ │ └──────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Source maps │ │ Codec │ │ Remote Config │
│ ─────────────── │ │ Registry │ │ (optional) │
│ override │ │ ────────── │ │ ─────────────── │
│ pflags │ │ json/yaml/ │ │ remoteConfigFac │
│ env │ │ toml/dotenv │ │ tory interface │
│ config │ │ (internal/) │ │ + remote/ module│
│ kvstore │ └──────────────┘ └──────────────────┘
│ defaults │
└──────────────────┘
│
▼
┌──────────────────────────┐
│ find(key) — waterfall │
│ lookup (viper.go:1194) │
│ override → flag → env │
│ → config → kvstore │
│ → default │
└──────────────────────────┘Core components#
Viper struct — the registry#
- Package:
github.com/spf13/viper(viper.go) - Responsibility: Central state holder. Owns all six source maps, configuration paths, codec registries, remote provider list, fsnotify watcher callback, and all behavioural flags (key delimiter, env prefix, etc.).
- Key types:
Viperstruct,Optioninterface,optionFunc - Dependencies:
afero.Fs(filesystem abstraction),slog.Logger,EncoderRegistry,DecoderRegistry,internal/features
Precedence Engine — find()#
- Package:
github.com/spf13/viper(viper.go:1194) - Responsibility: Implements the core value lookup. Walks the six source maps in strict priority order:
override→ changed pflags → env →config→kvstore→defaults→ pflag defaults. Also handles alias resolution and path-shadowing (a parent key present in a map blocks child lookups). - Key types: Internal method only; no public type
- Dependencies:
v.override,v.pflags,v.env,v.config,v.kvstore,v.defaults,cast(type coercion)
Codec Registry#
- Package:
github.com/spf13/viper(encoding.go) - Responsibility: Provides
EncoderandDecoderlookups by format string. Default codecs (yaml/json/toml/dotenv) are built into thecodec()switch; custom codecs can be registered at runtime viaRegisterCodec. TheDefaultCodecRegistryis initialised once perViperinstance inNew(). - Key types:
Encoder,Decoder,Codec,EncoderRegistry,DecoderRegistry,CodecRegistry(interfaces);DefaultCodecRegistry(implementation) - Dependencies:
internal/encoding/{yaml,json,toml,dotenv}
Config File Loader#
- Package:
github.com/spf13/viper(file.go,finder.go) - Responsibility: Discovers and reads the config file from the filesystem.
file.goimplements the legacy path search usingaferoandSupportedExts.finder.goprovides the experimental XDG-compliant alternative vialocafero, gated byinternal/features.Finder. - Key types:
Finderinterface (implemented bylocafero-based finder),defaultFinder(internal, legacy) - Dependencies:
afero.Fs,locafero(experimental path),internal/features
Live Reload (WatchConfig)#
- Package:
github.com/spf13/viper(viper.go:282) - Responsibility: Starts a background goroutine watching the config file directory with
fsnotify. On write/create/symlink-swap events, re-reads the config and callsonConfigChangecallback. Handles Kubernetes ConfigMap atomic replacement by watching the directory, not just the file. - Key types:
fsnotify.Watcher,onConfigChange func(fsnotify.Event)callback - Dependencies:
github.com/fsnotify/fsnotify
Remote Config Integration#
- Package:
github.com/spf13/viper(remote.go); implementation in separate modulegithub.com/spf13/viper/remote - Responsibility: Plugs in key/value store backends (etcd, Consul, Firestore, NATS). The main package declares the
remoteConfigFactoryinterface and a package-levelRemoteConfigvariable. Theremote/module registers a concrete implementation via a blank import side-effect (_ "github.com/spf13/viper/remote"), avoiding pulling heavy network deps into the main module. - Key types:
remoteConfigFactoryinterface,RemoteProviderinterface,defaultRemoteProvider - Dependencies: None in main module; heavy deps (etcd client, consul client, crypt) live in
remote/’s owngo.mod
Flag Binding#
- Package:
github.com/spf13/viper(flags.go) - Responsibility: Adapts
pflag.FlagSet/pflag.Flagto Viper’s ownFlagValueSet/FlagValueinterfaces, decoupling the core from thepflagpackage at the type level. Flags are only read when they have been explicitly changed (HasChanged()), preserving precedence semantics. - Key types:
FlagValue,FlagValueSet(interfaces);pflagValue,pflagValueSet(concrete wrappers) - Dependencies:
github.com/spf13/pflag(only in wrapper; core depends on the interface)
Data flow#
A typical call to viper.Get("database.host"):
- Global wrapper —
Get("database.host")delegates to the package-levelvsingleton’sGetmethod. - Key normalisation — key is lowercased; the dot delimiter splits it into
["database", "host"]. - Alias resolution —
realKey()resolves any registered aliases. find()waterfall (viper.go:1194): a. Searchv.overridemap (explicitSet()calls) — returns immediately if found. b. Searchv.pflags— only if the flag has been explicitly changed (HasChanged()). c. Search env vars — viaautomaticEnvor explicitBindEnvmappings. d. Searchv.config(parsed config file,map[string]any) — usessearchIndexableWithPathPrefixesfor nested access. e. Searchv.kvstore(remote k/v store data). f. Searchv.defaults(registered viaSetDefault). g. Return pflag default value as last resort.- Type coercion —
Getreturnsany; typed variants (GetString,GetInt, etc.) pass throughgithub.com/spf13/cast. - Return — first non-nil value found at any level;
nilif key is absent in all layers.
A ReadInConfig() flow:
- Discover config file path (via
getConfigFile()which checks explicit path, then search paths, using legacyfile.goor experimentalfinder.go). - Open file through
afero.Fs(allows in-memory FS in tests). - Determine format from file extension.
- Look up
Decoderfromv.decoderRegistry. - Decode bytes into
v.config(map[string]any). - Merge with existing config via
mergeFlatMap/ deep merge.
Initialization / Bootstrap#
- Package
init()—var v *Viperis initialised viainit() { v = New() }. This ensures global-API callers get a ready-to-use singleton without an explicit constructor call. New()— allocates theViperstruct, sets sensible defaults (keyDelim = ".",configName = "config",configPermissions = 0644), creates anafero.OsFs, initialises all six source maps, creates aDefaultCodecRegistry, wires up the codec registry to both encoder and decoder fields, readsinternal/featuresflags.NewWithOptions(opts ...Option)— callsNew()then applies eachOptionvia theOption.apply(*Viper)interface. Options includeWithEncoderRegistry,WithDecoderRegistry,WithCodecRegistry,KeyDelimiter,EnvKeyReplacer,WithDecodeHook.- No dependency injection framework — wiring is entirely manual. The
Option/ functional-options pattern is the DI mechanism: dependencies (custom codec registry, filesystem, log handler) are injected via options at construction time.
Configuration#
Viper configures itself rather than consuming external configuration. Its own configuration surface is:
- Functional options at construction:
NewWithOptions(KeyDelimiter(":"), WithCodecRegistry(myRegistry)) - Setter methods post-construction:
SetEnvPrefix,SetConfigName,AddConfigPath,AutomaticEnv,SetDefault,BindEnv,BindPFlag - No config file or env var of its own — deliberately; Viper is a leaf dependency that other projects use to read their config.
Key design decisions#
Six-layer waterfall with explicit priority order. The
find()function is the architectural heart of Viper. Every source is a separatemap[string]anyfield; priority is enforced by the sequential search order infind(), not by any merge or overwrite at write time. This makes the model easy to reason about: values are never lost, and the “winning” source is always deterministic.Global singleton + instance duality via a shared struct. All global functions (
viper.Get,viper.Set, etc.) are thin wrappers that call the same methods on a package-level*Vipersingleton initialised ininit(). Users who need multiple isolated configs (e.g., per-subsystem) useviper.New()instances. There is zero duplication of logic — both paths invoke identical methods.afero.Fsfor all filesystem access. Every file open, stat, and read goes through theafero.Fsinterface stored inv.fs. This allows tests to useafero.NewMemMapFs()for hermetic, fast file tests and lets advanced users substitute a custom filesystem (e.g., encrypted, embedded). Noos.Opencalls appear outside ofafero.Remote module separation via interface + blank-import registration. The
remoteConfigFactoryinterface lives in the main module; the concrete etcd/Consul implementation lives in a separatego.mod(viper/remote). The remote module self-registers by setting theRemoteConfigpackage variable in itsinit()function when blank-imported. This is a textbook Go plugin pattern that keeps the main module’s dependency graph minimal.Build-tag gated experimental features via
internal/features. New APIs (thelocafero-based finder,BindStruct) are introduced as pairs of files:feature_default.go(flag = false, build tag!viper_xxx) andfeature.go(flag = true, build tagviper_xxx). This is a compile-time feature-flag system with zero runtime cost, allowing users to opt into unstable APIs without polluting the default surface or requiring conditional logic at runtime.