apistockdocs
Decisions

Constructors and configuration

ADR-0020Accepted

Status: Accepted (2026-09-14) · Supersedes: ADR-0008 · Supersedes (with ADR-0017): ADR-0004 · Amended by: ADR-0031

Context#

v1 planned module Config structs with env:"…" tags embedded into one app config. That makes environment variable names part of the library API, ties every module to an env-loading library, and invites a giant global configuration object. Module constructors also need to grow new settings without breaking callers.

Options#

  1. Config structs with env tags in libraries.
  2. Plain config structs passed to every constructor.
  3. Required dependencies as positional parameters, optional settings as functional options; environment mapping owned by the app.

Decision#

Option 3.

Library modules#

go
// Shape only.
func New(db postgres.DBTX, mailer mail.Sender, audit audit.Recorder, opts ...Option) (*Service, error)

auth.WithSessionTTL(24 * time.Hour)
auth.WithPasswordPolicy(policy)
RuleDecision
Required dependenciesPositional parameters. Adding one is a compile error, never a silent nil at runtime.
Optional settingsFunctional options: exported Option interface with an unexported apply method; defaults set before options apply.
Small providers (≤ 3 settings)May accept a plain config struct instead.
Env awarenessNone. Libraries never read environment variables and carry no env tags.
Secretsconfig.Secret type from core: redacted by String() and LogValue().
ValidationConstructors validate options and return errors.

Generated application#

LayerOwns
internal/app/config.goLoading environment variables (.env in development only), typed structs per feature, fail-fast validation listing every problem at once
internal/app/infra_*.go, modules.goMapping each feature's config struct to library options
EnvironmentSource of truth in production; *_FILE variants for secrets mounted as files

Precedence: code defaults → .env (development only) → environment variables → *_FILE secrets.

Not supported: YAML/TOML per-environment overlays, and secrets or infrastructure configuration stored anywhere but the environment. Non-secret tunables that operators change at runtime are runtime settings (ADR-0031), stored in PostgreSQL and edited through /ops/settings; library options documented as live accept config.Value[T].

Why#

  • Library APIs stay stable and testable.
  • The app sees every setting with "go to definition" and controls env naming.
  • No global configuration object is passed into modules.

Trade-offs#

  • More mapping code in the app (generated).
  • Two styles (options and small config structs) to document.

Consequences#

  • aps doctor runs the app's config validation without starting it.
  • .env.example is generated from the app's config structs and kept in sync by recipes.
esc
↑↓ move↵ openesc close