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
- Source emits raw bytes when data changes
- Deserialize converts bytes to your struct (JSON/YAML)
- Validate calls your struct's
Validate()method - 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 │
└─────────┘ └─────────┘
| State | Has Config | Error | Meaning |
|---|---|---|---|
| Loading | No | No | Waiting for first value |
| Healthy | Yes | No | Valid config active |
| Degraded | Yes | Yes | Update failed, previous config retained |
| Empty | No | Yes | No valid config ever obtained |
Providers
The core package is zero-dependency. Providers live in pkg/ with isolated dependencies:
| Provider | Backend | Watch Mechanism |
|---|---|---|
pkg/file | Local filesystem | fsnotify |
pkg/redis | Redis | Keyspace notifications |
pkg/consul | Consul KV | Blocking queries |
pkg/etcd | etcd | Watch API |
pkg/nats | NATS JetStream | KV Watch |
pkg/kubernetes | ConfigMap/Secret | Watch API |
pkg/zookeeper | ZooKeeper | Node watch |
pkg/firestore | Firestore | Realtime 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
- Quickstart - Get running in 5 minutes
- Core Concepts - Understand the primitives
- Providers Guide - Choose your data source