Loading content...
You now understand the three lifetime strategies individually: transient (new each time), scoped (one per scope), and singleton (one forever). The remaining challenge is selection—given a service, how do you determine the correct lifetime?
This isn't always obvious. Make it too short-lived, and you waste resources recreating instances needlessly. Make it too long-lived, and you introduce threading bugs, stale data, or captive dependencies. The right choice depends on analyzing the service's characteristics: its state, its thread safety, its dependencies, and its role in the application.
This page provides a systematic framework for making these decisions. By the end, you'll be able to examine any service and confidently select its lifetime—not by guessing, but by applying clear principles.
By the end of this page, you will have a reliable decision process for lifetime selection. You'll understand the key questions to ask, the common patterns that emerge, and how to handle edge cases. You'll also see complete real-world service analyses that demonstrate the framework in action.
When determining a service's lifetime, ask these questions in order. Each question narrows the options until the correct lifetime becomes clear.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
START: What lifetime should this service have? ┌─────────────────────────────────────────────────────────────────────────────┐│ Q1: Does the service hold PER-REQUEST state? ││ (User context, request ID, validation errors, operation tracking) │├─────────────────────────────────────────────────────────────────────────────┤│ YES → Use SCOPED (or TRANSIENT if needed fresh per injection point) ││ NO → Continue to Q2 │└─────────────────────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────────────────┐│ Q2: Is the service expensive to create? ││ (Database connections, remote config loading, cache warming) │├─────────────────────────────────────────────────────────────────────────────┤│ YES → Prefer SCOPED or SINGLETON (to amortize creation cost) ││ Continue to Q3 to decide between them ││ NO → TRANSIENT is safe, but continue to Q3 for optimization opportunity │└─────────────────────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────────────────┐│ Q3: Is the service THREAD-SAFE? ││ (Stateless, immutable state, or synchronized mutable state) │├─────────────────────────────────────────────────────────────────────────────┤│ YES → SINGLETON is an option. Consider Q4. ││ NO → Cannot be SINGLETON. Use SCOPED or TRANSIENT. │└─────────────────────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────────────────┐│ Q4: Should all consumers share the SAME state/data? ││ (Application-wide cache, global configuration, shared metrics) │├─────────────────────────────────────────────────────────────────────────────┤│ YES → Use SINGLETON ││ NO → Continue to Q5 │└─────────────────────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────────────────┐│ Q5: Should consumers within ONE REQUEST share the same instance? ││ (DbContext, unit of work, request transaction) │├─────────────────────────────────────────────────────────────────────────────┤│ YES → Use SCOPED ││ NO → Use TRANSIENT │└─────────────────────────────────────────────────────────────────────────────┘If you're uncertain after the decision tree, default to SCOPED for database-touching services and TRANSIENT for everything else. This is conservative—you might create more instances than strictly necessary, but you won't introduce threading bugs or captive dependencies. Optimize to singleton only when you've verified thread safety.
Let's examine each decision question in depth, with examples of how to answer it for real services.
Per-request state is data that varies from one request to another and should not leak across requests. Examples:
If the service stores any of this, it must be scoped or transient. Singleton would cause cross-request contamination.
Expensive creation includes:
If creation is expensive, you want to minimize how often it happens. Singleton creates once for the application. Scoped creates once per request. Transient creates every time—expensive.
Thread safety means the service can be used by multiple threads simultaneously without corruption. A service is thread-safe if:
If not thread-safe, singleton is dangerous—concurrent requests will corrupt internal state.
Some services are valuable precisely because they're shared globally:
If sharing is the point, use singleton.
This is about intra-request sharing:
If services within a request should see the same instance, use scoped. If each injection point should get its own, use transient.
For common service types, the correct lifetime is well-established. Use this table as a quick reference:
| Service Type | Recommended | Rationale |
|---|---|---|
| Database Context (DbContext, Session) | Scoped | Transaction coherence, change tracking, not thread-safe |
| Repository | Scoped | Usually wraps DbContext, shares its lifetime |
| Application Service | Scoped | Orchestrates per-request operations |
| User/Request Context | Scoped | Per-request identity, must not leak |
| Validator | Scoped/Transient | Transient if stateless, scoped if collecting errors |
| Mapper (AutoMapper) | Singleton | Stateless, expensive to initialize |
| Configuration Service | Singleton | Read once, immutable thereafter |
| In-Memory Cache | Singleton | Shared across all requests, thread-safe collections |
| Logger/LoggerFactory | Singleton | Stateless, thread-safe by design |
| HttpClient/IHttpClientFactory | Singleton | Connection management, pooling |
| Feature Flag Service | Singleton | Shared flags, refreshed periodically |
| Metrics Collector | Singleton | Aggregates from all requests |
| Email Sender | Transient/Singleton | Transient if simple, singleton with pooling |
| File Processor | Transient | Each operation gets fresh instance |
| Document Generator | Transient | Per-operation state |
Your specific implementation might differ. An EmailSender that maintains a connection pool is better as singleton. A EmailSender that just constructs SMTP messages is fine as transient. Always apply the decision framework to your specific implementation.
A service's lifetime is constrained by its dependencies. The captive dependency rule states:
A service cannot safely depend on services with shorter lifetimes.
This is because the longer-lived service captures a reference to the shorter-lived one, keeping it alive beyond its intended scope.
| Service Lifetime | Can Safely Depend On | CANNOT Depend On |
|---|---|---|
| Singleton | Singleton only | Scoped (captive!), Transient (captive!) |
| Scoped | Scoped, Singleton | Transient* (wasteful, not dangerous) |
| Transient | Transient, Scoped, Singleton | (all valid) |
*Scoped depending on transient is technically safe but wasteful—the transient is created each time the scoped service is injected into some consumer, defeating the "once per scope" optimization.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
// Problem: Singleton depends on Scopedpublic class GlobalService // Singleton{ private readonly ApplicationDbContext _db; // Scoped - CAPTIVE! public GlobalService(ApplicationDbContext db) { _db = db; }} // Solution 1: Make the service scoped tooservices.AddScoped<GlobalService>(); // Now safe // Solution 2: Use scope factory (when singleton is actually needed)public class GlobalService // Singleton{ private readonly IServiceScopeFactory _scopeFactory; public GlobalService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public async Task DoSomethingAsync() { await using var scope = _scopeFactory.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); // Use db within this scope only }} // Solution 3: Redesign - remove the dependencypublic class GlobalService // Singleton{ // No DbContext dependency - receives data through method parameters public OrderSummary CalculateSummary(IEnumerable<Order> orders) { return new OrderSummary(orders); }} // Caller (scoped) provides the datapublic class OrderService // Scoped{ private readonly ApplicationDbContext _db; private readonly GlobalService _globalService; // Safe - scoped can depend on singleton public async Task<OrderSummary> GetSummaryAsync() { var orders = await _db.Orders.ToListAsync(); return _globalService.CalculateSummary(orders); }} // Enable validation to catch these at startup!builder.Host.UseDefaultServiceProvider(options =>{ options.ValidateScopes = true; options.ValidateOnBuild = true; // Catches captive dependencies!});Let's apply the decision framework to real services, walking through the reasoning for each lifetime choice.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
// ═══════════════════════════════════════════════════════════════// SERVICE 1: ProductCatalogService// ═══════════════════════════════════════════════════════════════public class ProductCatalogService{ private readonly ApplicationDbContext _db; public ProductCatalogService(ApplicationDbContext db) { _db = db; } public async Task<List<Product>> SearchAsync(string query) { return await _db.Products .Where(p => p.Name.Contains(query)) .ToListAsync(); }} // Analysis:// Q1: Per-request state? NO - just performs queries// Q2: Expensive to create? NO - just stores DbContext reference// Q3: Thread-safe? IRRELEVANT - depends on scoped DbContext// Q4: Share globally? NO// Q5: Share within request? YES - should use same DbContext as other services// → SCOPED (follows its DbContext dependency)services.AddScoped<IProductCatalogService, ProductCatalogService>(); // ═══════════════════════════════════════════════════════════════// SERVICE 2: PricingEngine// ═══════════════════════════════════════════════════════════════public class PricingEngine{ private readonly ImmutableDictionary<string, decimal> _discountRules; public PricingEngine(IConfiguration config) { // Loads discount rules at construction (takes 500ms) var rules = LoadDiscountRulesFromConfig(config); _discountRules = rules.ToImmutableDictionary(); } public decimal CalculatePrice(Product product, string customerTier) { var basePrice = product.Price; if (_discountRules.TryGetValue(customerTier, out var discount)) return basePrice * (1 - discount); return basePrice; }} // Analysis:// Q1: Per-request state? NO - rules are application-global// Q2: Expensive to create? YES - 500ms initialization// Q3: Thread-safe? YES - immutable state only, pure function// Q4: Share globally? YES - same rules for all requests// → SINGLETONservices.AddSingleton<IPricingEngine, PricingEngine>(); // ═══════════════════════════════════════════════════════════════// SERVICE 3: ShoppingCartService// ═══════════════════════════════════════════════════════════════public class ShoppingCartService{ private readonly ApplicationDbContext _db; private readonly IUserContext _userContext; public ShoppingCartService(ApplicationDbContext db, IUserContext userContext) { _db = db; _userContext = userContext; } public async Task AddToCartAsync(int productId, int quantity) { var userId = _userContext.CurrentUserId; var cart = await _db.Carts .Include(c => c.Items) .FirstOrDefaultAsync(c => c.UserId == userId); cart.Items.Add(new CartItem(productId, quantity)); await _db.SaveChangesAsync(); }} // Analysis:// Q1: Per-request state? NOT IN SERVICE - but depends on IUserContext which is per-request// Q2: Expensive? NO// Q3: Thread-safe? N/A - follows dependencies// Q4: Share globally? NO - user-specific operations// Q5: Share within request? YES - should share DbContext// → SCOPEDservices.AddScoped<IShoppingCartService, ShoppingCartService>(); // ═══════════════════════════════════════════════════════════════// SERVICE 4: InvoiceGenerator// ═══════════════════════════════════════════════════════════════public class InvoiceGenerator{ private readonly PdfDocument _document; // Non-thread-safe library private readonly List<LineItem> _items = new(); public InvoiceGenerator() { _document = new PdfDocument(); } public void AddItem(string description, decimal amount) { _items.Add(new LineItem(description, amount)); } public byte[] GeneratePdf() { // Writes all _items to _document and returns PDF bytes }} // Analysis:// Q1: Per-request state? YES - accumulates items for ONE invoice// Q2: Expensive? NO// Q3: Thread-safe? NO - PdfDocument and List are not thread-safe// Decision: TRANSIENT - must be fresh for each invoice generationservices.AddTransient<IInvoiceGenerator, InvoiceGenerator>(); // ═══════════════════════════════════════════════════════════════// SERVICE 5: RateLimiter// ═══════════════════════════════════════════════════════════════public class RateLimiter{ private readonly ConcurrentDictionary<string, SlidingWindow> _windows = new(); private readonly int _maxRequests; private readonly TimeSpan _windowSize; public RateLimiter(IConfiguration config) { _maxRequests = config.GetValue<int>("RateLimit:MaxRequests"); _windowSize = config.GetValue<TimeSpan>("RateLimit:WindowSize"); } public bool IsAllowed(string clientId) { var window = _windows.GetOrAdd(clientId, _ => new SlidingWindow(_windowSize)); return window.TryAcquire(); }} // Analysis:// Q1: Per-request state? NO - tracks ALL clients across ALL requests// Q2: Expensive? NO// Q3: Thread-safe? YES - uses ConcurrentDictionary// Q4: Share globally? YES - must track requests globally to rate limit// → SINGLETONservices.AddSingleton<IRateLimiter, RateLimiter>();12345678910111213141516171819202122232425262728293031323334
// 1. Enable scope validation in developmentif (builder.Environment.IsDevelopment()){ builder.Host.UseDefaultServiceProvider(options => { options.ValidateScopes = true; // Catches captive dependencies options.ValidateOnBuild = true; // Validates entire DI graph at startup });} // 2. Use code analyzers that flag DI issues// Install: dotnet add package Microsoft.Extensions.DependencyInjection.Analyzers // 3. Add unit tests for lifetime correctness[Fact]public void AllSingletons_ShouldNotDependOnScopedServices(){ var builder = WebApplication.CreateBuilder(); ConfigureServices(builder.Services); using var app = builder.Build(); // Trigger full DI graph construction - will throw if captive dependencies app.Services.GetRequiredService<IServiceProviderIsService>(); // No exception = graph is valid} // 4. Review checklist before registering any service:// □ What state does this service hold?// □ Does that state vary per-request?// □ Is the service thread-safe?// □ What are its dependencies' lifetimes?// □ Does a longer-lived service depend on it?Different architectural layers have characteristic lifetime patterns. Use these as starting points for your decisions:
| Layer | Typical Components | Typical Lifetime |
|---|---|---|
| Presentation | Controllers, Razor Pages, API Controllers | Transient/Scoped (framework-managed) |
| Application Services | Use case orchestrators, command handlers | Scoped |
| Domain Services | Domain logic, business rules | Scoped (often uses repositories) |
| Infrastructure - Data | Repositories, DbContext, UoW | Scoped |
| Infrastructure - External | API clients, message publishers | Singleton (with thread-safe design) |
| Infrastructure - Cross-cutting | Logging, caching, configuration | Singleton |
| Infrastructure - Per-Operation | File handlers, document generators | Transient |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
public static class ServiceCollectionExtensions{ public static IServiceCollection AddApplicationLayer(this IServiceCollection services) { // Application services: Scoped (orchestrate per-request work) services.AddScoped<IOrderApplicationService, OrderApplicationService>(); services.AddScoped<ICustomerApplicationService, CustomerApplicationService>(); services.AddScoped<IInventoryApplicationService, InventoryApplicationService>(); return services; } public static IServiceCollection AddDomainLayer(this IServiceCollection services) { // Domain services: Scoped (often need data access) services.AddScoped<IPricingDomainService, PricingDomainService>(); services.AddScoped<IInventoryAllocationService, InventoryAllocationService>(); return services; } public static IServiceCollection AddInfrastructure(this IServiceCollection services) { // Data access: Scoped (follows DbContext) services.AddScoped<IOrderRepository, OrderRepository>(); services.AddScoped<ICustomerRepository, CustomerRepository>(); services.AddScoped<ApplicationDbContext>(); // External APIs: Singleton (connection pooling, thread-safe) services.AddSingleton<IPaymentGateway, StripePaymentGateway>(); services.AddSingleton<IShippingService, FedExShippingService>(); // Cross-cutting: Singleton (stateless or thread-safe) services.AddSingleton<ICacheService, RedisCacheService>(); services.AddSingleton<IEventPublisher, RabbitMqEventPublisher>(); // Per-operation: Transient (fresh state each time) services.AddTransient<IPdfGenerator, PdfSharpGenerator>(); services.AddTransient<IExcelExporter, EPPlusExporter>(); return services; }} // Usage in Program.csbuilder.Services .AddApplicationLayer() .AddDomainLayer() .AddInfrastructure();Selecting the correct lifetime is a systematic process, not guesswork. By analyzing state characteristics, thread safety, creation costs, and dependency relationships, you can confidently choose the right lifetime for any service.
Module Complete:
You now have comprehensive mastery of dependency lifetime management. You understand each lifetime deeply, you know when to apply each, and you can systematically make correct decisions. This skill directly translates to more reliable, efficient, and maintainable applications.
Congratulations! You have mastered dependency lifetime management. You can now confidently select transient, scoped, or singleton lifetime for any service, understanding the implications of each choice. This knowledge is essential for building robust, scalable applications with proper dependency injection.