The modular framework now provides four types of configuration providers, each optimized for different use cases. This document explains when and how to use each provider type.
What it does: Returns the SAME reference on every GetConfig() call.
Use cases:
- Simple single-instance applications
- When you explicitly need shared mutable config
- Legacy compatibility
Thread safety: ❌ NOT thread-safe for modifications
Performance: ⚡️ Excellent (no overhead)
Example:
cfg := &MyConfig{Host: "localhost", Port: 8080}
provider := modular.NewStdConfigProvider(cfg)
// Both get the same reference
cfg1 := provider.GetConfig().(*MyConfig)
cfg2 := provider.GetConfig().(*MyConfig)
// cfg1 == cfg2 (same pointer)What it does: Returns a deep copy on EVERY GetConfig() call.
Use cases:
- ✅ Test isolation (RECOMMENDED for tests)
- Multi-tenant applications requiring per-tenant isolation
- Defensive programming where modules might mutate configs
Thread safety: ✅ Thread-safe (each call gets independent copy)
Performance: 🐌 Slower (deep copy on every access)
Example:
cfg := &MyConfig{Host: "localhost", Port: 8080}
provider := modular.NewIsolatedConfigProvider(cfg)
// Each call returns a completely independent copy
copy1 := provider.GetConfig().(*MyConfig)
copy2 := provider.GetConfig().(*MyConfig)
// copy1 != copy2 (different pointers)
copy1.Port = 9090 // Does NOT affect copy2✅ BEST FOR: Test scenarios to prevent config pollution between test runs.
What it does: Stores config in atomic.Value for lock-free concurrent reads.
Use cases:
- ✅ Production applications (RECOMMENDED for production)
- High-performance read-heavy workloads
- Configuration hot-reloading with atomic swaps
- Multi-tenant applications with shared config
Thread safety: ✅ Fully thread-safe with atomic operations
Performance: ⚡️ Excellent (lock-free reads, no copies)
Example:
cfg := &MyConfig{Host: "localhost", Port: 8080}
provider := modular.NewImmutableConfigProvider(cfg)
// Thread-safe reads from multiple goroutines
config := provider.GetConfig().(*MyConfig)
// Atomic update (useful for config hot-reload)
newCfg := &MyConfig{Host: "example.com", Port: 443}
provider.UpdateConfig(newCfg)✅ BEST FOR: Production concurrent scenarios with high read throughput.
What it does: Returns original for reads, provides explicit method for getting mutable copies.
Use cases:
- Modules that need to apply defensive modifications
- When you want explicit control over when copies are made
- Scenarios requiring both read-only and mutable access
Thread safety: ✅ Thread-safe with RWMutex
Performance: 🚀 Good (only copies when explicitly requested)
Example:
cfg := &MyConfig{Host: "localhost", Port: 8080}
provider := modular.NewCopyOnWriteConfigProvider(cfg)
// Read-only access (no copy, fast)
readCfg := provider.GetConfig().(*MyConfig)
// Need to modify? Get a mutable copy
mutableCfg, err := provider.GetMutableConfig()
if err == nil {
cfg := mutableCfg.(*MyConfig)
cfg.Port = 9090 // Safe to modify, won't affect others
}
// Update the original (e.g., for hot-reload)
newCfg := &MyConfig{Host: "example.com", Port: 443}
provider.UpdateOriginal(newCfg)✅ BEST FOR: Modules that occasionally need to make defensive config modifications.
| Scenario | Recommended Provider | Rationale |
|---|---|---|
| Unit Tests | IsolatedConfigProvider |
Prevents test pollution |
| Integration Tests | IsolatedConfigProvider |
Ensures test isolation |
| Production Single-Threaded | StdConfigProvider |
Simplest, no overhead |
| Production Multi-Threaded | ImmutableConfigProvider |
Lock-free, thread-safe |
| Config Hot-Reload | ImmutableConfigProvider |
Atomic updates |
| Multi-Tenant Apps | IsolatedConfigProvider or ImmutableConfigProvider |
Depends on isolation needs |
| Defensive Modules | CopyOnWriteConfigProvider |
Explicit copy control |
Bad:
func (m *MyModule) Init(app modular.Application) error {
cfg := app.GetConfig().(*MyConfig)
if cfg.Port == 0 {
cfg.Port = 8080 // ❌ Modifies shared config!
}
return nil
}Good:
func (m *MyModule) Init(app modular.Application) error {
cfg := app.GetConfig().(*MyConfig)
if cfg.Port == 0 {
// Use defaults without mutation
m.port = 8080
} else {
m.port = cfg.Port
}
return nil
}Good:
func TestMyModule(t *testing.T) {
cfg := &MyConfig{Host: "localhost", Port: 8080}
// Each test gets isolated config
provider := modular.NewIsolatedConfigProvider(cfg)
app := modular.NewApplication(
modular.NewStdConfigProvider(provider),
logger,
)
// Test won't pollute other tests
}Good:
func main() {
cfg := loadConfigFromFile()
// Thread-safe for concurrent access
provider := modular.NewImmutableConfigProvider(cfg)
app := modular.NewApplication(
modular.NewStdConfigProvider(provider),
logger,
)
// Can hot-reload config atomically
go watchConfigChanges(func(newCfg *Config) {
provider.UpdateConfig(newCfg)
})
}Good:
func (m *MyModule) Init(app modular.Application) error {
cowProvider := app.GetConfig().(*modular.CopyOnWriteConfigProvider)
// Get mutable copy for safe modifications
mutableCfg, err := cowProvider.GetMutableConfig()
if err != nil {
return err
}
cfg := mutableCfg.(*MyConfig)
// Safe to modify - won't affect other modules
cfg.Port = normalizePort(cfg.Port)
m.config = cfg
return nil
}Based on benchmarks (see config_provider_test.go):
BenchmarkConfigProviders/StdConfigProvider-10 ⚡️ ~1-2 ns/op
BenchmarkConfigProviders/ImmutableConfigProvider-10 ⚡️ ~3-5 ns/op
BenchmarkConfigProviders/CopyOnWriteConfigProvider_Read-10 🚀 ~10-20 ns/op
BenchmarkConfigProviders/IsolatedConfigProvider-10 🐌 ~500-2000 ns/op
BenchmarkConfigProviders/CopyOnWriteConfigProvider_Mutable-10 🐌 ~500-2000 ns/op
Key Takeaways:
StdConfigProvideris fastest but unsafeImmutableConfigProviderhas minimal overhead with full safetyIsolatedConfigProvideris slower but provides complete isolationCopyOnWriteConfigProvideris fast for reads, slower for mutable copies
Before:
cfg := &MyConfig{}
provider := modular.NewStdConfigProvider(cfg)After:
cfg := &MyConfig{}
provider := modular.NewIsolatedConfigProvider(cfg)Before:
cfg := loadConfig()
provider := modular.NewStdConfigProvider(cfg)
app := modular.NewApplication(provider, logger)After:
cfg := loadConfig()
provider := modular.NewImmutableConfigProvider(cfg)
app := modular.NewApplication(provider, logger)
// Optional: hot-reload support
provider.UpdateConfig(newCfg)The framework also exports a utility function for manually creating deep copies:
originalCfg := &MyConfig{
Host: "localhost",
Tags: []string{"a", "b"},
Metadata: map[string]string{"key": "value"},
}
// Create a deep copy
copiedCfg, err := modular.DeepCopyConfig(originalCfg)
if err != nil {
// Handle error
}
// Modifications to copy don't affect original
copy := copiedCfg.(*MyConfig)
copy.Tags[0] = "modified" // Original remains unchangedThis is useful when you need manual control over config copying outside of providers.
The framework provides specialized support for multi-tenant configurations with built-in isolation:
For complete tenant isolation:
defaultCfg := &MyConfig{Host: "localhost", Port: 8080}
// Each tenant gets isolated copies of configs
tcp := modular.NewTenantConfigProviderWithIsolation(defaultCfg)
// Set isolated config for tenant1
tenant1Cfg := &DatabaseConfig{Host: "tenant1-db.example.com"}
tcp.SetTenantConfigIsolated("tenant1", "database", tenant1Cfg)
// Set isolated config for tenant2
tenant2Cfg := &DatabaseConfig{Host: "tenant2-db.example.com"}
tcp.SetTenantConfigIsolated("tenant2", "database", tenant2Cfg)
// Each tenant gets completely isolated copies
provider, _ := tcp.GetTenantConfig("tenant1", "database")
cfg := provider.GetConfig().(*DatabaseConfig)
// Modifications to cfg won't affect tenant2 or the original✅ BEST FOR: Multi-tenant SaaS applications requiring strict tenant isolation.
For shared thread-safe configs:
// All tenants share immutable config (thread-safe)
tcp := modular.NewTenantConfigProviderImmutable(sharedCfg)
// Set immutable config for specific tenant
tcp.SetTenantConfigImmutable("tenant1", "cache", &CacheConfig{
TTL: 60 * time.Second,
})✅ BEST FOR: Multi-tenant apps where tenants share common config with thread-safe access.
You can mix different provider types for different tenants:
tcp := modular.NewTenantConfigProvider(defaultProvider)
// Tenant1 needs isolation
tcp.SetTenantConfigIsolated("tenant1", "app", cfg1)
// Tenant2 needs thread-safe shared config
tcp.SetTenantConfigImmutable("tenant2", "app", cfg2)
// Tenant3 uses standard provider
tcp.SetTenantConfig("tenant3", "app", modular.NewStdConfigProvider(cfg3))-
Use Isolation for Sensitive Data:
// Customer-specific database configs should be isolated tcp.SetTenantConfigIsolated(tenantID, "database", dbConfig)
-
Use Immutable for Shared Resources:
// Shared cache settings can be immutable tcp.SetTenantConfigImmutable(tenantID, "cache", cacheConfig)
-
Prevent Cross-Tenant Pollution:
// ❌ Bad: Shared mutable config can leak between tenants tcp.SetTenantConfig(tenantID, "app", modular.NewStdConfigProvider(cfg)) // ✅ Good: Isolated configs prevent cross-tenant pollution tcp.SetTenantConfigIsolated(tenantID, "app", cfg)
- CONFIG_ISOLATION_ARCHITECTURE.md - Problem analysis
- CLAUDE.md - Development guidelines
- AGENTS.md - Architecture overview
Choose your configuration provider based on your needs:
| Priority | Choose This |
|---|---|
| Test Isolation | IsolatedConfigProvider |
| Production Performance | ImmutableConfigProvider |
| Defensive Modules | CopyOnWriteConfigProvider |
| Simple/Legacy | StdConfigProvider (with caution) |
Default Recommendation: Use IsolatedConfigProvider for tests and ImmutableConfigProvider for production.