Understanding Dependency Injection Lifetimes: Singleton, Scoped, and Transient
Published: (December 3, 2025 at 05:55 AM EST)
4 min read
Source: Dev.to
Source: Dev.to
Singleton
Key Characteristics
- One instance for the entire application lifetime
- Shared across all requests
- Created once when first requested
- Disposed when the application shuts down
When to Use
- Stateless services (no instance‑specific data)
- Expensive‑to‑create services (caches, HTTP clients)
- Shared resources (configuration, logging, caching)
// Register as singleton
services.AddSingleton();
services.AddSingleton();
Example: Cache Service
public class CacheService : ICacheService
{
private readonly Dictionary _cache = new();
private readonly object _lock = new();
public void Set(string key, T value)
{
lock (_lock) { _cache[key] = value; }
}
public T Get(string key)
{
lock (_lock)
{
return _cache.TryGetValue(key, out var value) ? (T)value : default;
}
}
}
Watch Out For
- Shared mutable state
- Thread‑safety issues (ensure proper synchronization)
- Implement
IDisposablewhen holding unmanaged resources
// BAD: Singleton with mutable state
public class BadService
{
public string CurrentUserId { get; set; } // Shared across all requests!
}
Scoped
Key Characteristics
- One instance per scope (typically per HTTP request)
- Shared within the same scope, different across scopes
- Created when the scope begins
- Disposed when the scope ends
When to Use
- Database contexts (e.g., Entity Framework
DbContext) - Request‑specific services that need state during a request
- Unit‑of‑Work patterns
// Register as scoped
services.AddScoped();
services.AddScoped();
Example: Database Context and Order Service
public class ApplicationDbContext : DbContext
{
public DbSet Orders { get; set; }
}
public class OrderService
{
private readonly ApplicationDbContext _context;
public OrderService(ApplicationDbContext context) => _context = context;
public async Task CreateOrder(Order order)
{
_context.Orders.Add(order);
await _context.SaveChangesAsync();
return order;
}
}
Watch Out For
- Injecting scoped services into singletons (causes lifetime mismatches)
// BAD: Singleton depending on scoped service
public class BadSingleton
{
private readonly IScopedService _scoped; // Error!
}
// GOOD: Resolve scoped service via IServiceProvider
public class GoodSingleton
{
private readonly IServiceProvider _serviceProvider;
public GoodSingleton(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
public void DoWork()
{
using var scope = _serviceProvider.CreateScope();
var scoped = scope.ServiceProvider.GetRequiredService();
scoped.DoWork();
}
}
- Do not capture scoped services in background tasks without creating a proper scope.
Transient
Key Characteristics
- New instance every time it’s requested
- No sharing between consumers
- Created on demand, disposed when out of scope (garbage collected)
- Shortest lifetime of the three options
When to Use
- Lightweight services that are cheap to create
- Stateless operations
- Services that must not be shared (sharing would cause bugs)
// Register as transient
services.AddTransient();
services.AddTransient, OrderValidator>();
Example: Order Validator
public class OrderValidator : IValidator
{
public ValidationResult Validate(Order order)
{
var result = new ValidationResult();
if (order.Items == null || order.Items.Count == 0)
result.AddError("Order must have at least one item");
return result;
}
}
Watch Out For
- Using transients for expensive‑to‑create services (performance impact)
- Holding static references to transient instances (memory leaks)
- Unnecessary creation when a singleton would suffice
Comparison Table
| Aspect | Singleton | Scoped | Transient |
|---|---|---|---|
| Instance Count | One for entire app | One per scope | New every time |
| Lifetime | Application lifetime | Scope lifetime (per request) | Shortest – disposed when out of scope |
| Memory Usage | Low (shared) | Medium | High (many instances) |
| Thread Safety | Must be thread‑safe | Usually not required | Usually not required |
| Disposal | When app shuts down | When scope ends | When garbage collected |
| Typical Use | Stateless, expensive services | Request‑specific services | Lightweight, stateless services |
Service Registration Example
public void ConfigureServices(IServiceCollection services)
{
// Singleton: Shared across all requests
services.AddSingleton();
// Scoped: One per HTTP request
services.AddScoped();
// Transient: New instance each time
services.AddTransient();
}
Background Worker Example (Scoped Service in a Hosted Service)
public class BackgroundWorker : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
public BackgroundWorker(IServiceProvider serviceProvider) =>
_serviceProvider = serviceProvider;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Create a scope for each iteration
using var scope = _serviceProvider.CreateScope();
var scopedService = scope.ServiceProvider.GetRequiredService();
await scopedService.DoWorkAsync();
await Task.Delay(1000, stoppingToken);
}
}
}
Lifetime Dependency Rules
- A service can only depend on services with equal or longer lifetimes.
| Service Lifetime | Allowed Dependencies |
|---|---|
| Singleton | Singleton |
| Scoped | Singleton, Scoped |
| Transient | Singleton, Scoped, Transient |
Common Mistake
// BAD: Singleton depending on scoped service
public class SingletonService
{
private readonly IScopedService _scoped; // Error!
}
// GOOD: Resolve scoped service via IServiceProvider when needed
public class SingletonService
{
private readonly IServiceProvider _serviceProvider;
public SingletonService(IServiceProvider serviceProvider) =>
_serviceProvider = serviceProvider;
public void DoWork()
{
using var scope = _serviceProvider.CreateScope();
var scoped = scope.ServiceProvider.GetRequiredService();
// Use scoped service...
}
}