Shubham Kumar Nayak
All writing

ASP.NET Core DI Lifetimes: When to Use Transient, Scoped, and Singleton

May 12, 2026

.NETAPI DesignArchitecturePerformanceReliabilityWeb

A practical guide to ASP.NET Core Dependency Injection lifetimes, with simple examples, lifetime rules, Mermaid diagrams, and the common captive dependency mistake freshers should understand early.

Dependency Injection is one of the first things we use in ASP.NET Core, but it is also one of the first things that creates hidden bugs when we do not understand service lifetimes properly.

Most developers start with this:

builder.Services.AddTransient<IEmailService, EmailService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();

At first, this looks like a registration choice.

But in real applications, this is an architecture decision.

The lifetime you choose decides how long an object lives, who shares it, when it gets disposed, and whether it is safe to use across multiple requests.

This article explains Dependency Injection lifetimes in a simple way, especially for starters, with real examples and one very important rule:

A service with a longer lifetime should not directly depend on a service with a shorter lifetime.

This is the rule behind many confusing DI bugs in ASP.NET Core.

The real problem

Imagine we are building a normal e-commerce Web API.

We may have an order endpoint like this:

OrderController
  -> OrderService
      -> AppDbContext
      -> PriceCalculator
      -> EmailService
      -> ProductCache

All these classes are services, but they do not have the same behavior.

Some are simple and stateless. Some are tied to one HTTP request. Some should be shared across the full application.

If we register everything without thinking, the application may still compile and run. But later we may see problems like:

  • database context reused incorrectly
  • disposed object errors
  • stale data
  • wrong user data leaking between requests
  • unnecessary object creation
  • memory pressure
  • thread safety issues
  • hard-to-debug bugs in background jobs

So DI lifetime is not just about performance.

It is about ownership.

Who owns this object?

How long should it live?

Can it safely be shared?

Does it depend on request-specific data?

Those questions matter more than simply choosing the lifetime that looks fastest.

The three lifetimes in simple words

ASP.NET Core has three common service lifetimes:

Transient  -> created every time it is requested
Scoped     -> created once per scope, usually once per HTTP request
Singleton  -> created once for the whole application lifetime

A simple mental model is:

Transient = new object whenever needed
Scoped    = one object for one request
Singleton = one object for the whole app

Lifetime diagram

Mermaid diagram

The live article renders this source as a diagram.

flowchart TD
    A[Application starts] --> B[Singleton instance created once]

    A --> C[HTTP Request 1]
    A --> D[HTTP Request 2]

    C --> C1[Scoped instance for Request 1]
    C --> C2[Transient instance A]
    C --> C3[Transient instance B]

    D --> D1[Scoped instance for Request 2]
    D --> D2[Transient instance C]
    D --> D3[Transient instance D]

    B --> X[Shared across all requests]
    C1 --> Y[Shared only within Request 1]
    D1 --> Z[Shared only within Request 2]

This diagram is the simplest way to understand lifetimes.

A Singleton is shared across the application.

A Scoped service is shared only within one request.

A Transient service is created every time it is requested.

Transient lifetime

A Transient service is created every time it is requested from DI.

Registration:

builder.Services.AddTransient<IPriceCalculator, PriceCalculator>();

Use Transient for lightweight and stateless services.

A service is stateless when it does not store request data or user-specific information inside fields.

Good examples of Transient services:

  • price calculator
  • tax calculator
  • email body builder
  • validator
  • mapper
  • formatter
  • small utility service
  • simple strategy class

Example:

public interface IPriceCalculator
{
    decimal CalculateFinalPrice(decimal price, decimal taxPercent);
}

public class PriceCalculator : IPriceCalculator
{
    public decimal CalculateFinalPrice(decimal price, decimal taxPercent)
    {
        var taxAmount = price * taxPercent / 100;
        return price + taxAmount;
    }
}

Registration:

builder.Services.AddTransient<IPriceCalculator, PriceCalculator>();

Usage:

public class OrderService
{
    private readonly IPriceCalculator _priceCalculator;

    public OrderService(IPriceCalculator priceCalculator)
    {
        _priceCalculator = priceCalculator;
    }

    public decimal CalculateOrderTotal(decimal price)
    {
        return _priceCalculator.CalculateFinalPrice(price, 18);
    }
}

This is a good Transient service because it is simple and does not hold state.

It receives input, calculates output, and finishes.

When Transient is a good choice

Use Transient when:

  • the service is lightweight
  • the service is stateless
  • the service is cheap to create
  • each use can safely get a new object
  • the service does not manage shared resources
  • the service does not need to maintain consistency during one request

For starters, the easiest question is:

If I create this class again and again, will anything break?

If the answer is no, Transient may be fine.

When Transient can be a bad choice

Transient is not always safe.

It can be a bad choice when the service is expensive to create or holds resources that should be reused carefully.

For example, do not create heavy clients manually in a Transient service without understanding their lifecycle.

Also, if a Transient service stores temporary data in fields and you expect that data to remain available later, your design may break because a new instance may be created next time.

Bad mental model:

public class CurrentOrderState
{
    public int OrderId { get; set; }
}

If this is Transient and multiple services receive different instances, one service may set OrderId, but another service may not see it.

If state must be shared during a request, Scoped is usually better.

Scoped lifetime

A Scoped service is created once per scope.

In ASP.NET Core Web API, one HTTP request usually creates one scope.

Registration:

builder.Services.AddScoped<IOrderService, OrderService>();

Use Scoped for services that belong to one request.

Good examples of Scoped services:

  • DbContext
  • repository
  • unit of work
  • business service that uses database
  • current user service
  • request context service
  • service that should share state inside one HTTP request

Example with Entity Framework Core:

builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddScoped<IOrderService, OrderService>();
public interface IOrderService
{
    Task<int> CreateOrderAsync(CreateOrderRequest request);
}

public class OrderService : IOrderService
{
    private readonly AppDbContext _dbContext;
    private readonly IPriceCalculator _priceCalculator;

    public OrderService(
        AppDbContext dbContext,
        IPriceCalculator priceCalculator)
    {
        _dbContext = dbContext;
        _priceCalculator = priceCalculator;
    }

    public async Task<int> CreateOrderAsync(CreateOrderRequest request)
    {
        var total = _priceCalculator.CalculateFinalPrice(
            request.Price,
            request.TaxPercent);

        var order = new Order
        {
            ProductId = request.ProductId,
            Quantity = request.Quantity,
            TotalAmount = total
        };

        _dbContext.Orders.Add(order);
        await _dbContext.SaveChangesAsync();

        return order.Id;
    }
}

This design is natural:

OrderService is Scoped
AppDbContext is Scoped
PriceCalculator is Transient

The request gets one OrderService and one AppDbContext for that request.

The calculator can be Transient because it is stateless.

Why DbContext is usually Scoped

A database context usually represents a unit of work.

In a Web API, a common unit of work is one HTTP request.

During one request, you may load an entity, change it, add another entity, and call SaveChangesAsync.

You want those operations to happen in a consistent request boundary.

That is why DbContext is usually Scoped.

It should not be Singleton because it is not designed to be shared across all users and all requests.

It should not usually be Transient inside a normal request because different services may accidentally get different contexts when they should participate in the same unit of work.

When Scoped is a good choice

Use Scoped when:

  • the service uses DbContext
  • the service represents business logic for one request
  • the service uses current user information
  • the service holds request-specific data
  • the service should behave consistently within one request
  • multiple services in the same request should share the same instance

For normal ASP.NET Core Web API business services, Scoped is often the safest default.

Not always, but often.

When Scoped can be a bad choice

Scoped can be a bad choice if the service is completely stateless and very small.

In that case, Transient may be simpler.

Scoped can also create confusion in background workers because background services do not automatically behave like HTTP requests.

For example, a background service is usually Singleton. If it needs a Scoped service like AppDbContext, it should create a scope manually using IServiceScopeFactory.

We will cover this later.

Singleton lifetime

A Singleton service is created once and shared for the whole application lifetime.

Registration:

builder.Services.AddSingleton<ICacheService, MemoryCacheService>();

Use Singleton for application-wide services that are safe to share.

Good examples of Singleton services:

  • memory cache wrapper
  • configuration provider
  • feature flag reader
  • application-level lookup data provider
  • thread-safe shared service
  • expensive object that is safe to reuse

Example:

public interface IProductCache
{
    void Set(string key, ProductSummary value);
    ProductSummary? Get(string key);
}

public class ProductCache : IProductCache
{
    private readonly ConcurrentDictionary<string, ProductSummary> _cache = new();

    public void Set(string key, ProductSummary value)
    {
        _cache[key] = value;
    }

    public ProductSummary? Get(string key)
    {
        return _cache.TryGetValue(key, out var value)
            ? value
            : null;
    }
}

Registration:

builder.Services.AddSingleton<IProductCache, ProductCache>();

This can be Singleton because the cache is shared across the application and the dictionary is thread-safe.

But this is important:

A Singleton is used by many requests at the same time.

So if it stores mutable state, that state must be thread-safe.

When Singleton is a good choice

Use Singleton when:

  • the service is application-wide
  • the service is expensive to create and safe to reuse
  • the service does not store user-specific request data
  • the service is thread-safe
  • the service does not directly depend on Scoped services

For starters, ask this:

Can this same object safely be used by every user and every request?

If the answer is yes, Singleton may be a good fit.

If the answer is no, avoid Singleton.

When Singleton can be dangerous

Singleton is dangerous when it stores data that belongs to a user or request.

Bad example:

public class CurrentUserStore
{
    public int UserId { get; set; }
}

If this is Singleton, every request shares the same UserId.

That means User A may set the value, and User B may read the same value.

That is a serious bug.

Request-specific data should not be stored in a Singleton.

Use Scoped for request-specific data.

The most important rule: lifetime direction

The most important DI lifetime rule is:

A longer-living service should not directly depend on a shorter-living service.

Why?

Because the longer-living service may capture the shorter-living service and keep it alive longer than intended.

This is called a captive dependency.

Lifetime dependency graph

Mermaid diagram

The live article renders this source as a diagram.

flowchart BT
    T[Transient\nshortest lifetime]
    S[Scoped\nper request]
    SG[Singleton\nlongest lifetime]

    T --> S
    S --> SG

    SG -. should not depend on .-> S
    SG -. be careful with .-> T
    S --> T

    classDef good fill:#e8f5e9,stroke:#2e7d32,color:#111;
    classDef warn fill:#fff8e1,stroke:#f9a825,color:#111;
    classDef bad fill:#ffebee,stroke:#c62828,color:#111;

    class T,S,SG good;

Another way to remember it:

Shorter lifetime can depend on longer lifetime.
Longer lifetime should not depend on shorter lifetime.

A Scoped service can depend on a Singleton.

A Transient service can depend on a Scoped service when it is created inside a request scope.

But a Singleton should not directly depend on a Scoped service.

Bad example: Singleton depending on DbContext

This is one of the most common mistakes.

builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddSingleton<IReportCache, ReportCache>();
public class ReportCache : IReportCache
{
    private readonly AppDbContext _dbContext;

    public ReportCache(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task RefreshAsync()
    {
        var reports = await _dbContext.Reports.ToListAsync();
        // cache reports
    }
}

This is wrong.

ReportCache is Singleton. It lives for the whole application lifetime.

AppDbContext is Scoped. It is supposed to live for one request or one scope.

The Singleton is trying to hold a Scoped dependency.

That means the AppDbContext may be used outside its intended scope.

This can cause:

  • disposed object errors
  • stale data
  • threading issues
  • unexpected behavior across requests
  • incorrect unit-of-work boundaries
  • bugs that appear only under load

ASP.NET Core may catch this during validation in some cases, but the architectural rule is more important than the error message.

A Singleton should not capture a Scoped service.

Simple analogy for starters

Think of lifetimes like this:

Singleton = building manager
Scoped    = meeting notebook
Transient = sticky note

The building manager lives for years.

A meeting notebook is only for one meeting.

A sticky note is created whenever needed.

Now imagine the building manager takes one meeting notebook and uses it forever for every future meeting.

That is wrong because the notebook belonged to one meeting.

This is what happens when a Singleton captures a Scoped service.

The Singleton lives too long, and the Scoped object was not designed for that.

How to fix Singleton needing Scoped data

Sometimes a Singleton needs to load data from the database.

That does not mean we should inject AppDbContext directly into the Singleton.

Instead, create a scope when needed.

Example using IServiceScopeFactory:

public class ReportRefreshService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public ReportRefreshService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public async Task RefreshAsync()
    {
        using var scope = _scopeFactory.CreateScope();

        var dbContext = scope.ServiceProvider
            .GetRequiredService<AppDbContext>();

        var reports = await dbContext.Reports.ToListAsync();

        // update singleton cache using the loaded reports
    }
}

Now the Singleton does not hold the AppDbContext forever.

It creates a temporary scope, uses the Scoped service inside that scope, and then disposes the scope.

This is common in background workers, cache refresh services, and scheduled jobs.

Alternative: use a factory

Another option is to use a factory pattern.

For database work, IDbContextFactory<TContext> can be useful in scenarios where a normal request scope is not the right boundary.

The idea is the same:

Do not hold a Scoped dependency forever.
Create it when needed, use it, then release it.

The exact implementation depends on the project.

The architectural rule stays the same.

Practical dependency rules

Here is a practical table:

Consumer serviceDependency serviceUsually okay?Notes
TransientTransientYesNew objects as needed.
TransientScopedUsually yes inside a requestBe careful if resolved from root provider.
TransientSingletonYesCommon and safe if Singleton is thread-safe.
ScopedTransientYesCommon for helpers and calculators.
ScopedScopedYesCommon for business services and DbContext.
ScopedSingletonYesCommon for cache/config providers.
SingletonSingletonYesSafe if thread-safe.
SingletonScopedNoCaptive dependency risk.
SingletonTransientBe carefulThe Transient may effectively become Singleton if captured.

The most important row is:

Singleton -> Scoped = avoid

Example architecture for a Web API

Let us design a simple order API.

Mermaid diagram

The live article renders this source as a diagram.

flowchart TD
    A[OrderController\nScoped by request pipeline] --> B[OrderService\nScoped]
    B --> C[AppDbContext\nScoped]
    B --> D[PriceCalculator\nTransient]
    B --> E[EmailTemplateBuilder\nTransient]
    B --> F[ProductCache\nSingleton]
    B --> G[PaymentClient\nHttpClientFactory]

A practical registration can look like this:

var builder = WebApplication.CreateBuilder(args);

// Database
builder.Services.AddDbContext<AppDbContext>();

// Business services
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<ICustomerService, CustomerService>();

// Stateless helpers
builder.Services.AddTransient<IPriceCalculator, PriceCalculator>();
builder.Services.AddTransient<IEmailTemplateBuilder, EmailTemplateBuilder>();

// Shared application services
builder.Services.AddSingleton<IProductCache, ProductCache>();

// External API client
builder.Services.AddHttpClient<IPaymentClient, PaymentClient>();

builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();
app.Run();

This is a reasonable starting point.

OrderService is Scoped because it works with request-level business logic and database operations.

AppDbContext is Scoped because it belongs to a unit of work.

PriceCalculator is Transient because it is stateless.

ProductCache is Singleton because it is shared application-level data and should be thread-safe.

PaymentClient is created through HttpClientFactory, which helps avoid common HTTP client lifetime problems.

Example: current user service should be Scoped

A current user service is usually request-specific.

public interface ICurrentUserService
{
    int? UserId { get; }
}

public class CurrentUserService : ICurrentUserService
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public CurrentUserService(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public int? UserId
    {
        get
        {
            var value = _httpContextAccessor.HttpContext?
                .User?
                .FindFirst("user_id")?
                .Value;

            return int.TryParse(value, out var userId)
                ? userId
                : null;
        }
    }
}

Registration:

builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();

Why Scoped?

Because the current user belongs to the current request.

It should not be shared across the whole application.

Do not put request-specific user data in a Singleton.

Example: email sending service

An email sender may look like a Transient service.

public interface IEmailSender
{
    Task SendAsync(string to, string subject, string body);
}

public class EmailSender : IEmailSender
{
    public Task SendAsync(string to, string subject, string body)
    {
        // send email
        return Task.CompletedTask;
    }
}

Registration:

builder.Services.AddTransient<IEmailSender, EmailSender>();

This is fine if the service is stateless and uses properly managed dependencies.

But if the email sender holds expensive connections, queues, background workers, or shared state, the design may need to change.

The lifetime depends on behavior, not only the name of the class.

Example: cache service as Singleton

A cache wrapper can be Singleton if it is designed for shared access.

public interface IAppCache
{
    void Set<T>(string key, T value);
    T? Get<T>(string key);
}

public class AppCache : IAppCache
{
    private readonly IMemoryCache _memoryCache;

    public AppCache(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    public void Set<T>(string key, T value)
    {
        _memoryCache.Set(key, value, TimeSpan.FromMinutes(10));
    }

    public T? Get<T>(string key)
    {
        return _memoryCache.TryGetValue(key, out T? value)
            ? value
            : default;
    }
}

Registration:

builder.Services.AddMemoryCache();
builder.Services.AddSingleton<IAppCache, AppCache>();

This is reasonable because cache is application-level.

But be careful about what you store.

Do not accidentally cache user-specific data with a shared key.

Bad key:

cart

Better key:

cart:{userId}

A Singleton cache is shared, so cache keys and data boundaries matter.

Example: background service using Scoped services

A background service is usually Singleton.

But it often needs Scoped services like AppDbContext or business services.

Do not inject the Scoped service directly.

Bad:

public class OrderCleanupWorker : BackgroundService
{
    private readonly AppDbContext _dbContext;

    public OrderCleanupWorker(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // wrong lifetime boundary
    }
}

Better:

public class OrderCleanupWorker : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public OrderCleanupWorker(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = _scopeFactory.CreateScope();

            var dbContext = scope.ServiceProvider
                .GetRequiredService<AppDbContext>();

            var oldOrders = dbContext.Orders
                .Where(x => x.CreatedAt < DateTime.UtcNow.AddDays(-30));

            dbContext.Orders.RemoveRange(oldOrders);
            await dbContext.SaveChangesAsync(stoppingToken);

            await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
        }
    }
}

Registration:

builder.Services.AddHostedService<OrderCleanupWorker>();

The background service lives long, but the database context is created inside a short-lived scope.

That is the correct boundary.

Common mistakes

Mistake 1: Registering everything as Singleton

Some developers think Singleton is faster because it creates only one instance.

That is not a good reason.

Singleton is only safe when the object is safe to share across all requests.

Wrong thinking:

Singleton creates fewer objects, so it must be better.

Better thinking:

Singleton shares one object across the app, so it must be safe for that responsibility.

Mistake 2: Registering DbContext as Singleton

Do not register DbContext as Singleton in a normal Web API.

It is request and unit-of-work oriented.

Use the normal AddDbContext registration unless you have a specific reason and understand the trade-off.

Mistake 3: Storing request data in Singleton

Do not store current user, current tenant, current request ID, or request-specific values in a Singleton field.

Those values belong to a request.

Use Scoped services or pass the values as method parameters.

Mistake 4: Singleton directly depending on Scoped

This is the captive dependency problem.

Bad:

Singleton service -> Scoped DbContext

Better:

Singleton service -> IServiceScopeFactory -> create scope -> resolve DbContext -> use -> dispose

Mistake 5: Thinking lifetime is only about memory

Lifetime is not only about memory.

It is also about correctness, isolation, consistency, disposal, and concurrency.

Practical decision checklist

When registering a service, ask these questions.

1. Does this service store user or request-specific data?

Use Scoped.

Example:

CurrentUserService -> Scoped
TenantContext -> Scoped
RequestCorrelationService -> Scoped

2. Does this service use DbContext?

Usually use Scoped.

Example:

OrderService -> Scoped
CustomerRepository -> Scoped
UnitOfWork -> Scoped

3. Is this service stateless and lightweight?

Use Transient.

Example:

PriceCalculator -> Transient
EmailTemplateBuilder -> Transient
Validator -> Transient

4. Is this service shared across the entire application?

Use Singleton only if it is safe and thread-safe.

Example:

ProductCache -> Singleton
FeatureFlagProvider -> Singleton
ApplicationLookupProvider -> Singleton

5. Does a Singleton need database data?

Do not inject DbContext directly.

Use a scope or a factory.

6. Am I unsure?

For normal Web API business services, Scoped is often a safer starting point.

Then adjust when the behavior is clear.

Practical architecture summary

Here is a simple architecture direction for most ASP.NET Core Web APIs:

Controllers                 -> framework-created per request
Business services            -> Scoped
DbContext / repositories      -> Scoped
Request/user context services -> Scoped
Stateless helpers             -> Transient
Validators/calculators        -> Transient
Application cache/config      -> Singleton
Background workers            -> Singleton, but create scopes when needed
External API clients          -> use HttpClientFactory

This is not a universal law, but it is a good starting point.

The final decision should always come from the service behavior.

Honest trade-offs

Transient is simple and safe for stateless services, but it can create unnecessary objects if used for expensive services.

Scoped is usually the best fit for Web API business logic, but it should not be captured by Singletons.

Singleton is efficient and useful for shared state, but it requires careful design because it is shared across requests and threads.

A wrong Singleton can be more dangerous than a few extra Transient objects.

Performance matters, but correctness comes first.

The goal is not to minimize object creation at any cost.

The goal is to make object lifetime match the real responsibility of the service.

Clear takeaway

Dependency Injection lifetimes are not just syntax.

They describe how your application owns and shares objects.

Remember this:

Transient = create whenever needed
Scoped    = create once per request
Singleton = create once for the whole app

And remember the most important rule:

A longer-living service should not directly depend on a shorter-living service.

Especially avoid this:

Singleton -> Scoped

That usually means the Singleton is capturing something that was designed to live only for one request.

Good DI design keeps your ASP.NET Core application clean, testable, predictable, and easier to debug.

Choose the lifetime based on behavior, not habit.