DEV Community

Cover image for Understanding Dependency Injection Lifetimes: Singleton, Scoped, and Transient
Outdated Dev
Outdated Dev

Posted on

Understanding Dependency Injection Lifetimes: Singleton, Scoped, and Transient

Hello there!πŸ‘‹πŸ§”β€β™‚οΈ Today, we're exploring the three main service lifetimes: singleton, scoped, and transient.

Understanding dependency injection (DI) lifecycle management is crucial for building reliable applications. Get it wrong, and you might end up with memory leaks or shared state bugs. Get it right, and your code will be cleaner and easier to maintain!

Overview

When you register a service in a dependency injection container, you need to specify its *lifetime, how long the container should keep an instance alive. The three main lifetimes are:

  1. Singleton - One instance for the entire application lifetime
  2. Scoped - One instance per scope (typically per HTTP request in web apps)
  3. Transient - A new instance every time it's requested

1. Singleton

What is Singleton?

Singleton is like having one shared tool that everyone uses. There's only one instance, and it lives for the entire lifetime of your application.

Key Characteristics:

  • One instance for the entire application
  • Shared across all requests
  • Created once when first requested
  • Disposed when the application shuts down

When to Use Singleton

Use singleton for:

  • βœ… Stateless services - Services that don't hold instance-specific data
  • βœ… Expensive to create - Services costly to initialize (caches, HTTP clients)
  • βœ… Shared resources - Configuration, logging, caching

Example

// Register as singleton
services.AddSingleton<ICacheService, CacheService>();
services.AddSingleton<ILogger, Logger>();

// Example: Cache service
public class CacheService : ICacheService
{
    private readonly Dictionary<string, object> _cache = new();
    private readonly object _lock = new object();

    public void Set<T>(string key, T value)
    {
        lock (_lock) { _cache[key] = value; }
    }

    public T Get<T>(string key)
    {
        lock (_lock)
        {
            return _cache.TryGetValue(key, out var value) ? (T)value : default(T);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls

⚠️ Watch out for:

  1. Shared State Issues
   // BAD: Singleton with mutable state
   public class BadService
   {
       public string CurrentUserId { get; set; } // Shared across all requests!
   }
Enter fullscreen mode Exit fullscreen mode
  1. Thread Safety - Ensure thread safety if shared state is needed
  2. Disposal - Implement IDisposable if holding resources

2. Scoped

What is Scoped?

Scoped is like having a personal workspace for each task. In web applications, each HTTP request gets its own scope, and all scoped services share the same instance within that scope.

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 Scoped

Use scoped for:

  • βœ… Database contexts - Entity Framework DbContext
  • βœ… Request-specific services - Services that need state during a request
  • βœ… Unit of Work patterns - Services coordinating multiple operations

Example

// Register as scoped
services.AddScoped<IDbContext, ApplicationDbContext>();
services.AddScoped<IOrderService, OrderService>();

// Example: Database context
public class ApplicationDbContext : DbContext
{
    public DbSet<Order> Orders { get; set; }
}

public class OrderService
{
    private readonly ApplicationDbContext _context;

    public OrderService(ApplicationDbContext context) => 
        _context = context; // Same instance throughout the request

    public async Task<Order> CreateOrder(Order order)
    {
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
        return order;
    }
}
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls

⚠️ Watch out for:

  1. Using Scoped Services in Singleton
   // BAD: Singleton depending on scoped service
   public class BadSingleton
   {
       private readonly IScopedService _scoped; // Error!
   }

   // GOOD: Use 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<IScopedService>();
               scoped.DoWork();
           }
       }
   }
Enter fullscreen mode Exit fullscreen mode
  1. Capturing Scoped Services - Don't capture scoped services in background tasks without proper scope management

3. Transient

What is Transient?

Transient is like getting a fresh tool every time you need one. Each time you request a transient service, you get a brand new instance. Important: Transient instances are NOT kept alive for the app lifetime, they're created on-demand and disposed when they go out of scope (garbage collected).

Key Characteristics:

  • New instance every time it's requested
  • No sharing between consumers
  • Created on demand
  • Disposed when out of scope (garbage collected, NOT kept for app lifetime)
  • Shortest lifetime of all three options

When to Use Transient

Use transient for:

  • βœ… Lightweight services - Services that are cheap to create
  • βœ… Stateless operations - Services that don't maintain state
  • βœ… Services that shouldn't be shared - When sharing would cause issues

Example

// Register as transient
services.AddTransient<IEmailService, EmailService>();
services.AddTransient<IValidator<Order>, OrderValidator>();

// Example: Validator service
public class OrderValidator : IValidator<Order>
{
    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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls

⚠️ Watch out for:

  1. Performance Issues - Don't use transient for expensive-to-create services
  2. Memory Leaks - Don't hold static references in transient services
  3. Unnecessary Creation - Don't use transient when singleton would work

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 created)
Thread Safety Must be thread-safe Usually not needed Usually not needed
Disposal When app shuts down When scope ends When garbage collected
Use Case Stateless, expensive Request-specific Lightweight, stateless

Common Scenarios

Web Application

public void ConfigureServices(IServiceCollection services)
{
    // Singleton: Shared across all requests
    services.AddSingleton<ICacheService, CacheService>();

    // Scoped: One per HTTP request
    services.AddScoped<IDbContext, ApplicationDbContext>();

    // Transient: New instance each time
    services.AddTransient<IEmailService, EmailService>();
}
Enter fullscreen mode Exit fullscreen mode

Background 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<IScopedService>();
                await scopedService.DoWorkAsync();
            }

            await Task.Delay(1000, stoppingToken);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Dependency Rules

A service can only depend on services with equal or longer lifetimes.

  • βœ… Singleton can depend on: Singleton
  • βœ… Scoped can depend on: Singleton, Scoped
  • βœ… Transient can depend on: Singleton, Scoped, Transient

Common Mistake:

// BAD: Singleton depending on scoped service
public class SingletonService
{
    private readonly IScopedService _scoped; // Error!
}

// GOOD: Use IServiceProvider to create scope 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<IScopedService>();
            scoped.DoWork();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Choose the Right Lifetime

  • Singleton: Stateless, expensive, shared resources
  • Scoped: Request-specific, database contexts
  • Transient: Lightweight, stateless, fresh instance needed

2. Follow Dependency Rules

  • Never have shorter lifetimes depend on longer ones
  • Use IServiceProvider when you need to break the rules
  • Understand the implications of each choice

3. Handle Disposal Properly

// Implement IDisposable for services that hold resources
public class ResourceService : IDisposable
{
    private bool _disposed = false;

    public void Dispose()
    {
        if (_disposed) return;

        // Clean up resources
        _disposed = true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Understanding dependency injection lifetimes is crucial for building robust applications:

Singleton - One instance for the entire application:

  • Use for stateless, expensive-to-create services
  • Ensure thread safety
  • Great for caching, logging, configuration

Scoped - One instance per scope (request):

  • Use for request-specific services
  • Perfect for database contexts
  • Automatically managed in ASP.NET Core

Transient - New instance every time:

  • Use for lightweight, stateless services
  • When you need a fresh instance each time
  • NOT kept alive for app lifetime - instances are created and disposed quickly
  • Be mindful of performance implications (many instances created)

The Golden Rules:

  • Choose the shortest lifetime that meets your needs
  • Follow dependency lifetime rules (shorter can't depend on longer)
  • Implement IDisposable for resource cleanup
  • Test and monitor your choices

Remember, there's no one-size-fits-all answer. The right choice depends on your specific use case and performance requirements. Start with the most restrictive lifetime that works, and adjust as needed.

You've got this! πŸ’ͺ Happy coding!

Additional Resources

Top comments (0)