Choosing the Right Service Lifetime
The choice of which lifetime to use depends on factors like resource efficiency, isolation requirements, and concurrency considerations in your specific application scenario. Different lifetimes provide different trade-offs, and it’s essential to choose the one that aligns with your application’s requirements and performance constraints. Intr Ques
Transient
Use Transient services for lightweight, short-lived components and operations that do not maintain state between method calls and are not thread safe
Because each dependent class receives its own instance, methods on the instance are safe to mutate internal state without fear of access by other consumers and threads. Ref
Note
Transient = Any time this service is resolved is the equivalent of assigning your variable
new TService
builder.Services.AddTransient<ITaxCalculator, TaxCalculator>();Good Use Cases
- Services with no internal / request-specific state
- Stateless operations like formatters, mappers, or request builders, which are cheap to construct
- Doesn’t need to be shared (hence thread safe)
- a class that has a bunch of methods that perform some business logic, but those methods don’t set and class properties or anything on your transient class, they just take an input and return an output, for example if you have a service which read or write data from file
- Short-lived operations that don’t need disposal tracking
Scoped
Use Scoped services for components that require per-request/ scope state or resources, ensuring that each HTTP request receives its own instance.
An Instance of the scoped service lives for the length of the scope from which it is resolved. Scoped services resolved from scoped container, this container is created when request being received by the framework, as well it’s being disposed once request handling is completed, so this services would be unique per request.
Because the container resolves a new instance of the type per request, it is not generally required to be thread safe. All framework components such as middleware and MVC controllers get the same instance of a scoped service when handling a particular request. Components within the request life cycle are invoked in sequence, so the shared instance is not used concurrently.
Note
Scoped will cache the first initialisation of it for that “scope” (http request in most cases).
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();Good Use Cases
- Services that track or depend on request-specific data (e.g. current user, correlation ID)
- Caching layers tied to the request
- Scoped services are useful if multiple consumers may require the same dependency during a request.
- An excellent example of such a case is when using Entity Framework Core. By default, the DbContext is registered with the scoped lifetime. Change tracking, therefore, applies across the entire request. Multiple components can make changes to the shared DbContext.
Singleton
Use Singleton services for stateless components and resources that can be shared across the entire application.
flowchart TD A["Application"] --> B["Singleton"] B --> C["Request A"] B --> D["Request B"] B --> E["Request C"] B --> F["Request D"] G["ONE shared instance"] --> B
Because the same instance of a Singleton service can be used by multiple requests concurrently, It must consider thread safety. Any mutable state without suitable locking mechanism could lead to unexpected behaviour. Ref
The instance will remain reachable for the lifetime of the container, so it does not require disposal or garbage collection. Implication of this is Ref
- It is possible to create memory leaks is the instance holds large amount of memory
- This can be especially problematic if memory usage can grow during the instance’s lifetime, given that it will never be released for garbage collection.
- If a service has high memory requirements but is used very infrequently, then the singleton lifetime may not be the most appropriate choice.
Note
Singleton will cache only one instance ever for the lifetime of the application
builder.Services.AddSingleton<IConfigurationCache,
ConfigurationCache>();Good Singleton Example
An immutable application configuration object:
public class CurrencyConfiguration
{
public IReadOnlyDictionary<string, decimal> Rates { get; }
public CurrencyConfiguration(
IReadOnlyDictionary<string, decimal> rates)
{
Rates = rates;
}
}Registration:
builder.Services.AddSingleton<CurrencyConfiguration>();Things to Watch Out For:
- Don’t inject Scoped services into Singletons - it will crash at runtime
- Be cautious with internal state - if it’s mutable and accessed concurrently, you need to handle thread safety
- Avoid heavy dependencies or disposable resources unless you’re handling cleanup properly
Good Use Cases
- Data is shared, doesn’t change
- Object is thread-safe
- Every request can safely use the same instance
- Singleton are very well suited to functional-style services, where the methods take an input and return an output, with no shared state being used
- Configuration providers / objects (that you are binding from you settings file)
- Caching services / memory cache, where the state must be shared for the cache to function.
- Logging, time services, and single-source utilities
- Background workers and hosted services
- Working with some particular SDK, and you know it’s thread safe
Singleton and Thread Safety
A singleton is potentially accessed by many requests simultaneously.
flowchart TD A["Singleton Cache"] --> B["Request A"] A --> C["Request B"] A --> D["Request C"] A --> E["Request D"] F["Concurrent access"] --> A
Therefore:
Singleton Rule
A Singleton must be thread-safe.
Do not put ordinary mutable collections into a singleton and assume they are safe for concurrent access.
For example, consider:
private Dictionary<string, string> _cache = new();If multiple requests modify it concurrently, synchronization/thread-safe collections may be required.
Possible option:
private readonly ConcurrentDictionary<string, string> _cache = new();Avoiding Captive Dependencies
When registering dependencies, it is crucial to ensure that the chosen lifetime is appropriate considering any dependencies that the service has of its own. This is necessary to avoid something called captive dependencies, which is where a service may live longer than is intended. Ref
Thumb rule
A service should not depend on a service with a lifetime shorter than its own.
For example, a service registered with the singleton lifetime should not depend on a transient service. Doing so would result in the transient service being captured by the singleton service, with the instance unintentionally being referenced for the life of the application.
This can lead to problematic and sometime hard to track down runtime bugs and behaviours, such as accidentally sharing non-thread safe services between threads or allowing objects to live past their intended lifetime.

Correct Dependency Direction
To visualize this, let us consider which lifetimes can safely depend on services using another lifetime.
flowchart TD A["Singleton"] --> B["Singleton"] C["Scoped"] --> A C --> D["Scoped"] E["Transient"] --> A E --> D
- Since it will be a short-lived service, a transient service can safely depend on service that have transient, scoped or singleton lifetime
- Scope services are little tricky. If they depend on a transient service, then a single instance of that transient service will live for the life of the scope across the whole request. You may or may not want this behaviour.
- To be absolutely safe, you might choose not to depend on transient services from scoped services, but it is safe for a scoped service to depend on other scoped or singleton services.
- A singleton service is most restrictive in terms of its dependencies, and it should not depend on transient or scoped services, but can depend upon other singleton services.
- Capture of scope services by singletons is one of the more dangerous possibilities. Because scoped services could be disposed of when the scope ends, It is possible that the singleton may try and access them after their disposal. This could lead to runtime exceptions in production, a really bad situation.
Note
A scoped service can depend on a singleton if the singleton is thread-safe and doesn’t depend on scoped state.
Why DbContext Is Scoped
EF Core’s DbContext is normally registered as scoped.
builder.Services.AddDbContext<AppDbContext>();Think of the request as a unit of work:
flowchart TD A["HTTP Request"] --> B["Scoped DbContext"] B --> C["Read Order"] B --> D["Update Customer"] B --> E["Create Payment"] B --> F["SaveChangesAsync"] F --> G["Database"]
The same DbContext can track changes during the request.
Why NOT Singleton?
DbContext:
- is not thread-safe
- maintains change-tracking state
- represents a unit of work
- should not be shared concurrently across requests
Bad:
Request A ─┐
├── Singleton DbContext ❌
Request B ─┘Good:
Request A → DbContext A
Request B → DbContext B
Request C → DbContext CInterview Rule
Never make
DbContexta Singleton.The normal ASP.NET Core registration is Scoped.
Singleton Across ECS Tasks
This is a production-level AWS question.
Suppose your ASP.NET Core application runs on ECS/Fargate:
flowchart TD A["ALB"] --> B["ECS Task 1"] A --> C["ECS Task 2"] A --> D["ECS Task 3"] B --> E["Singleton A"] C --> F["Singleton B"] D --> G["Singleton C"]
Important:
Singleton ≠ Distributed Singleton
A .NET Singleton exists inside a particular application process/container.
If you have three ECS tasks, you effectively have three Singleton instances.
Therefore this is not a distributed cache:
ECS Task 1 → Singleton Cache A
ECS Task 2 → Singleton Cache B
ECS Task 3 → Singleton Cache CFor shared state:
flowchart LR A["ECS Task 1"] --> D["Redis / ElastiCache"] B["ECS Task 2"] --> D C["ECS Task 3"] --> D
Interview keyword
Process-local · Distributed state · ElastiCache · Redis
Singleton in AWS Lambda
Be careful with this interview question.
Lambda execution environments can be reused:
flowchart TD A["Lambda Environment A"] --> B["Invocation 1"] A --> C["Invocation 2"] A --> D["Invocation 3"] E["Lambda Environment B"] --> F["Invocation 4"] E --> G["Invocation 5"]
A Singleton may therefore survive between invocations within the same execution environment.
But:
Lambda Environment A
→ Singleton A
Lambda Environment B
→ Singleton B
Lambda Environment C
→ Singleton CSo:
Attention
A Singleton is not global across all Lambda invocations.
Never use an in-memory singleton as distributed state.
Real-World Decision Matrix
| Scenario | Recommended | Why |
|---|---|---|
| Stateless calculator | Transient | Cheap and stateless |
| DTO mapper | Transient | Lightweight |
| Validator | Usually Transient | Typically stateless |
DbContext | Scoped | Unit of work/request state |
| Repository using DbContext | Scoped | Shares request DbContext |
| Business service using DbContext | Scoped | Request-level operation |
| Request-specific state | Scoped | Isolated per request |
| Immutable configuration | Singleton | Shared and safe |
| Thread-safe application cache | Singleton | Shared within process |
| Redis client/connection abstraction | Usually Singleton/managed | Expensive shared resource |
| Mutable request data | Scoped | Must not leak between requests |
Don't memorize the table blindly
Ask:
- Does it contain request-specific state? → Scoped
- Is it stateless and cheap? → Transient
- Should it be shared across requests? → Consider Singleton
- Is Singleton state mutable? → Check thread safety
- Does it depend on Scoped state? → Lifetime mismatch
Production ASP.NET Core Example
A realistic Order API might look like:
flowchart TD A["HTTP Request"] --> B["OrderController"] B --> C["OrderService<br/>Scoped"] C --> D["OrderRepository<br/>Scoped"] D --> E["AppDbContext<br/>Scoped"] C --> F["PaymentClient"] F --> G["HttpClient"] C --> H["TaxCalculator<br/>Transient"] C --> I["ApplicationCache<br/>Singleton"] E --> J["PostgreSQL / SQL Server"] F --> K["Payment Service"]
Registration:
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddTransient<ITaxCalculator, TaxCalculator>();
builder.Services.AddSingleton<IApplicationCache, ApplicationCache>();
builder.Services.AddHttpClient<IPaymentClient, PaymentClient>();Why?
| Dependency | Lifetime | Reason |
|---|---|---|
OrderService | Scoped | Request-level business operation |
OrderRepository | Scoped | Works with request’s DbContext |
DbContext | Scoped | Unit of work/change tracking |
TaxCalculator | Transient | Stateless/lightweight |
ApplicationCache | Singleton | Shared application-wide state |
HttpClient | Managed by IHttpClientFactory | Connection/resource management |
References
- https://www.linkedin.com/posts/sagar-saini-a36590243_dotnet-dotnetcore-aspnetcore-activity-7408711635235725312-K_Ti/
- https://codewithmukesh.com/blog/when-to-use-transient-scoped-singleton-dotnet/
- https://youtu.be/v6Nr7Zman_Y?si=RtoiEgExnjm8ldZc
- https://stackoverflow.com/questions/38138100/addtransient-addscoped-and-addsingleton-services-differences
- https://stackoverflow.com/a/76634885/6465925
TODO
-
https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection/overview#the-concept
-
https://www.linkedin.com/in/sagar-saini-a36590243/recent-activity/all/
-
Service Collection
-
Service Provider
-
Build Service Provider
Q. AddSingleton(), AddScoped(), AddTransient() are these extension methods ? if yes, of which class ?
Yes. AddSingleton(), AddScoped(), and AddTransient() are extension methods.
They are extension methods on IServiceCollection, defined in the Microsoft.Extensions.DependencyInjection namespace.
Example
builder.Services.AddScoped<IOrderService, OrderService>();Here:
builder.Services
│
▼
IServiceCollection
│
├── AddScoped()
├── AddSingleton()
└── AddTransient()The methods are provided by the ServiceCollectionServiceExtensions class:
namespace Microsoft.Extensions.DependencyInjection;
public static class ServiceCollectionServiceExtensions
{
public static IServiceCollection AddScoped<TService, TImplementation>(
this IServiceCollection services)
where TImplementation : class, TService
{
// registration...
}
}So the interview answer is:
“
AddSingleton,AddScoped, andAddTransientare extension methods defined in theServiceCollectionServiceExtensionsstatic class, in theMicrosoft.Extensions.DependencyInjectionnamespace. They extendIServiceCollection.”
Why does this work?
Because builder.Services is an IServiceCollection:
IServiceCollection services = builder.Services;
services.AddScoped<IOrderService, OrderService>();The this keyword makes it an extension method:
public static IServiceCollection AddScoped<TService, TImplementation>(
this IServiceCollection services)🔥 Interview distinction
Don’t say:
“
AddScoped()is a method ofIServiceCollection.”
More precisely:
“
AddScoped()is an extension method that extendsIServiceCollection.”
IServiceCollection itself is essentially the collection used to register services and their lifetimes for the .NET DI container.