Service Lifetimes
Service lifetimes determine when instances are created and how long they live. Understanding lifetimes is crucial for building efficient applications.
The Three Lifetimes
Singleton - Application Lifetime
Created once, shared everywhere throughout the application.
func NewDatabaseConnection(config Config) DatabaseConnection {
// This expensive connection is created only once
conn, _ := sql.Open("postgres", config.DatabaseURL)
return &databaseConnection{conn: conn}
}
services.AddSingleton(NewDatabaseConnection)
// Later in your app
db1 := godi.MustResolve[DatabaseConnection](provider) // Created
db2 := godi.MustResolve[DatabaseConnection](provider) // Same instance
// db1 == db2 (true)
Use Singleton for:
Database connections
Configuration objects
Loggers
Cache instances
HTTP clients
Any shared, thread-safe resource
Scoped - Request Lifetime
Created once per scope, shared within that scope.
func NewRequestContext() *RequestContext {
return &RequestContext{
ID: uuid.New().String(),
StartTime: time.Now(),
}
}
services.AddScoped(NewRequestContext)
// In HTTP handler
func Handler(provider godi.Provider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
scope, _ := provider.CreateScope(r.Context())
defer scope.Close()
ctx1 := godi.MustResolve[*RequestContext](scope) // Created
ctx2 := godi.MustResolve[*RequestContext](scope) // Same instance
// ctx1 == ctx2 (true) - same within scope
// Different scope = different instance
scope2, _ := provider.CreateScope(r.Context())
defer scope2.Close()
ctx3 := godi.MustResolve[*RequestContext](scope2) // New instance
// ctx1 == ctx3 (false)
}
}
Use Scoped for:
HTTP request context
Database transactions
User sessions
Unit of work patterns
Request-specific caches
Transient - Always New
Created fresh every time it’s requested.
func NewTempFileHandler() TempFileHandler {
file, _ := os.CreateTemp("", "temp")
return &tempFileHandler{file: file}
}
services.AddTransient(NewTempFileHandler)
// Each resolution creates a new instance
handler1 := godi.MustResolve[TempFileHandler](provider)
handler2 := godi.MustResolve[TempFileHandler](provider)
// handler1 != handler2 (different instances)
Use Transient for:
Temporary objects
Builders
Unique instances
Stateful helpers
Objects that shouldn’t be shared
Lifetime Rules
The Golden Rule
A service can only depend on services with the same or longer lifetime.
// ✅ Valid: Scoped depending on Singleton
func NewUserService(db Database) UserService { // Database is Singleton
return &userService{db: db}
}
services.AddScoped(NewUserService) // Scoped
services.AddSingleton(NewDatabase) // Singleton
// ❌ Invalid: Singleton depending on Scoped
func NewCache(ctx *RequestContext) Cache { // RequestContext is Scoped
return &cache{context: ctx}
}
services.AddSingleton(NewCache) // Error at build time!
services.AddScoped(NewRequestContext) // Scoped
Practical Examples
Web Application Pattern
// Singleton - shared resources
services.AddSingleton(NewLogger)
services.AddSingleton(NewDatabasePool)
services.AddSingleton(NewRedisCache)
services.AddSingleton(NewEmailClient)
// Scoped - per request
services.AddScoped(NewRequestContext)
services.AddScoped(NewDatabaseTransaction)
services.AddScoped(NewUserSession)
// Transient - always new
services.AddTransient(NewEmailBuilder)
services.AddTransient(NewQueryBuilder)
Background Job Pattern
// Singleton - shared
services.AddSingleton(NewJobQueue)
services.AddSingleton(NewMetricsCollector)
// Scoped - per job execution
services.AddScoped(NewJobContext)
services.AddScoped(NewJobLogger)
// Transient - utilities
services.AddTransient(NewRetryHandler)
Lifetime and Performance
Memory Usage
// Singleton: 1 instance total
services.AddSingleton(NewHeavyService) // 100MB
// Total memory: 100MB
// Scoped: 1 instance per active scope
services.AddScoped(NewHeavyService) // 100MB per request
// 10 concurrent requests = 1GB
// Transient: 1 instance per resolution
services.AddTransient(NewHeavyService) // 100MB per use
// Can grow unbounded!
Creation Cost
// Singleton: Created once at startup
services.AddSingleton(NewExpensiveService) // 5 second setup
// Cost: 5 seconds total
// Scoped: Created per scope
services.AddScoped(NewExpensiveService) // 5 second setup
// Cost: 5 seconds per request!
// Transient: Created every time
services.AddTransient(NewExpensiveService) // 5 second setup
// Cost: 5 seconds per resolution!
Disposal and Cleanup
Services implementing Disposable are cleaned up based on lifetime:
type DatabaseConnection struct {
conn *sql.DB
}
func (d *DatabaseConnection) Close() error {
return d.conn.Close()
}
// Singleton: Closed when provider closes
services.AddSingleton(NewDatabaseConnection)
provider.Close() // Disposes all singletons
// Scoped: Closed when scope closes
services.AddScoped(NewTransaction)
scope.Close() // Disposes all scoped services
// Transient: Closed when scope closes (if tracked)
services.AddTransient(NewTempFile)
scope.Close() // Disposes transients created in this scope
Common Mistakes
1. Wrong Lifetime for Database Connections
// ❌ Don't make connections transient
services.AddTransient(NewDatabaseConnection)
// Creates new connection every time - connection pool exhaustion!
// ✅ Use singleton for connection pools
services.AddSingleton(NewDatabasePool)
2. Caching Request Data in Singletons
// ❌ Don't store request data in singletons
type Cache struct {
userID string // Wrong! Shared across all requests
}
services.AddSingleton(NewCache)
// ✅ Use scoped for request-specific data
type RequestCache struct {
userID string // Correct - isolated per request
}
services.AddScoped(NewRequestCache)
3. Not Considering Thread Safety
// ❌ Mutable singleton without synchronization
type Counter struct {
count int // Race condition!
}
services.AddSingleton(NewCounter)
// ✅ Thread-safe singleton
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
Best Practices
Start with Singleton for shared, thread-safe services
Use Scoped for request/operation-specific state
Reserve Transient for stateless utilities or unique instances
Consider memory impact when choosing lifetimes
Validate at build time - godi will catch lifetime violations
Implement Disposable for resources needing cleanup
Next Steps
Learn about Service Registration
Understand Dependency Resolution
Explore Scopes & Isolation