zoobzio December 10, 2025 Edit this page

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

SourceRecommended Debounce
File (editor save)100-200ms
Redis/Consul50-100ms
Kubernetes ConfigMap100-500ms
Database200-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