Dependency Injection is one of the most commonly used features in ASP.NET Core applications.
Almost every project uses it in some form, whether it’s for services, repositories, logging, caching, or database access.
But while most developers use Dependency Injection daily, many still get confused when it comes to service lifetimes.
builder.Services.AddScoped<IService, Service>();
builder.Services.AddTransient<IService, Service>();
At first glance, these registrations look almost identical.
But the way they behave at runtime is completely different.
The lifetime you choose affects:
- How instances are created
- How data is shared
- How memory is used
- How your application behaves under load
Choosing the wrong lifetime might not fail immediately, but it can lead to subtle bugs, shared state issues, or unnecessary performance overhead that becomes difficult to debug later.
In this article, we’ll understand how Singleton, Scoped, and Transient actually behave in real-world ASP.NET Core applications.
What is Dependency Injection?
Dependency Injection is a design pattern where objects receive their dependencies from an external source instead of creating them manually.
Instead of doing this:
ASP.NET Core automatically creates and injects the required service for you.
For example:
public class UserController : ControllerBase
{
private readonly IUserService _userService;
public UserController(IUserService userService)
{
_userService = userService;
}
}
Here, the framework creates the instance of IUserService and injects it into the controller.
This improves:
- Maintainability
- Testability
- Flexibility
- Separation of concerns
But the important question is:
How long should that service instance live?
That’s where service lifetimes come into the picture.
Singleton Lifetime
builder.Services.AddSingleton<IService, Service>();
Singleton creates a single instance of the service and reuses it throughout the entire lifetime of the application.
No matter how many requests are made, the same object instance is shared everywhere.
Request Flow:
Request 1 → Instance A
Request 2 → Instance A
Request 3 → Instance A
This means every user and every request interacts with the same object.
If a value changes inside that service, the updated value is visible everywhere because the same instance is being reused.
Real-World Example
Singleton works well for services that should remain shared globally across the application.
Examples include:
- Application configuration
- Caching services
- Logging services
- Shared utility services
Example:
builder.Services.AddSingleton<ICacheService, CacheService>();
A cache service usually stores data that should be accessible across the entire application, so creating multiple instances would not make sense.
Things to Be Careful About
Because Singleton shares the same instance everywhere, storing request-specific or mutable data inside it can lead to unexpected behavior.
For example:
public class UserContextService
{
public string CurrentUser { get; set; }
}
If this service is registered as Singleton, different requests may overwrite CurrentUser unexpectedly.
This can create hard-to-debug issues because data starts leaking across requests.
Singleton is powerful, but it should only be used when shared state is actually required.
Scoped Lifetime
Scoped creates one instance of the service per request.
Within a single request, the same instance is reused across controllers, services, and repositories.
Once the request is completed, the instance is disposed.
Request 1 → Instance A
Request 2 → Instance B
Request 3 → Instance C
This means each request gets its own isolated instance.
However, everything inside the same request shares that instance.
Real-World Example
Scoped is ideal for request-based operations.
The most common example is DbContext.
builder.Services.AddScoped<AppDbContext>();
This ensures:
- Consistent tracking
- Shared transaction context
- Predictable database operations
But once the request finishes, that instance is removed.
This prevents data from leaking between users.
Why Scoped is Common in ASP.NET Core
Most business operations happen within the lifecycle of a request.
A user sends a request.
The application processes it.
The response is returned.
Scoped fits naturally into this flow.
That’s why many application services are registered as Scoped.
Transient Lifetime
Transient creates a new instance every time the service is requested.
Even within the same request, multiple usages of the same service create different instances.
Request Flow:
Req 1 → Instance A, B, C
Req 2 → Instance D, E, F
Nothing is reused.
Every dependency resolution creates a completely new object.
Real-World Example
Transient is useful for lightweight and stateless services.
Example:
builder.Services.AddTransient<IEmailFormatter, EmailFormatter>();
If the service only performs a simple operation and does not maintain any shared state, creating new instances is usually fine.
Things to Be Careful About
Overusing Transient services can lead to unnecessary object creation.
In high-load applications, excessive allocations may increase memory usage and affect performance.
Transient is useful when independence matters more than reuse.
But it should not become the default choice for everything.
Common Mistakes Developers Make
1. Using Singleton for Request-Specific Data
This is one of the most common mistakes.
If a Singleton stores user-specific or request-specific information, that data becomes shared across requests.
This can lead to unexpected behavior and data leaks.
2. Injecting Scoped Service into Singleton
Example:
public class NotificationService
{
private readonly AppDbContext _dbContext;
public NotificationService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
}
If NotificationService is Singleton and AppDbContext is Scoped, ASP.NET Core will throw an exception.
Why?
Because a Singleton lives longer than a Scoped service.
The framework prevents this mismatch to avoid invalid object lifetimes.
3. Using Transient Everywhere
Some developers register almost everything as Transient thinking it is the safest option.
While this avoids shared state, it can also create unnecessary objects repeatedly.
This increases allocations and may reduce performance under heavy load.
How to Choose the Correct Lifetime
A simple way to think about lifetimes is:
- Use Singleton when data should be shared globally
- Use Scoped when data should stay consistent within a request
- Use Transient when every usage should be independent
The decision is not about which lifetime is better.
It is about understanding how long your object should live.

0 Comments