Dependency Resolution
Understanding how godi resolves dependencies helps you build better applications.
How Resolution Works
The Dependency Graph
When you call Build(), godi:
Analyzes all registered constructors
Builds a dependency graph
Validates for cycles and lifetime violations
Sorts services in dependency order
Creates singletons at build time
// Given these registrations:
services.AddSingleton(NewLogger) // No dependencies
services.AddSingleton(NewConfig) // No dependencies
services.AddSingleton(NewDatabase) // Needs Config, Logger
services.AddSingleton(NewCache) // Needs Config
services.AddScoped(NewUserRepository) // Needs Database, Cache
services.AddScoped(NewUserService) // Needs UserRepository, Logger
// godi creates this dependency order:
// 1. Logger, Config (no dependencies, can be parallel)
// 2. Database, Cache (depend on level 1)
// 3. UserRepository (depends on level 2)
// 4. UserService (depends on level 3)
Resolution Process
When you request a service:
userService := godi.MustResolve[UserService](provider)
godi follows this process:
Check cache - Is it already created?
Find descriptor - Get the service registration
Resolve dependencies - Recursively resolve each dependency
Create instance - Call the constructor
Cache if needed - Based on lifetime
Return instance
Resolution Methods
Basic Resolution
// Resolve with error handling
service, err := godi.Resolve[MyService](provider)
if err != nil {
// Handle error - service not found, etc.
}
// Resolve or panic
service := godi.MustResolve[MyService](provider)
Keyed Resolution
// Resolve named service
cache := godi.MustResolveKeyed[Cache](provider, "redis")
// With error handling
cache, err := godi.ResolveKeyed[Cache](provider, "redis")
Group Resolution
// Resolve all services in a group
handlers := godi.MustResolveGroup[Handler](provider, "http")
// With error handling
handlers, err := godi.ResolveGroup[Handler](provider, "http")
Automatic Dependency Injection
Constructor Analysis
godi automatically analyzes constructor parameters:
func NewUserService(
db Database, // godi will inject Database
cache Cache, // godi will inject Cache
logger Logger, // godi will inject Logger
config *AppConfig, // godi will inject *AppConfig
) UserService {
return &userService{
db: db,
cache: cache,
logger: logger,
config: config,
}
}
// Just register it - godi handles the rest
services.AddScoped(NewUserService)
Recursive Resolution
Dependencies are resolved recursively:
// A depends on B
// B depends on C
// C depends on D
func NewA(b B) A { return &a{b: b} }
func NewB(c C) B { return &b{c: c} }
func NewC(d D) C { return &c{d: d} }
func NewD() D { return &d{} }
// When you resolve A:
a := godi.MustResolve[A](provider)
// godi automatically:
// 1. Sees A needs B
// 2. Sees B needs C
// 3. Sees C needs D
// 4. Creates D (no dependencies)
// 5. Creates C with D
// 6. Creates B with C
// 7. Creates A with B
Circular Dependencies
Detection
godi detects circular dependencies at build time:
func NewServiceA(b ServiceB) ServiceA {
return &serviceA{b: b}
}
func NewServiceB(a ServiceA) ServiceB {
return &serviceB{a: a}
}
services.AddSingleton(NewServiceA)
services.AddSingleton(NewServiceB)
provider, err := services.Build()
// Error: Circular dependency detected: ServiceA -> ServiceB -> ServiceA
Breaking Circles
Option 1: Lazy Resolution with Provider
type ServiceA interface {
DoWork()
}
type serviceA struct {
provider godi.Provider
}
func NewServiceA(provider godi.Provider) ServiceA {
return &serviceA{provider: provider}
}
func (a *serviceA) DoWork() {
// Resolve B only when needed
b := godi.MustResolve[ServiceB](a.provider)
b.Process()
}
Option 2: Setter Injection
type ServiceA interface {
Process()
}
type serviceA struct {
b ServiceB
}
func NewServiceA() ServiceA {
return &serviceA{}
}
func (a *serviceA) SetServiceB(b ServiceB) {
a.b = b
}
// After resolution
a := godi.MustResolve[ServiceA](provider)
b := godi.MustResolve[ServiceB](provider)
a.SetServiceB(b)
Option 3: Refactor Design
// Extract shared functionality
type SharedService interface {
SharedMethod()
}
func NewSharedService() SharedService {
return &sharedService{}
}
func NewServiceA(shared SharedService) ServiceA {
return &serviceA{shared: shared}
}
func NewServiceB(shared SharedService) ServiceB {
return &serviceB{shared: shared}
}
Lifetime Validation
The Problem
Singletons can’t depend on scoped services:
// ❌ This will fail at build time
func NewSingletonCache(ctx RequestContext) Cache { // RequestContext is scoped
return &cache{context: ctx}
}
services.AddSingleton(NewSingletonCache)
services.AddScoped(NewRequestContext)
provider, err := services.Build()
// Error: Singleton service Cache cannot depend on Scoped service RequestContext
Why It Matters
// If this were allowed:
// Request 1
scope1 := provider.CreateScope(ctx)
cache := godi.MustResolve[Cache](scope1) // Singleton created with scope1's context
// Request 2
scope2 := provider.CreateScope(ctx)
cache2 := godi.MustResolve[Cache](scope2) // Same singleton, still has scope1's context!
// Bug: cache is using wrong request context
Valid Dependency Chains
// ✅ Singleton -> Singleton
services.AddSingleton(NewLogger)
services.AddSingleton(NewDatabase) // Can depend on Logger
// ✅ Scoped -> Singleton
services.AddSingleton(NewDatabase)
services.AddScoped(NewRepository) // Can depend on Database
// ✅ Scoped -> Scoped
services.AddScoped(NewRequestContext)
services.AddScoped(NewUserService) // Can depend on RequestContext
// ✅ Transient -> Any
services.AddTransient(NewTempHandler) // Can depend on anything
Resolution Context
Using Scopes
// Root provider resolution
logger := godi.MustResolve[Logger](provider) // From root
// Scoped resolution
scope, _ := provider.CreateScope(context.Background())
defer scope.Close()
service := godi.MustResolve[MyService](scope) // From scope
Context Integration
// Store scope in context
func Middleware(provider godi.Provider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
scope, _ := provider.CreateScope(r.Context())
defer scope.Close()
// Pass scope through context
r = r.WithContext(scope.Context())
// ... handle request
}
}
// Retrieve scope from context
func DeepFunction(ctx context.Context) {
scope, _ := godi.FromContext(ctx)
service := godi.MustResolve[MyService](scope)
}
Performance Considerations
Caching Strategy
// Singleton: Resolved once, cached forever
// Cost: O(1) after first resolution
logger := godi.MustResolve[Logger](provider)
// Scoped: Resolved once per scope, cached in scope
// Cost: O(1) within same scope
ctx := godi.MustResolve[RequestContext](scope)
// Transient: Never cached, always created
// Cost: O(n) where n is dependency depth
temp := godi.MustResolve[TempHandler](provider)
Build-Time Optimization
// Singletons are created at build time
services.AddSingleton(NewExpensiveService) // 5 second initialization
start := time.Now()
provider, _ := services.Build() // Takes 5 seconds
fmt.Printf("Build took: %v\n", time.Since(start))
// Resolution is instant
start = time.Now()
service := godi.MustResolve[ExpensiveService](provider) // Instant
fmt.Printf("Resolution took: %v\n", time.Since(start))
Error Handling
Resolution Errors
// Service not found
service, err := godi.Resolve[UnknownService](provider)
if err != nil {
// err: service not found
}
// Keyed service not found
cache, err := godi.ResolveKeyed[Cache](provider, "unknown")
if err != nil {
// err: keyed service 'unknown' not found
}
// Constructor error
func NewDatabase() (Database, error) {
return nil, errors.New("connection failed")
}
services.AddSingleton(NewDatabase)
provider, err := services.Build()
// err: failed to create singleton: connection failed
Best Practices
Let godi handle dependencies - Don’t manually wire services
Validate at build time - Catch issues early
Use appropriate lifetimes - Prevent lifetime violations
Avoid circular dependencies - Refactor if detected
Cache expensive services - Use singleton or scoped
Handle resolution errors - Don’t always use MustResolve
Next Steps
Explore Scopes & Isolation
Learn about Resource Management
Understand Modules