Files

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/appfile needs types from pkg/controller
  • pkg/controller needs functionality from pkg/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 T
  • Get[T any]() (T, bool) - Retrieve registered implementation
  • Snapshot() 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.