Service Lifetimes Reference
Service lifetimes control when instances are created and how long they live. Understanding lifetimes is crucial for building efficient and correct applications.
Overview
godi supports three service lifetimes:
Lifetime |
Instance Creation |
Instance Sharing |
Disposal |
|---|---|---|---|
Singleton |
Once per application |
Shared globally |
When provider closes |
Scoped |
Once per scope |
Shared within scope |
When scope closes |
Transient |
Every resolution |
Never shared |
When containing scope closes |
Singleton Services
Singleton services are created once and shared throughout the application lifetime.
Characteristics
One instance for the entire application
Created on first request (lazy initialization)
Thread-safe instance sharing
Disposed when the root provider is closed
Cannot depend on scoped services
When to Use
Stateless services: Loggers, configuration, metrics collectors
Expensive resources: Database connections, HTTP clients
Shared state: Caches, connection pools
Application-wide services: Background workers, schedulers
Example
// Good singleton examples
collection.AddSingleton(NewLogger) // Stateless
collection.AddSingleton(NewConfiguration) // Immutable
collection.AddSingleton(NewDatabasePool) // Shared resource
collection.AddSingleton(NewMetricsCollector) // Thread-safe
// Bad singleton examples
collection.AddSingleton(NewHttpContext) // ❌ Request-specific
collection.AddSingleton(NewTransaction) // ❌ Should be scoped
Implementation Details
type Cache struct {
mu sync.RWMutex
items map[string]interface{}
}
func NewCache() *Cache {
return &Cache{
items: make(map[string]interface{}),
}
}
// Thread-safe methods required for singletons
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.items[key]
return val, ok
}
func (c *Cache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = value
}
Scoped Services
Scoped services are created once per scope and shared within that scope.
Characteristics
One instance per scope
Created when first requested in a scope
Shared by all services within the same scope
Disposed when the scope is closed
Can depend on singleton or other scoped services
When to Use
Request-specific services: HTTP context, request ID, user context
Unit of work patterns: Database transactions, batch operations
Temporary state: Request caches, operation context
Resource isolation: Per-request database connections
Example
// Good scoped examples
collection.AddScoped(NewRequestContext) // HTTP request context
collection.AddScoped(NewUnitOfWork) // Database transaction
collection.AddScoped(NewUserContext) // Authenticated user
collection.AddScoped(NewRequestLogger) // Request-scoped logger
// Web request handling
func HandleRequest(provider godi.ServiceProvider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Create scope for this request
scope := provider.CreateScope(r.Context())
defer scope.Close()
// All services share the same UnitOfWork in this scope
service, _ := godi.Resolve[*OrderService](scope.ServiceProvider())
service.CreateOrder(order) // Uses scoped UnitOfWork
}
}
Scope Hierarchy
// Root scope (provider level)
provider, _ := collection.BuildServiceProvider()
// Request scope
requestScope := provider.CreateScope(ctx)
defer requestScope.Close()
// Nested scope (e.g., for batch processing)
batchScope := requestScope.ServiceProvider().CreateScope(ctx)
defer batchScope.Close()
Transient Services
Transient services are created every time they’re requested.
Characteristics
New instance every time
Never cached or shared
Lightweight creation expected
Disposed with containing scope
Can depend on any lifetime
When to Use
Stateful operations: Commands, queries, operations
Unique state: Email messages, notifications
Factory pattern: When each usage needs configuration
Mutable objects: When sharing would cause issues
Example
// Good transient examples
collection.AddTransient(NewEmailMessage) // Unique per send
collection.AddTransient(NewCommand) // Stateful operation
collection.AddTransient(NewGuid) // Unique value
collection.AddTransient(NewStopwatch) // Timing operations
// Usage
emailService, _ := godi.Resolve[EmailService](provider)
for _, user := range users {
// Each call creates a new EmailMessage
emailService.SendWelcome(user.Email)
}
Factory Pattern
Transient services in godi use a factory pattern internally:
// When you register a transient
collection.AddTransient(NewEmailMessage)
// godi creates a factory function
// func() EmailMessage
// Each resolution calls the factory
msg1, _ := godi.Resolve[EmailMessage](provider) // New instance
msg2, _ := godi.Resolve[EmailMessage](provider) // Different instance
Lifetime Compatibility
Dependency Rules
Singleton can depend on:
✅ Other singletons
❌ Scoped services (causes captive dependency)
❌ Transient services (holds reference forever)
Scoped can depend on:
✅ Singletons
✅ Other scoped services
✅ Transient services
Transient can depend on:
✅ Singletons
✅ Scoped services
✅ Other transient services
Captive Dependencies
A captive dependency occurs when a service with a longer lifetime holds a reference to a service with a shorter lifetime:
// ❌ BAD: Singleton holding scoped
type SingletonService struct {
scopedDb ScopedDatabase // Will capture first scope's instance!
}
// ✅ GOOD: Use a factory or service provider
type SingletonService struct {
provider godi.ServiceProvider
}
func (s *SingletonService) DoWork(ctx context.Context) {
scope := s.provider.CreateScope(ctx)
defer scope.Close()
db, _ := godi.Resolve[ScopedDatabase](scope.ServiceProvider())
// Use db within scope
}
Disposal Order
Services are disposed in reverse order of creation (LIFO):
scope := provider.CreateScope(ctx)
// Creation order:
// 1. Logger (singleton - not disposed with scope)
// 2. Database (scoped)
// 3. Repository (scoped)
// 4. Service (scoped)
scope.Close()
// Disposal order:
// 1. Service
// 2. Repository
// 3. Database
// (Logger remains - disposed with provider)
Best Practices
Choose the Right Lifetime
// Stateless, thread-safe → Singleton
collection.AddSingleton(NewLogger)
collection.AddSingleton(NewConfiguration)
// Request/operation specific → Scoped
collection.AddScoped(NewDbContext)
collection.AddScoped(NewRequestContext)
// Unique state, lightweight → Transient
collection.AddTransient(NewCommand)
collection.AddTransient(NewNotification)
Avoid Common Pitfalls
Don’t capture scoped in singleton
// ❌ Wrong func NewCache(db Database) *Cache { return &Cache{db: db} // If db is scoped, this is wrong } // ✅ Correct func NewCache(provider ServiceProvider) *Cache { return &Cache{provider: provider} }
Don’t make heavy objects transient
// ❌ Wrong - expensive to create collection.AddTransient(NewDatabaseConnection) // ✅ Correct - reuse connection collection.AddSingleton(NewDatabaseConnection)
Don’t share mutable transients
// ❌ Wrong - transients aren't shared collection.AddTransient(NewSharedState) // ✅ Correct - use scoped for sharing collection.AddScoped(NewSharedState)
Testing Considerations
Different lifetimes require different testing approaches:
// Singleton - mock once
collection.AddSingleton(func() Logger {
return &MockLogger{}
})
// Scoped - mock per test scope
func TestWithScope(t *testing.T) {
provider, _ := collection.BuildServiceProvider()
scope := provider.CreateScope(context.Background())
defer scope.Close()
// Test with scoped mocks
}
// Transient - verify multiple calls
mockService := &MockService{}
collection.AddTransient(func() Service {
mockService.callCount++
return mockService
})
Performance Implications
Lifetime |
Creation Cost |
Memory Usage |
Caching |
|---|---|---|---|
Singleton |
Once (low) |
Constant |
Yes |
Scoped |
Per scope (medium) |
Per scope |
Per scope |
Transient |
Per request (high) |
Per request |
No |
Optimization Tips
Use singleton for expensive resources
Use scoped for request-bound state
Use transient only for lightweight objects
Consider pooling for transient-like behavior with reuse
Summary
Singleton: Application-wide, shared, thread-safe services
Scoped: Per-operation services with shared state within scope
Transient: Unique instances with independent state
Choose lifetimes based on:
State requirements
Resource cost
Sharing needs
Thread safety
Disposal requirements