Understanding Service Lifecycles in ASP.NET Core Like an Engineer
When developers first learn Dependency Injection in ASP.NET Core, service lifetimes usually feel like something you memorize.

Understanding Service Lifecycles in ASP.NET Core Like an Engineer
When developers first learn Dependency Injection in ASP.NET Core, service lifetimes usually feel like something you memorize.
Transient. Scoped. Singleton.
Most tutorials explain them in one paragraph each and move on.
But in real applications, service lifetimes are not just definitions. They directly affect how your application behaves under load, how memory is managed, how requests stay consistent, and whether your application remains reliable as it grows.
A service lifetime simply defines one thing:
How long should this object live in memory?
That question sounds small, but it shapes the architecture of your application.
Why Service Lifecycles Exist
Imagine an ASP.NET Core application handling thousands of requests every minute. For every request, the application creates services, processes business logic, talks to databases, sends emails, validates tokens, and writes logs. If every object stayed alive forever, memory usage would continuously grow. If every object was recreated unnecessarily, performance would suffer.
ASP.NET Core needs a strategy for object management.
Some objects should exist only for a moment. Some should survive for an entire request. Some should be shared across the whole application.
That is exactly what service lifetimes solve.
They help the framework decide:
- when to create objects
- how long to keep them alive
- and when to destroy them
This is not only about memory. It is also about consistency, scalability, and concurrency.
Dependency Injection and Object Management
ASP.NET Core uses Dependency Injection to create and manage services automatically.
Instead of manually creating dependencies:
var emailService = new EmailService();
You ask ASP.NET Core for them:
public class UserController
{
private readonly IEmailService _emailService;
public UserController(IEmailService emailService)
{
_emailService = emailService;
}
}
Now ASP.NET Core controls the lifecycle of that object. But the framework still needs instructions.
Should it create a new instance every time? Should it reuse one instance during a request? Should it share one instance across the entire application?
That instruction is the service lifetime.
1) Transient — Fresh Every Time
A transient service creates a brand-new instance every time it is requested.
builder.Services.AddTransient<IEmailService, EmailService>();
This is ideal for lightweight, stateless operations.
Think about an email service.
public class EmailService : IEmailService
{
public void Send(string to)
{
Console.WriteLine($"Sending email to {to}");
}
}
Every email operation is independent. The service does not need to remember previous calls. It simply performs work and finishes.
That is the behavior transient models perfectly.
What Happens If EmailService Is Scoped Instead?
Now imagine someone changes the registration:
builder.Services.AddScoped<IEmailService, EmailService>();
At first, nothing looks wrong. But later, another developer adds internal state:
public class EmailService
{
private readonly List<string> _recipients = new();
public void Send(string to)
{
_recipients.Add(to);
}
}
Since scoped services live for the entire request, multiple operations inside the same request now share the same recipient list.
Suddenly:
- duplicate emails may appear
- stale recipients may remain in memory
The problem is not the code itself. The problem is that the lifecycle no longer matches the behavior of the service. A stateless operation accidentally became stateful because the object lived longer than it should.
2) Scoped — One Instance Per Request
Scoped services create one instance for an entire HTTP request.
builder.Services.AddScoped<IUserService, UserService>();
During the request, every component shares the same instance. Once the request ends, ASP.NET Core disposes it. This is one of the most important lifecycles in web applications because requests naturally represent a unit of work.
Why DbContext Is Scoped?
DbContext is the classic example.
builder.Services.AddDbContext<AppDbContext>();
Inside one request, Entity Framework needs consistency. If a controller loads a user and a service updates that same user, both operations should work with the same tracked entity state. That only works correctly if the same DbContext instance is shared across the request.
What Happens If DbContext Is Transient?
Now imagine this registration:
builder.Services.AddTransient<AppDbContext>();
This creates a new database context every time it is requested.
That means:
- the controller gets one
DbContext - the service gets another
- the repository gets another
Now entity tracking becomes fragmented. One context may load an entity while another tries to update it. Transactions become inconsistent because different parts of the request are no longer operating inside the same unit of work. Under load, this also creates unnecessary database connections and extra memory allocations.
Everything still compiles. The application still runs.
But internally, the request has lost consistency.
That is why scoped exists.
Singleton — Shared Across the Entire Application
Singleton services create one instance for the entire application lifetime.
builder.Services.AddSingleton<ICacheService, CacheService>();
Every request, every user, and every thread shares the same object.
This is useful for application-wide resources like caching. A cache is meant to be shared. If one request loads product data into memory, another request should reuse it instead of hitting the database again.
That makes singleton a natural fit.
What is caching?
Caching is simply: Storing frequently used data in memory so you don’t have to fetch it again from the database or external service every time.
Instead of repeatedly doing this:
User request → Database → Return product
You do this:
User request → Memory (cache) → Return product instantly
So cache is basically a fast memory layer between your application and slow resources (like database).
Example:
Imagine Daraz homepage showing:
- “Top selling phones”
- “Trending laptops”
- “Flash sale products”
Now think: 1 million users open Daraz in 1 hour but product list doesn’t change every second.
So instead of hitting DB every time for each user → DB query (slow + expensive)
Daraz does:
First request → DB → store in cache Next 999,999 requests → memory cache (fast)
That is caching.
Why caching improves performance
Without cache:
- DB hit every request
- slow response
- high load on database
With cache:
- memory access
- almost instant response
- DB load drastically reduced
Memory is thousands of times faster than database access.
Why caching should be Singleton
Now the key question:
Why do we register cache as Singleton? Because: Cache only works if everyone shares the SAME stored data.
What if cache is Transient?
builder.Services.AddTransient<ICacheService, CacheService>();
Now every request gets a new cache:
User A → Cache A (empty)
User B → Cache B (empty)
User C → Cache C (empty)
As a Result:
- No shared data
- No reuse
- Every request goes to DB again
So caching becomes useless.
The Dangerous Side of Singleton
Singletons are powerful, but they are also dangerous when they store mutable shared state.
Consider this:
public class CounterService
{
public int Count = 0;
public void Increment()
{
Count++;
}
}
Registered as singleton:
builder.Services.AddSingleton<CounterService>();
Now every request modifies the same object simultaneously.
Two requests may try to update Count at the same time, creating race conditions and corrupted state.
This is why singleton services should usually remain:
- immutable
- thread-safe
- infrastructure-focused
Singletons are excellent for shared resources. They are dangerous for shared mutable state.
메타데이터
- post_id
- 0e05013b079f
- slug
- understanding-service-lifecycles-in-asp-net-core-like-an-engineer-0e05013b079f
- url
- https://medium.com/@sudippaudel944/understanding-service-lifecycles-in-asp-net-core-like-an-engineer-0e05013b079f
- canonical_url
- https://medium.com/@sudippaudel944/understanding-service-lifecycles-in-asp-net-core-like-an-engineer-0e05013b079f
- author_url
- https://medium.com/@sudippaudel944
- status
- ok
- fetched_at
- 2026-07-13 22:13:33