Service Lifetimes

When should a database connection be shared? When should a request context be unique? Lifetimes answer these questions.

Visual Overview

┌─────────────────────────────────────────────────────────────────┐
│ Application Lifetime                                            │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │                        SINGLETON                            │ │
│ │   Logger, Database Pool, Config, HTTP Client                │ │
│ │   Created once at startup, shared everywhere                │ │
│ └─────────────────────────────────────────────────────────────┘ │
│                                                                 │
│ ┌──────────────┐  ┌──────────────┐  ┌──────────────┐            │
│ │  Request 1   │  │  Request 2   │  │  Request 3   │            │
│ │              │  │              │  │              │            │
│ │   SCOPED     │  │   SCOPED     │  │   SCOPED     │            │
│ │  UserSession │  │  UserSession │  │  UserSession │            │
│ │  Transaction │  │  Transaction │  │  Transaction │            │
│ │              │  │              │  │              │            │
│ │  TRANSIENT   │  │  TRANSIENT   │  │  TRANSIENT   │            │
│ │  new each    │  │  new each    │  │  new each    │            │
│ │  resolution  │  │  resolution  │  │  resolution  │            │
│ └──────────────┘  └──────────────┘  └──────────────┘            │
└─────────────────────────────────────────────────────────────────┘

Singleton

One instance for the entire application.

services.AddSingleton(NewDatabasePool)

// Same instance everywhere
db1 := godi.MustResolve[*DatabasePool](provider)
db2 := godi.MustResolve[*DatabasePool](provider)
// db1 == db2 ✓

When to Use Singleton

  • Database connection pools

  • Configuration objects

  • Loggers

  • HTTP clients

  • Caches

  • Any shared, thread-safe resource

Singleton Lifecycle

┌──────────────────────────────────────────────────────────┐
│  Application Start                                       │
│       │                                                  │
│       ▼                                                  │
│  First Resolution ──▶ Constructor Called ──▶ Cached      │
│       │                                                  │
│       ▼                                                  │
│  Subsequent Resolutions ──▶ Return Cached Instance       │
│       │                                                  │
│       ▼                                                  │
│  provider.Close() ──▶ Dispose (if implements Close())    │
└──────────────────────────────────────────────────────────┘

Scoped

One instance per scope. Different scopes get different instances.

services.AddScoped(NewRequestContext)

// Create a scope
scope, _ := provider.CreateScope(ctx)
defer scope.Close()

// Same within scope
ctx1 := godi.MustResolve[*RequestContext](scope)
ctx2 := godi.MustResolve[*RequestContext](scope)
// ctx1 == ctx2 ✓

// Different scope = different instance
scope2, _ := provider.CreateScope(ctx)
defer scope2.Close()
ctx3 := godi.MustResolve[*RequestContext](scope2)
// ctx1 == ctx3 ✗

When to Use Scoped

  • Request context

  • Database transactions

  • User sessions

  • Per-request caches

  • Unit of work patterns

Scoped Lifecycle

┌──────────────────────────────────────────────────────────┐
│  provider.CreateScope(ctx)                               │
│       │                                                  │
│       ▼                                                  │
│  First Resolution in Scope ──▶ Constructor ──▶ Cached    │
│       │                                                  │
│       ▼                                                  │
│  More Resolutions in Scope ──▶ Return Cached             │
│       │                                                  │
│       ▼                                                  │
│  scope.Close() ──▶ Dispose All Scoped Services           │
└──────────────────────────────────────────────────────────┘

Transient

New instance every single time.

services.AddTransient(NewEmailBuilder)

// Always new
builder1 := godi.MustResolve[*EmailBuilder](provider)
builder2 := godi.MustResolve[*EmailBuilder](provider)
// builder1 == builder2 ✗

When to Use Transient

  • Builders

  • Temporary objects

  • Stateful utilities that shouldn’t be shared

  • Unique instances

Transient Lifecycle

┌──────────────────────────────────────────────────────────┐
│  Each Resolution                                         │
│       │                                                  │
│       ▼                                                  │
│  Constructor Called ──▶ New Instance Returned            │
│       │                                                  │
│       ▼                                                  │
│  scope.Close() ──▶ Dispose (if tracked and disposable)   │
└──────────────────────────────────────────────────────────┘

The Golden Rule

A service can only depend on services with the same or longer lifetime.

Lifetime Order (longest to shortest):
  Singleton > Scoped > Transient

Valid Dependencies

// ✓ Scoped depending on Singleton
services.AddSingleton(NewLogger)
services.AddScoped(func(logger *Logger) *UserService {
    return &UserService{logger: logger}
})

// ✓ Transient depending on Singleton
services.AddSingleton(NewLogger)
services.AddTransient(func(logger *Logger) *TempService {
    return &TempService{logger: logger}
})

// ✓ Transient depending on Scoped
services.AddScoped(NewRequestContext)
services.AddTransient(func(ctx *RequestContext) *Handler {
    return &Handler{ctx: ctx}
})

Invalid Dependencies

// ✗ Singleton depending on Scoped
services.AddScoped(NewRequestContext)
services.AddSingleton(func(ctx *RequestContext) *Cache {
    return &Cache{ctx: ctx}  // Build error!
})
// Why? The singleton lives forever, but the scoped service
// is destroyed when the scope closes. The singleton would
// hold a dangling reference.

// ✗ Singleton depending on Transient
services.AddTransient(NewTempFile)
services.AddSingleton(func(file *TempFile) *Storage {
    return &Storage{file: file}  // Build error!
})

Performance Considerations

Memory Usage

// Singleton: 1 instance total
services.AddSingleton(NewHeavyService) // 100MB
// Total: 100MB

// Scoped: 1 instance per active scope
services.AddScoped(NewHeavyService) // 100MB each
// 10 concurrent requests = 1GB

// Transient: 1 instance per resolution
services.AddTransient(NewHeavyService) // 100MB each
// Can grow unbounded!

Creation Cost

// Singleton: Paid once
services.AddSingleton(NewExpensiveService) // 5 seconds
// Total cost: 5 seconds

// Scoped: Paid per scope
services.AddScoped(NewExpensiveService) // 5 seconds
// Per request cost: 5 seconds

// Transient: Paid every time
services.AddTransient(NewExpensiveService) // 5 seconds
// Every resolution: 5 seconds

Quick Reference

Lifetime

Created

Shared

Disposed

Best For

Singleton

Once

App-wide

provider.Close()

DB pools, config, loggers

Scoped

Per scope

Within scope

scope.Close()

Request context, transactions

Transient

Every time

Never

scope.Close()

Builders, temp objects

Common Patterns

Web Application

// Singletons - shared infrastructure
services.AddSingleton(NewLogger)
services.AddSingleton(NewDatabasePool)
services.AddSingleton(NewRedisClient)
services.AddSingleton(NewHTTPClient)

// Scoped - per-request state
services.AddScoped(NewRequestContext)
services.AddScoped(NewTransaction)
services.AddScoped(NewUserSession)

// Transient - utilities
services.AddTransient(NewQueryBuilder)
services.AddTransient(NewEmailBuilder)

Background Worker

// Singletons - shared
services.AddSingleton(NewJobQueue)
services.AddSingleton(NewMetrics)

// Scoped - per-job
services.AddScoped(NewJobContext)
services.AddScoped(NewJobLogger)

// Transient - utilities
services.AddTransient(NewRetryHandler)

Next: Learn about scopes and request isolation