zoobzio December 10, 2025 Edit this page

Overview

Configuration management in Go usually means one of two extremes: reload-on-restart or complex orchestration.

Flux offers a third path: reactive configuration that updates automatically when sources change.

type Config struct {
    Port int    `json:"port"`
    Host string `json:"host"`
}

func (c Config) Validate() error {
    if c.Port < 1 || c.Port > 65535 {
        return errors.New("port must be between 1 and 65535")
    }
    if c.Host == "" {
        return errors.New("host is required")
    }
    return nil
}

capacitor := flux.New[Config](
    file.New("/etc/myapp/config.json"),
    func(ctx context.Context, prev, curr Config) error {
        log.Printf("config changed: port %d -> %d", prev.Port, curr.Port)
        return app.Reconfigure(curr)
    },
)

if err := capacitor.Start(ctx); err != nil {
    log.Printf("initial config failed: %v", err)
}

File changes trigger automatic reload. Invalid configs are rejected. Previous valid config is retained on failure.

Core Concepts

Capacitor - The central type. Watches a source, deserializes data, validates it, and delivers changes to your callback.

Watcher - Abstraction for data sources. Implementations exist for files, Redis, Consul, etcd, NATS, Kubernetes, ZooKeeper, and Firestore.

State Machine - Tracks configuration health: Loading, Healthy, Degraded, or Empty.

Codec - Handles deserialization. JSON by default, YAML available.

The Pipeline

Every configuration change flows through:

Source → Deserialize → Validate → Callback
  1. Source emits raw bytes when data changes
  2. Deserialize converts bytes to your struct (JSON/YAML)
  3. Validate calls your struct's Validate() method
  4. Callback receives both previous and current values

If any step fails, the previous valid configuration is retained.

State Machine

┌─────────┐   success   ┌─────────┐
│ Loading │────────────▶│ Healthy │◀──┐
└─────────┘             └─────────┘   │
     │                       │        │
     │ failure          failure│   success
     ▼                       ▼        │
┌─────────┐             ┌─────────┐───┘
│  Empty  │             │Degraded │
└─────────┘             └─────────┘
StateHas ConfigErrorMeaning
LoadingNoNoWaiting for first value
HealthyYesNoValid config active
DegradedYesYesUpdate failed, previous config retained
EmptyNoYesNo valid config ever obtained

Providers

The core package is zero-dependency. Providers live in pkg/ with isolated dependencies:

ProviderBackendWatch Mechanism
pkg/fileLocal filesystemfsnotify
pkg/redisRedisKeyspace notifications
pkg/consulConsul KVBlocking queries
pkg/etcdetcdWatch API
pkg/natsNATS JetStreamKV Watch
pkg/kubernetesConfigMap/SecretWatch API
pkg/zookeeperZooKeeperNode watch
pkg/firestoreFirestoreRealtime listeners

Observability

Flux emits capitan signals for all state transitions and errors:

capitan.Hook(flux.CapacitorStateChanged, func(ctx context.Context, e *capitan.Event) {
    old, _ := flux.KeyOldState.From(e)
    new, _ := flux.KeyNewState.From(e)
    log.Printf("state: %s -> %s", old, new)
})

When to Use Flux

Good fit:

  • Feature flags that change at runtime
  • Database connection pool sizing
  • Rate limits and quotas
  • Service discovery endpoints
  • A/B test configurations

Not ideal:

  • Secrets that require rotation workflows
  • Configuration that requires coordinated rollout
  • Static config that never changes

Next Steps