Docs Configuration Management

Configuration Management

Configuration is not data.

It is environmental influence on behavior.

When configuration handling is sloppy, systems tend to exhibit:

  • Hidden behavior differences between environments
  • Hard-to-reproduce bugs
  • Accidental production-only logic
  • Implicit coupling between infrastructure and core logic

Plumego encourages a strict stance:

Configuration must be explicit, centralized, and injected — never pulled ad hoc.

This document defines a configuration management pattern that keeps systems predictable as they grow.


The Core Problem with Configuration

In many systems, configuration access looks like this:

  • Environment variables read anywhere
  • Global config packages imported everywhere
  • Feature flags checked deep inside domain logic
  • Conditional behavior hidden behind if os.Getenv(...)

This leads to:

  • Untraceable behavior changes
  • Poor testability
  • Inconsistent defaults
  • Architecture leakage

Configuration becomes control flow, but without visibility.


Configuration Is an Input, Not a Dependency

A key mental model:

Configuration is input to the system, not something the system should depend on implicitly.

This means:

  • Configuration is read at startup
  • Parsed into explicit structures
  • Passed into components that need it
  • Never fetched globally during execution

This mirrors how external requests are handled —
explicit inputs at the boundary.


Centralize Configuration Loading

All configuration loading should happen in one place.

Typically:

  • main
  • or a dedicated bootstrap package

Example (conceptual):

type Config struct {
	Env        string
	HTTP       HTTPConfig
	Database   DatabaseConfig
	JWT        JWTConfig
}

func LoadConfig() (Config, error) {
	// read env vars, files, flags
}

After loading, configuration should be treated as immutable.


Configuration Should Be Typed

Avoid configuration as raw strings or maps.

Prefer explicit types:

type HTTPConfig struct {
	Addr         string
	ReadTimeout  time.Duration
	WriteTimeout time.Duration
}

Typed configuration provides:

  • Validation at startup
  • Clear documentation
  • Safer refactoring
  • Better IDE support

If configuration cannot be typed, it cannot be trusted.


Validate Configuration Early

Configuration errors should fail fast and loud.

Recommended practices:

  • Validate required fields
  • Validate ranges and formats
  • Fail during startup, not at first request

Example:

if cfg.HTTP.Addr == "" {
	return errors.New("HTTP address is required")
}

A service that starts with invalid configuration is already broken.


Inject Configuration Explicitly

Once loaded, configuration must be injected, not accessed globally.

Example:

func NewServer(cfg Config) *http.Server {
	// use cfg explicitly
}

This applies to:

  • HTTP servers
  • Database connections
  • Middleware
  • Usecases
  • Infrastructure adapters

Avoid config.Get() or global singletons.


Configuration Boundaries by Layer

Different layers should see different shapes of configuration.

LayerConfiguration Exposure
DomainNone
UsecaseBehavior flags only
InfrastructureConnection details
HTTPTimeouts, limits

Do not pass full config structs everywhere.

Pass only what is needed.


Feature Flags and Configuration

Feature flags are configuration with behavioral impact.

Treat them carefully:

  • Flags belong in application/usecase layer
  • Flags should be injected, not queried globally
  • Flag-driven behavior must be testable

Avoid:

if os.Getenv("ENABLE_X") == "true" {
	// logic
}

Prefer:

type FeatureFlags struct {
	EnableX bool
}

Configuration and Testing

Explicit configuration dramatically improves testability:

  • Tests can inject minimal config
  • No environment manipulation required
  • Behavior is deterministic

Example:

cfg := Config{
	Env: "test",
}

Tests should never rely on real environment variables.


Configuration Reloading (Advanced)

Hot-reloading configuration adds complexity.

If required:

  • Separate reloadable config from static config
  • Make reload boundaries explicit
  • Ensure thread safety
  • Log reload events clearly

Most systems do not need runtime reload.

Static configuration is simpler and safer.


Avoiding Common Configuration Anti-Patterns

Global Config Packages

They hide dependencies and break test isolation.


Reading Environment Variables in Business Logic

This leaks infrastructure concerns inward.


Configuration as Control Flow

Behavior that changes based on config without visibility is a maintenance hazard.


Configuration and Observability

Configuration should be observable:

  • Log effective configuration at startup (excluding secrets)
  • Log environment identity (dev / staging / prod)
  • Log feature flag states when relevant

This helps correlate behavior with configuration.


Secrets Are Not Configuration

Secrets require additional care:

  • Use secure storage
  • Avoid logging
  • Inject via the same mechanism, but with stricter handling

Do not treat secrets casually just because they come from environment variables.


Summary

In Plumego:

  • Configuration is input
  • Loaded once, early
  • Typed and validated
  • Injected explicitly
  • Never accessed globally
  • Invisible to the domain

Good configuration management removes an entire class of bugs
before they can happen.


Next

With configuration under control, the next useful pattern is:

→ Dependency Wiring

This explains how to assemble the system explicitly without relying on magic or frameworks.