Using Lifetimes
When should a database connection be shared? When should a request context be unique? Lifetimes answer these questions.
The Three Lifetimes
┌────────────────────────────────────────────────────────┐
│ Application Lifetime │
│ ┌───────────────────────────────────────────────────┐ │
│ │ SINGLETON: Logger, Database, Config │ │
│ │ Created once, shared everywhere │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Request 1 │ │ Request 2 │ │ Request 3 │ │
│ │ │ │ │ │ │ │
│ │ SCOPED: │ │ SCOPED: │ │ SCOPED: │ │
│ │ UserSession │ │ UserSession │ │ UserSession │ │
│ │ Transaction │ │ Transaction │ │ Transaction │ │
│ │ │ │ │ │ │ │
│ │ TRANSIENT: │ │ TRANSIENT: │ │ TRANSIENT: │ │
│ │ new instance │ │ new instance │ │ new instance │ │
│ │ every time │ │ every time │ │ every time │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────────────────────────────────────────┘
Singleton - One Instance Forever
Created once when first requested. Shared by everyone.
services.AddSingleton(NewDatabasePool)
// Same instance everywhere
db1 := godi.MustResolve[*DatabasePool](provider)
db2 := godi.MustResolve[*DatabasePool](provider)
// db1 == db2 ✓
Use for: Database connections, configuration, loggers, HTTP clients, caches
Scoped - One Instance Per Scope
Created once per scope. Different scopes get different instances.
services.AddScoped(NewRequestContext)
// Create a scope (typically per HTTP request)
scope1, _ := provider.CreateScope(ctx)
defer scope1.Close()
// Same within scope
ctx1 := godi.MustResolve[*RequestContext](scope1)
ctx2 := godi.MustResolve[*RequestContext](scope1)
// ctx1 == ctx2 ✓
// Different scope = different instance
scope2, _ := provider.CreateScope(ctx)
defer scope2.Close()
ctx3 := godi.MustResolve[*RequestContext](scope2)
// ctx1 == ctx3 ✗
Use for: Request context, database transactions, user sessions, per-request caches
Transient - New Instance Every Time
Created fresh on every resolution.
services.AddTransient(NewEmailBuilder)
// Always new
builder1 := godi.MustResolve[*EmailBuilder](provider)
builder2 := godi.MustResolve[*EmailBuilder](provider)
// builder1 == builder2 ✗
Use for: Builders, temporary objects, stateful utilities
Complete Example
package main
import (
"context"
"fmt"
"log"
"github.com/junioryono/godi/v4"
)
// Singleton - shared everywhere
type Logger struct {
id int
}
var loggerCount = 0
func NewLogger() *Logger {
loggerCount++
return &Logger{id: loggerCount}
}
// Scoped - one per scope
type RequestID struct {
value int
}
var requestCount = 0
func NewRequestID() *RequestID {
requestCount++
return &RequestID{value: requestCount}
}
// Transient - always new
type TempFile struct {
name string
}
var fileCount = 0
func NewTempFile() *TempFile {
fileCount++
return &TempFile{name: fmt.Sprintf("temp_%d.txt", fileCount)}
}
func main() {
services := godi.NewCollection()
services.AddSingleton(NewLogger)
services.AddScoped(NewRequestID)
services.AddTransient(NewTempFile)
provider, err := services.Build()
if err != nil {
log.Fatal(err)
}
defer provider.Close()
// Simulate two HTTP requests
for i := 1; i <= 2; i++ {
fmt.Printf("\n--- Request %d ---\n", i)
scope, _ := provider.CreateScope(context.Background())
// Singleton: same logger
logger := godi.MustResolve[*Logger](scope)
fmt.Printf("Logger ID: %d\n", logger.id)
// Scoped: same within request
reqID1 := godi.MustResolve[*RequestID](scope)
reqID2 := godi.MustResolve[*RequestID](scope)
fmt.Printf("RequestID (same scope): %d == %d? %v\n",
reqID1.value, reqID2.value, reqID1 == reqID2)
// Transient: different every time
file1 := godi.MustResolve[*TempFile](scope)
file2 := godi.MustResolve[*TempFile](scope)
fmt.Printf("TempFile: %s, %s\n", file1.name, file2.name)
scope.Close()
}
}
Output:
--- Request 1 ---
Logger ID: 1
RequestID (same scope): 1 == 1? true
TempFile: temp_1.txt, temp_2.txt
--- Request 2 ---
Logger ID: 1
RequestID (same scope): 2 == 2? true
TempFile: temp_3.txt, temp_4.txt
The Golden Rule
A service can only depend on services with the same or longer lifetime.
// ✓ OK: Scoped can depend on Singleton
services.AddSingleton(NewLogger)
services.AddScoped(func(logger *Logger) *UserService {
return &UserService{logger: logger}
})
// ✗ ERROR: Singleton cannot depend on Scoped
services.AddScoped(NewRequestContext)
services.AddSingleton(func(ctx *RequestContext) *Cache { // Build error!
return &Cache{ctx: ctx}
})
Why? A singleton lives forever, but scoped services are destroyed when the scope closes. The singleton would hold a reference to something that no longer exists.
Quick Reference
Lifetime |
Created |
Shared |
Destroyed |
Use Case |
|---|---|---|---|---|
Singleton |
Once |
App-wide |
Provider.Close() |
DB pools, config |
Scoped |
Per scope |
Within scope |
Scope.Close() |
Request context, transactions |
Transient |
Every time |
Never |
Scope.Close() |
Builders, temp objects |
Next: Build a web application