Best Practices
Guidelines for using flux effectively in production systems.
Configuration Design
Keep Configs Small and Focused
// Good: Focused configuration
type FeatureFlags struct {
EnableNewCheckout bool `json:"enable_new_checkout"`
EnableDarkMode bool `json:"enable_dark_mode"`
MaxUploadSize int `json:"max_upload_size" validate:"min=1"`
}
// Avoid: Kitchen sink configuration
type EverythingConfig struct {
Database DatabaseConfig
Cache CacheConfig
Features FeatureFlags
Logging LoggingConfig
Secrets SecretsConfig // Don't mix secrets with config
// ...50 more fields
}
Implement Thorough Validation
Always implement comprehensive Validate() methods:
type Config struct {
Port int `json:"port"`
Host string `json:"host"`
Timeout time.Duration `json:"timeout"`
LogLevel string `json:"log_level"`
MaxRetries int `json:"max_retries"`
Concurrency int `json:"concurrency"`
}
func (c Config) Validate() error {
if c.Port < 1 || c.Port > 65535 {
return fmt.Errorf("port must be between 1 and 65535")
}
if c.Host == "" {
return errors.New("host is required")
}
if c.Timeout < time.Second || c.Timeout > 5*time.Minute {
return errors.New("timeout must be between 1s and 5m")
}
switch c.LogLevel {
case "debug", "info", "warn", "error":
default:
return errors.New("log_level must be one of: debug, info, warn, error")
}
if c.MaxRetries < 0 || c.MaxRetries > 10 {
return errors.New("max_retries must be between 0 and 10")
}
if c.Concurrency < 1 || c.Concurrency > 1000 {
return errors.New("concurrency must be between 1 and 1000")
}
return nil
}
Validate Business Rules in Callback
For rules that span multiple fields:
capacitor := flux.New[Config](
watcher,
func(ctx context.Context, prev, curr Config) error {
// Cross-field validation
if curr.MinConnections > curr.MaxConnections {
return errors.New("min_connections cannot exceed max_connections")
}
// Safe transitions
if prev.MaxConnections > 0 && curr.MaxConnections < prev.MaxConnections/2 {
return errors.New("cannot reduce max_connections by more than 50%")
}
return app.Apply(curr)
},
)
Callback Design
Keep Callbacks Fast
The callback blocks the processing goroutine. Avoid slow operations:
// Good: Quick application
func(ctx context.Context, prev, curr Config) error {
app.config.Store(&curr)
return nil
}
// Avoid: Slow operations in callback
func(ctx context.Context, prev, curr Config) error {
db.Migrate(curr.Schema) // Slow
http.Post(webhookURL, curr) // Network I/O
time.Sleep(time.Second) // Never do this
return nil
}
For slow operations, trigger them asynchronously:
func(ctx context.Context, prev, curr Config) error {
app.config.Store(&curr)
// Trigger async if needed
if prev.Schema != curr.Schema {
go app.scheduleMigration(curr.Schema)
}
return nil
}
Handle Prev Value Correctly
On first load, prev is the zero value:
func(ctx context.Context, prev, curr Config) error {
if prev.Port == 0 {
// First load - initialize
return app.Initialize(curr)
}
// Subsequent loads - update
if prev.Port != curr.Port {
return app.RestartServer(curr.Port)
}
return app.UpdateConfig(curr)
}
Error Handling
Don't Swallow Errors
Return errors from callback to trigger Degraded state:
// Good: Return errors
func(ctx context.Context, prev, curr Config) error {
if err := app.Apply(curr); err != nil {
return fmt.Errorf("failed to apply config: %w", err)
}
return nil
}
// Avoid: Swallowing errors
func(ctx context.Context, prev, curr Config) error {
if err := app.Apply(curr); err != nil {
log.Printf("error: %v", err) // Logged but not returned
}
return nil // State shows Healthy but config wasn't applied
}
Monitor All Error Types
func setupMonitoring() {
capitan.Hook(flux.CapacitorTransformFailed, logAndAlert)
capitan.Hook(flux.CapacitorValidationFailed, logAndAlert)
capitan.Hook(flux.CapacitorApplyFailed, logAndAlert)
}
func logAndAlert(ctx context.Context, e *capitan.Event) {
err, _ := flux.KeyError.From(e)
log.Printf("[%s] %s", e.Signal.Name(), err)
metrics.Increment("config_errors", "signal", e.Signal.Name())
}
Debounce Tuning
Match Your Use Case
// Fast iteration (development)
flux.New[Config](watcher, callback).Debounce(50 * time.Millisecond)
// Normal operation (production)
flux.New[Config](watcher, callback).Debounce(100 * time.Millisecond) // Default
// Expensive operations
flux.New[Config](watcher, callback).Debounce(500 * time.Millisecond)
// Very expensive (database migrations)
flux.New[Config](watcher, callback).Debounce(5 * time.Second)
Consider Source Characteristics
| Source | Recommended Debounce |
|---|---|
| File (editor save) | 100-200ms |
| Redis/Consul | 50-100ms |
| Kubernetes ConfigMap | 100-500ms |
| Database | 200-1000ms |
Testing
Always Use Sync Mode in Unit Tests
func TestConfig(t *testing.T) {
capacitor := flux.New[Config](
flux.NewSyncChannelWatcher(ch),
callback,
).SyncMode() // Deterministic
}
Test Error Paths
func TestInvalidConfig(t *testing.T) {
ch := make(chan []byte, 1)
ch <- []byte(`{"port": -1}`) // Invalid
capacitor := flux.New[Config](
flux.NewSyncChannelWatcher(ch),
callback,
).SyncMode()
err := capacitor.Start(ctx)
assert.Error(t, err)
assert.Equal(t, flux.StateEmpty, capacitor.State())
}
Test Recovery
func TestRecovery(t *testing.T) {
ch := make(chan []byte, 3)
ch <- []byte(`{"port": 8080}`) // Valid
ch <- []byte(`{"port": -1}`) // Invalid
ch <- []byte(`{"port": 9090}`) // Recovery
// ... verify state transitions
}
Production Checklist
- Validation tags on all config fields
- Business rule validation in callback
- Error monitoring via capitan hooks
- Health endpoint exposing state
- Metrics for state and errors
- Alerting on Degraded/Empty states
- Graceful degradation with defaults
- Integration tests with real providers
- Documentation of config schema
Anti-Patterns
Don't Create Capacitors Dynamically
// Bad: Creates unbounded goroutines
func handleRequest(key string) {
capacitor := flux.New[Config](
redis.New(client, key),
callback,
)
capacitor.Start(ctx)
}
Don't Ignore Initial Errors
// Bad: Ignoring initial error
capacitor.Start(ctx) // Error discarded
// Good: Handle initial error
if err := capacitor.Start(ctx); err != nil {
// Decide: fatal, defaults, or wait
}
Don't Mix Secrets with Config
// Bad: Secrets in config
type Config struct {
Port int `json:"port"`
APISecret string `json:"api_secret"` // Use secret manager
}
// Good: Separate concerns
type Config struct {
Port int `json:"port"`
}
// Use dedicated secret manager for sensitive data
Next Steps
- State Management - Recovery patterns
- Testing Guide - Comprehensive testing