Performance Guide
This guide covers performance considerations and optimization techniques when using godi.
Performance Overview
godi is built on top of Uber’s dig, which is highly optimized. However, understanding the performance characteristics helps you make better design decisions.
Performance Characteristics
Operation |
Performance |
Notes |
|---|---|---|
Service Registration |
O(1) |
One-time at startup |
Provider Build |
O(n) |
Once at startup, validates graph |
Singleton Resolution |
O(1)* |
Cached after first creation |
Scoped Resolution |
O(1)* |
Cached within scope |
Transient Resolution |
O(d) |
d = dependency depth |
Scope Creation |
O(s) |
s = scoped services count |
* After initial creation
Resolution Performance
Singleton Services
Singletons are the most performant - created once and cached:
// First resolution - constructs the service
logger, _ := godi.Resolve[Logger](provider) // ~100μs
// Subsequent resolutions - returns cached instance
logger2, _ := godi.Resolve[Logger](provider) // ~1μs
Scoped Services
Scoped services are cached within their scope:
scope := provider.CreateScope(ctx)
// First resolution in scope - constructs the service
repo, _ := godi.Resolve[Repository](scope.ServiceProvider()) // ~50μs
// Subsequent resolutions in same scope - cached
repo2, _ := godi.Resolve[Repository](scope.ServiceProvider()) // ~1μs
// Different scope - new instance
scope2 := provider.CreateScope(ctx)
repo3, _ := godi.Resolve[Repository](scope2.ServiceProvider()) // ~50μs
Transient Services
Transients have the highest overhead - created every time:
// Every resolution creates a new instance
cmd1, _ := godi.Resolve[Command](provider) // ~30μs
cmd2, _ := godi.Resolve[Command](provider) // ~30μs (not cached)
Optimization Techniques
1. Cache Service Resolution
For hot paths, resolve services once:
// ❌ Bad - resolves on every request
func (h *Handler) HandleRequest(w http.ResponseWriter, r *http.Request) {
service, _ := godi.Resolve[UserService](h.scope.ServiceProvider())
service.ProcessRequest(r)
}
// ✅ Good - resolve once in constructor
type Handler struct {
userService UserService
}
func NewHandler(userService UserService) *Handler {
return &Handler{userService: userService}
}
2. Minimize Transient Usage
Use transients only when necessary:
// ❌ Bad - transient for stateless service
collection.AddTransient(NewLogger) // Creates new logger each time
// ✅ Good - singleton for stateless service
collection.AddSingleton(NewLogger) // Reuses same instance
// ✅ Good - transient for stateful objects
collection.AddTransient(NewEmailMessage) // Unique state per message
3. Optimize Scope Usage
Create scopes at the right granularity:
// ❌ Bad - scope for each small operation
for _, item := range items {
scope := provider.CreateScope(ctx) // Overhead
processItem(scope, item)
scope.Close()
}
// ✅ Good - batch operations in scope
scope := provider.CreateScope(ctx)
defer scope.Close()
processor, _ := godi.Resolve[BatchProcessor](scope.ServiceProvider())
processor.ProcessItems(items)
4. Lazy Resolution
Defer expensive service resolution:
type Service struct {
provider godi.ServiceProvider
expensive ExpensiveService // Don't resolve in constructor
once sync.Once
}
func (s *Service) getExpensive() ExpensiveService {
s.once.Do(func() {
s.expensive, _ = godi.Resolve[ExpensiveService](s.provider)
})
return s.expensive
}
func (s *Service) ProcessIfNeeded(condition bool) {
if !condition {
return // Expensive service never created
}
expensive := s.getExpensive()
expensive.Process()
}
Benchmarking
Basic Benchmark
func BenchmarkServiceResolution(b *testing.B) {
collection := godi.NewServiceCollection()
collection.AddSingleton(NewConfig)
collection.AddSingleton(NewLogger)
collection.AddScoped(NewRepository)
collection.AddTransient(NewCommand)
provider, _ := collection.BuildServiceProvider()
defer provider.Close()
b.Run("Singleton", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = godi.Resolve[*Config](provider)
}
})
b.Run("Scoped", func(b *testing.B) {
scope := provider.CreateScope(context.Background())
defer scope.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = godi.Resolve[*Repository](scope.ServiceProvider())
}
})
b.Run("Transient", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = godi.Resolve[*Command](provider)
}
})
}
Concurrent Benchmark
func BenchmarkConcurrentResolution(b *testing.B) {
collection := godi.NewServiceCollection()
collection.AddSingleton(NewThreadSafeService)
provider, _ := collection.BuildServiceProvider()
defer provider.Close()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = godi.Resolve[ThreadSafeService](provider)
}
})
}
Memory Optimization
1. Dispose Scopes Promptly
// ❌ Bad - scope lives too long
func ProcessRequests(provider godi.ServiceProvider, requests []Request) {
scope := provider.CreateScope(context.Background())
defer scope.Close()
for _, req := range requests {
// All request instances accumulate in scope
processRequest(scope, req)
}
}
// ✅ Good - scope per request
func ProcessRequests(provider godi.ServiceProvider, requests []Request) {
for _, req := range requests {
func() {
scope := provider.CreateScope(context.Background())
defer scope.Close()
processRequest(scope, req)
}() // Scope disposed after each request
}
}
2. Avoid Service Leaks
// ❌ Bad - holds references to scoped services
type BadSingleton struct {
cache map[string]ScopedService // Leaks scoped services!
}
// ✅ Good - stores only data
type GoodSingleton struct {
cache map[string]ServiceData // Only data, not services
}
3. Pool Expensive Objects
// Object pool for expensive transients
type PooledService struct {
pool sync.Pool
}
func NewPooledService() *PooledService {
return &PooledService{
pool: sync.Pool{
New: func() interface{} {
return &ExpensiveObject{}
},
},
}
}
func (s *PooledService) GetObject() *ExpensiveObject {
return s.pool.Get().(*ExpensiveObject)
}
func (s *PooledService) ReturnObject(obj *ExpensiveObject) {
obj.Reset() // Clear state
s.pool.Put(obj)
}
Provider Options for Performance
Validation Timing
// Development - validate everything upfront
devOptions := &godi.ServiceProviderOptions{
ValidateOnBuild: true, // Slower startup, catches errors early
}
// Production - defer validation
prodOptions := &godi.ServiceProviderOptions{
ValidateOnBuild: false, // Faster startup
DeferAcyclicVerification: true, // Defer cycle detection
}
Resolution Monitoring
options := &godi.ServiceProviderOptions{
OnServiceResolved: func(serviceType reflect.Type, instance interface{}, duration time.Duration) {
if duration > 100*time.Millisecond {
log.Warnf("Slow resolution: %s took %v", serviceType, duration)
metrics.RecordSlowResolution(serviceType, duration)
}
},
}
Common Performance Pitfalls
1. Excessive Transient Usage
// ❌ Bad - transient for everything
collection.AddTransient(NewLogger) // Wasteful
collection.AddTransient(NewConfig) // Wasteful
collection.AddTransient(NewMetrics) // Wasteful
// ✅ Good - appropriate lifetimes
collection.AddSingleton(NewLogger) // Shared, stateless
collection.AddSingleton(NewConfig) // Shared, immutable
collection.AddSingleton(NewMetrics) // Shared, thread-safe
collection.AddTransient(NewCommand) // Unique state needed
2. Deep Dependency Chains
// ❌ Bad - deep nesting
A depends on B
B depends on C
C depends on D
D depends on E
...
// ✅ Good - flatter structure
A depends on B, C
B depends on D
C depends on D
3. Scope Explosion
// ❌ Bad - nested scopes
scope1 := provider.CreateScope(ctx)
scope2 := scope1.ServiceProvider().CreateScope(ctx)
scope3 := scope2.ServiceProvider().CreateScope(ctx)
// ✅ Good - single scope level
scope := provider.CreateScope(ctx)
Performance Best Practices
1. Profile Before Optimizing
import _ "net/http/pprof"
func main() {
// Enable profiling
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Your application
}
// Profile: go tool pprof http://localhost:6060/debug/pprof/profile
2. Measure Resolution Times
func measureResolution[T any](provider godi.ServiceProvider, name string) (T, error) {
start := time.Now()
service, err := godi.Resolve[T](provider)
duration := time.Since(start)
log.Printf("Resolution of %s took %v", name, duration)
return service, err
}
3. Use Appropriate Lifetimes
Service Type |
Recommended Lifetime |
Reason |
|---|---|---|
Stateless |
Singleton |
No overhead, safe to share |
Database Connection |
Singleton |
Expensive to create |
HTTP Client |
Singleton |
Connection pooling |
Request Context |
Scoped |
Request-specific |
Transaction |
Scoped |
Must not be shared |
Command/Query |
Transient |
Unique parameters |
4. Batch Operations
// ❌ Bad - multiple resolutions
func ProcessItems(provider godi.ServiceProvider, items []Item) {
for _, item := range items {
service, _ := godi.Resolve[ItemService](provider)
service.Process(item)
}
}
// ✅ Good - single resolution
func ProcessItems(provider godi.ServiceProvider, items []Item) {
service, _ := godi.Resolve[ItemService](provider)
service.ProcessBatch(items)
}
Optimization Checklist
Use singletons for stateless services
Cache service resolutions in hot paths
Minimize transient usage
Dispose scopes promptly
Avoid deep dependency chains
Profile before optimizing
Monitor resolution times in production
Use connection pooling for external resources
Batch operations when possible
Avoid holding references to scoped services
Conclusion
godi is designed for performance, but following these guidelines ensures optimal performance:
Choose appropriate lifetimes - Singleton > Scoped > Transient
Cache resolutions - Don’t resolve in loops
Manage scopes carefully - Create at the right granularity
Monitor in production - Track slow resolutions
Profile when needed - Measure before optimizing
With proper usage, godi adds minimal overhead while providing significant architectural benefits.