Signed-off-by: Brian Kane <briankane1@gmail.com>
Provider Registry
A minimal, interface-based provider registry for breaking import cycles in the codebase.
Purpose
This registry is a fallback mechanism for situations where import cycles block development work. It provides runtime indirection through interface-based contracts while maintaining type safety.
Why It Exists
Large, mature codebases sometimes develop import cycles between packages:
pkg/appfileneeds types frompkg/controllerpkg/controllerneeds functionality frompkg/appfile- Result: Import cycle prevents compilation
While the ideal solution is restructuring packages with clear boundaries, this isn't always practical in the short term. The registry unblocks development while allowing refactoring efforts to be planned appropriately.
Design Philosophy
Use as a fallback, not a default:
- New code should use well-designed package boundaries and constructor injection
- Existing code can use the registry when cycles genuinely block work
- Services registered here are candidates for future refactoring
Keep it simple:
- Interface-only registration (enforced)
- Thread-safe operations
- No complex lifecycle management
- Stdlib dependencies only
How It Works
1. Define an Interface
// In cmd/core/app/bootstrap.go or appropriate location
type MyProvider interface {
DoSomething(ctx context.Context) error
}
2. Register During Bootstrap
// In cmd/core/app/bootstrap.go
func bootstrapProviderRegistry() {
// MyProvider - Brief description of what it does
// Cycle: pkg/foo ↔ pkg/bar
// Note: Consider refactoring to extract shared interfaces
provider := foo.NewMyProvider()
registry.RegisterAs[MyProvider](provider)
}
3. Retrieve Where Needed
// In any package that needs it
provider, ok := registry.Get[MyProvider]()
if !ok {
return fmt.Errorf("MyProvider not registered")
}
err := provider.DoSomething(ctx)
API
RegisterAs[T any](impl T)- Register an implementation for interface TGet[T any]() (T, bool)- Retrieve registered implementationSnapshot() RegistrySnapshot- Save current state (for testing)Restore(snapshot RegistrySnapshot)- Restore saved state (for testing)
Testing
Use Snapshot/Restore to isolate tests:
func TestSomething(t *testing.T) {
snapshot := registry.Snapshot()
defer registry.Restore(snapshot)
// Override with mock
registry.RegisterAs[MyProvider](mockImpl)
// Test code using mock
}
// Original providers restored automatically
When NOT to Use
Prefer constructor injection when:
- Writing new code with clean package boundaries
- No import cycle exists between packages
- Extracting interfaces to a neutral package is straightforward
- Dependencies can be passed explicitly through constructors
Trade-offs
Benefits:
- Unblocks development immediately
- Breaks cycles without major refactoring
- Type-safe through generics
- Easy to mock in tests
Costs:
- Less visible than constructor injection
- Runtime lookups instead of compile-time
- Can hide architectural issues if overused
Guidance
Use this registry judiciously as a pragmatic tool. Each provider registered represents an opportunity to improve package structure in the future. The goal is to keep usage minimal while unblocking important development work.
For detailed implementation guidelines, see the package documentation in registry.go.