Friday, August 21, 2026

Service Life Cycle in .NET Core Application

 


Transient vs Scoped vs Singleton with Real-Time E-Commerce Example

Dependency Injection (DI) is one of the most important concepts in ASP.NET Core. It helps us create loosely coupled, maintainable, testable, and scalable applications.

One of the most frequently asked .NET interview questions is:

What is Service Lifetime in .NET Core? Explain Transient, Scoped, and Singleton with a real-time example.

To understand this properly, we need to understand what happens to a service from the moment it is requested until the moment it is destroyed.


1. What is Service Life Cycle?

A Service Life Cycle defines:

  • When an object is created

  • How long that object lives

  • Whether the same object is reused

  • When the object is destroyed

In ASP.NET Core, the built-in Dependency Injection container provides three primary service lifetimes:

  1. Transient

  2. Scoped

  3. Singleton

The basic idea is:

Application Starts
       |
       v
DI Container Created
       |
       v
Request Comes
       |
       v
Controller / Service Requests Dependency
       |
       v
DI Container Checks Service Lifetime
       |
       +-------------------+
       |                   |
   Transient           Scoped
       |                   |
New instance          One per Request
       |                   |
       +---------+---------+
                 |
              Singleton
                 |
          One per Application

2. Dependency Injection in ASP.NET Core

Suppose we have an e-commerce application.

A request comes to:

GET /api/orders/1001

The request reaches the controller:

public class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;

    public OrdersController(IOrderService orderService)
    {
        _orderService = orderService;
    }
}

ASP.NET Core needs to create IOrderService.

It looks into the DI container and asks:

How is IOrderService registered?
What is its lifetime?
Should I create a new instance?
Can I reuse an existing instance?

For example:

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

This tells ASP.NET Core:

Create one OrderService instance for each HTTP request and reuse it throughout that request.


3. The Three Service Lifetimes

The three important registrations are:

services.AddTransient<IService, Service>();

services.AddScoped<IService, Service>();

services.AddSingleton<IService, Service>();

Their behavior is different.

LifetimeInstance CreationLifetime
TransientNew instance every time requestedVery short
ScopedOne instance per HTTP requestRequest lifetime
SingletonOne instance for applicationApplication lifetime

Let's understand each one with a real-time example.


4. Transient Lifetime

Transient means:

A new object is created every time the service is requested from the DI container.

Registration:

builder.Services.AddTransient<IEmailService, EmailService>();

Suppose:

public interface IEmailService
{
    void SendEmail();
}

Implementation:

public class EmailService : IEmailService
{
    public EmailService()
    {
        Console.WriteLine("EmailService Created");
    }

    public void SendEmail()
    {
        Console.WriteLine("Email Sent");
    }
}

Now imagine:

public class OrderService
{
    private readonly IEmailService _emailService;

    public OrderService(IEmailService emailService)
    {
        _emailService = emailService;
    }
}

Another service also requests:

public class NotificationService
{
    private readonly IEmailService _emailService;

    public NotificationService(IEmailService emailService)
    {
        _emailService = emailService;
    }
}

Because EmailService is transient:

OrderService
     |
     +---- EmailService Instance #1

NotificationService
     |
     +---- EmailService Instance #2

Two different objects are created.


5. Real-Time Use Cases for Transient

Transient is suitable for lightweight, stateless services.

Examples:

Email Formatter
Message Formatter
DTO Mapper
Validation Service
Small Calculation Service
Data Transformation Service

For example:

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

Every time IPriceCalculator is requested, a new instance can be created.


6. Scoped Lifetime

Scoped is probably the most commonly used lifetime in ASP.NET Core Web API applications.

Registration:

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

Scoped means:

One instance is created for a particular scope. In ASP.NET Core Web API, the scope normally corresponds to one HTTP request.

Consider:

POST /api/orders

The request enters ASP.NET Core.

ASP.NET Core creates a request scope.

HTTP Request
     |
     v
Request Scope Created
     |
     +---- OrderService
     |
     +---- CustomerService
     |
     +---- Repository
     |
     +---- DbContext
     |
     v
Request Completed
     |
     v
Request Scope Destroyed

All scoped services requested during that request can share their scoped instance.


7. Real-Time Example – E-Commerce Order

Consider an e-commerce application.

The request:

POST /api/orders

requires:

OrdersController
       |
       v
OrderService
       |
       v
OrderRepository
       |
       v
ApplicationDbContext

Registration:

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

During Request #1:

Request #1

OrderService       ---> Instance A
OrderRepository    ---> Instance A
DbContext          ---> Instance A

Another request arrives:

POST /api/orders

Request #2 gets different scoped objects:

Request #2

OrderService       ---> Instance B
OrderRepository    ---> Instance B
DbContext           ---> Instance B

Therefore:

Request #1
   |
   +-- OrderService A
   +-- Repository A
   +-- DbContext A


Request #2
   |
   +-- OrderService B
   +-- Repository B
   +-- DbContext B

This isolation is one of the major reasons why DbContext is normally registered as Scoped.


8. Why DbContext is Usually Scoped

Consider an order transaction:

Create Order
     |
     +-- Insert Order
     |
     +-- Insert Order Items
     |
     +-- Update Inventory
     |
     +-- SaveChanges()

The same DbContext can track entities involved in that request.

For example:

public class OrderService
{
    private readonly ApplicationDbContext _context;

    public OrderService(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task CreateOrder(Order order)
    {
        _context.Orders.Add(order);

        await _context.SaveChangesAsync();
    }
}

Using a scoped context helps keep the database unit of work associated with the request.


9. Singleton Lifetime

Singleton means:

Only one instance of the service is created for the application's DI container lifetime, and that instance is reused wherever the service is requested.

Registration:

builder.Services.AddSingleton<IApplicationConfiguration, ApplicationConfiguration>();

Conceptually:

Application Starts
       |
       v
Singleton Instance Created
       |
       +-----------------------+
       |                       |
Request 1                  Request 2
       |                       |
       +----------+------------+
                  |
          Same Singleton

The same object can be reused across requests.


10. Real-Time Singleton Example

Suppose our application has application-wide configuration.

public interface IApplicationConfiguration
{
    string ApplicationName { get; }
}

Implementation:

public class ApplicationConfiguration : IApplicationConfiguration
{
    public string ApplicationName => "E-Commerce API";
}

Registration:

builder.Services.AddSingleton<
    IApplicationConfiguration,
    ApplicationConfiguration>();

Now multiple requests can use the same instance.

Request 1 ----+
              |
Request 2 ----+----> ApplicationConfiguration
              |
Request 3 ----+

11. Real-Time Example – Product Cache

Suppose an application frequently reads product categories.

Instead of querying the database repeatedly, we may maintain an application-level cache.

Conceptually:

public class ProductCache
{
    private readonly Dictionary<int, string> _products
        = new();

    public void Add(int id, string name)
    {
        _products[id] = name;
    }

    public string? Get(int id)
    {
        return _products.TryGetValue(id, out var name)
            ? name
            : null;
    }
}

Registration:

builder.Services.AddSingleton<ProductCache>();

The same cache object can be shared by requests.

However, when using mutable shared state, thread safety must be considered carefully. A singleton should not simply contain an ordinary mutable collection and assume concurrent requests are safe.


12. Complete E-Commerce Example

Let's build a simplified dependency chain.

OrdersController
       |
       v
OrderService
       |
       +---- OrderRepository
       |
       +---- PaymentService
       |
       +---- EmailService
       |
       +---- ApplicationCache
       |
       v
ApplicationDbContext

Possible registrations:

builder.Services.AddControllers();

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

builder.Services.AddScoped<IOrderRepository, OrderRepository>();

builder.Services.AddScoped<IPaymentService, PaymentService>();

builder.Services.AddTransient<IEmailService, EmailService>();

builder.Services.AddSingleton<ProductCache>();

builder.Services.AddDbContext<ApplicationDbContext>();

Now each service has an appropriate lifetime based on its responsibilities.


13. What Happens During an HTTP Request?

Let's follow a request step-by-step.

Request:

POST /api/orders

Step 1 – Request Arrives

The client sends:

POST /api/orders

ASP.NET Core receives the request.


Step 2 – Request Scope Is Created

ASP.NET Core creates a scope for the HTTP request.

Request Scope
     |
     +-------------------------+
     |                         |
     v                         v
Scoped Services          Other Dependencies

Step 3 – Controller Is Created

ASP.NET Core needs:

OrdersController

Its constructor requires:

IOrderService

The DI container resolves it.


Step 4 – Scoped OrderService Is Created

Because it is registered as:

AddScoped<IOrderService, OrderService>()

ASP.NET Core creates one instance for this request.


Step 5 – Dependencies Are Resolved

Suppose OrderService requires:

IOrderRepository
IPaymentService
IEmailService

The DI container resolves them.

OrderService
     |
     +-- OrderRepository    Scoped
     |
     +-- PaymentService     Scoped
     |
     +-- EmailService       Transient

Step 6 – DbContext Is Resolved

OrderRepository requires:

ApplicationDbContext

Because DbContext is scoped, the request receives its scoped instance.

OrderService
      |
      v
OrderRepository
      |
      v
ApplicationDbContext

Step 7 – Singleton Is Resolved

Suppose ProductCache is needed.

The DI container checks:

Is ProductCache already created?

If yes:

Use existing instance

If no:

Create singleton instance

Step 8 – Request Executes

The order is processed.

Validate Order
      |
      v
Check Product
      |
      v
Create Order
      |
      v
Process Payment
      |
      v
Save Database Changes
      |
      v
Send Notification

Step 9 – Request Completes

The HTTP request ends.

ASP.NET Core disposes the request scope.

Scoped and transient disposable services associated with that scope are disposed according to the DI container's lifetime management.

The singleton remains alive while its container remains alive.


14. Visualizing All Three Lifetimes

Imagine three HTTP requests.

                  APPLICATION
                      |
             Singleton Instance
                      |
          +-----------+-----------+
          |           |           |
       Request 1   Request 2   Request 3
          |           |           |
       Scoped A    Scoped B    Scoped C
          |           |           |
       Transient   Transient   Transient
          A           B           C

The important point is:

Singleton
   |
   +-- Same instance across requests

Scoped
   |
   +-- Same instance within one request
   +-- Different instance for another request

Transient
   |
   +-- New instance whenever resolved

15. Important Difference: Scoped vs Transient

This is a common interview question.

Suppose:

builder.Services.AddScoped<IService, MyService>();

and the same service is requested twice within one scope.

Conceptually:

Request
  |
  +-- Resolve IService ---> Instance A
  |
  +-- Resolve IService ---> Instance A

Same instance.

With:

builder.Services.AddTransient<IService, MyService>();

you can get:

Request
  |
  +-- Resolve IService ---> Instance A
  |
  +-- Resolve IService ---> Instance B

Different instances.


16. Important Difference: Scoped vs Singleton

Scoped:

Request 1 ---> Instance A
Request 2 ---> Instance B
Request 3 ---> Instance C

Singleton:

Request 1 ---+
Request 2 ---+---> Same Instance A
Request 3 ---+

Therefore, singleton services should be designed very carefully because multiple requests can access the same instance concurrently.


17. Can Singleton Depend on Scoped Service?

This is an important interview question.

Suppose:

builder.Services.AddSingleton<MySingleton>();
builder.Services.AddScoped<MyScoped>();

and:

public class MySingleton
{
    private readonly MyScoped _scoped;

    public MySingleton(MyScoped scoped)
    {
        _scoped = scoped;
    }
}

This creates a lifetime mismatch.

A singleton lives much longer than a scoped service.

Conceptually:

Singleton
     |
     v
Scoped Service

The scoped service cannot naturally live for the entire lifetime of the singleton.

ASP.NET Core's DI validation can detect such captive dependency problems in appropriate environments/configurations.

General rule:

Singleton
   ↓
Should not directly depend on
   ↓
Scoped

18. Can Scoped Depend on Singleton?

Yes.

For example:

Scoped OrderService
        |
        v
Singleton ApplicationConfiguration

This is generally valid because the singleton has a lifetime longer than the scoped service.


19. Can Transient Depend on Scoped?

Within a valid request scope, yes.

For example:

Scoped OrderService
        |
        v
Transient EmailFormatter

The transient object is created for the resolution and can use dependencies available in that scope.


20. Can Singleton Depend on Transient?

This requires careful consideration.

Technically, a singleton can resolve a transient dependency during its construction, but that transient instance then effectively becomes held by the singleton for as long as the singleton holds it.

Therefore, the dependency's effective lifetime can become much longer than intended.

This is sometimes called a captive dependency.

So don't choose lifetimes merely because the DI container allows the registration.

Choose them based on the object's state and responsibilities.


21. Service Lifetime and Thread Safety

This is especially important for Singleton services.

Imagine:

public class CounterService
{
    private int _count;

    public void Increment()
    {
        _count++;
    }
}

If registered as:

builder.Services.AddSingleton<CounterService>();

multiple requests may access the same object concurrently.

Therefore:

Request A ----+
              |
Request B ----+----> Same Singleton
              |
Request C ----+

The implementation must be safe for concurrent access.

For shared mutable state, use appropriate thread-safe techniques or concurrency-safe collections where necessary.


22. Service Lifetime in Microservices

In a microservices architecture, each microservice normally has its own DI container and process/application lifetime.

For example:

Order Service
     |
     +-- Singleton
     +-- Scoped
     +-- Transient

Payment Service
     |
     +-- Singleton
     +-- Scoped
     +-- Transient

Inventory Service
     |
     +-- Singleton
     +-- Scoped
     +-- Transient

A singleton in the Order Service is not automatically shared with the Payment Service.

Each application has its own process/container and its own DI registrations.


23. Service Lifetime vs Database Lifetime

Don't confuse these concepts.

Service lifetime:

Transient
Scoped
Singleton

Database connection lifetime is a separate concern.

For example:

Web API
   |
   v
DbContext
   |
   v
Database Provider
   |
   v
Database

DbContext is normally scoped, while the underlying database connection management is handled by the database provider and connection pooling mechanisms.


24. Common Real-Time Registration

A typical ASP.NET Core application might contain:

builder.Services.AddControllers();

builder.Services.AddDbContext<ApplicationDbContext>();

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

builder.Services.AddScoped<IPaymentService, PaymentService>();

builder.Services.AddTransient<IEmailTemplateService, EmailTemplateService>();

builder.Services.AddSingleton<IApplicationSettings, ApplicationSettings>();

A reasonable conceptual mapping is:

ServiceTypical LifetimeReason
DbContextScopedRequest/unit-of-work oriented
RepositoryScopedWorks with DbContext
Business ServiceScopedRequest-level operation
Stateless FormatterTransientLightweight/stateless
Application ConfigurationSingletonShared application-level data
In-memory CacheSingletonShared cache, if designed safely

These are common patterns, not absolute rules.


25. Service Life Cycle and IDisposable

Another important concept is disposal.

Suppose:

public class FileService : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Disposed");
    }
}

If the DI container creates and owns a disposable service, it generally manages its disposal according to the service lifetime and scope.

For example, a scoped disposable service is normally disposed when its request scope ends.

A singleton disposable service is normally disposed when the application's DI container is disposed.

This is one reason you should generally let the DI container manage dependencies that it creates rather than manually disposing injected dependencies.


26. Service Lifetime and Background Services

A common mistake is trying to inject a scoped service directly into a long-running BackgroundService.

For example:

public class OrderBackgroundService : BackgroundService
{
    private readonly ApplicationDbContext _context;

    public OrderBackgroundService(ApplicationDbContext context)
    {
        _context = context;
    }
}

This is problematic because BackgroundService is effectively long-lived, while DbContext is scoped.

A better approach is to create a scope when processing each unit of work:

public class OrderBackgroundService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

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

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

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

        // Process work
    }
}

For repeated background work, create and dispose an appropriate scope for each unit of work rather than keeping one scoped dependency forever.


27. How DI Container Resolves a Service

Suppose:

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

and:

public class OrderService : IOrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }
}

When the controller requests IOrderService, the DI container performs roughly this process:

1. Controller requests IOrderService
             |
             v
2. DI Container checks registration
             |
             v
3. Finds OrderService
             |
             v
4. Checks OrderService lifetime
             |
             v
5. Scoped
             |
             v
6. Checks current scope
             |
             v
7. Creates OrderService if not already created
             |
             v
8. Sees IOrderRepository dependency
             |
             v
9. Resolves IOrderRepository
             |
             v
10. Creates OrderService
             |
             v
11. Injects dependencies
             |
             v
12. Controller receives OrderService

This is the essence of Dependency Injection.


28. Real-Time Request Example

Let's assume:

Customer places an order

Request:

POST /api/orders

Flow:

Client
  |
  | HTTP Request
  v
ASP.NET Core
  |
  v
Middleware Pipeline
  |
  v
Request Scope
  |
  v
OrdersController
  |
  v
OrderService
  |
  +---- OrderRepository
  |          |
  |          v
  |      DbContext
  |
  +---- PaymentService
  |
  +---- EmailService
  |
  +---- ProductCache
  |
  v
Database

Lifetimes might be:

OrderService        -> Scoped
OrderRepository     -> Scoped
DbContext           -> Scoped
PaymentService      -> Scoped
EmailService        -> Transient
ProductCache        -> Singleton

At the end:

HTTP Request Ends
       |
       v
Request Scope Disposed
       |
       +-- Scoped services disposed
       +-- Request-owned transient disposables disposed
       |
       v
Singleton remains available

29. Common Interview Questions

Q1. What are the three service lifetimes?

Answer:

Transient
Scoped
Singleton

Q2. What is Transient?

A new service instance is created each time the service is requested from the DI container.

services.AddTransient<IEmailService, EmailService>();

Q3. What is Scoped?

One service instance is generally created per scope. In ASP.NET Core Web API, this normally means one instance per HTTP request.

services.AddScoped<IOrderService, OrderService>();

Q4. What is Singleton?

One service instance is reused for the lifetime of the DI container/application.

services.AddSingleton<ICache, Cache>();

Q5. Which lifetime is normally used for DbContext?

DbContext is normally registered as Scoped in ASP.NET Core applications.


Q6. Why shouldn't DbContext normally be Singleton?

Because DbContext is designed around a unit-of-work pattern and is not intended to be shared concurrently across unrelated requests.


Q7. Which lifetime is best for stateless lightweight services?

Often Transient, although Scoped can also be appropriate depending on the service's dependencies and design.


Q8. Which lifetime is best for shared application-wide state?

A Singleton can be appropriate, but shared mutable state must be designed for concurrent access and application lifetime.


Q9. Can Singleton depend on Scoped?

Generally, no direct dependency should be created, because it creates a lifetime mismatch/captive dependency.


Q10. Can Scoped depend on Singleton?

Yes. This is generally valid.


30. Common Mistakes

Mistake 1 – Making Everything Singleton

Avoid:

services.AddSingleton<OrderService>();
services.AddSingleton<OrderRepository>();
services.AddSingleton<ApplicationDbContext>();

This can create serious lifetime and concurrency problems.


Mistake 2 – Making Everything Transient

Using transient everywhere can cause unnecessary object creation and can undermine intentional request-level sharing.


Mistake 3 – Ignoring Thread Safety

Singleton services may be accessed concurrently.

Never assume:

Singleton = Automatically Thread Safe

It is not.


Mistake 4 – Injecting Scoped Services into Long-Lived Services

For example:

BackgroundService
       |
       v
DbContext

Instead, create a scope when processing the background operation.


31. Easy Way to Remember

Remember this formula:

Transient = Every Time

Scoped = Every Request

Singleton = Entire Application

Or:

Transient
    ↓
New Object

Scoped
    ↓
One Object Per Scope

Singleton
    ↓
One Object Per Container Lifetime

32. Final Comparison

FeatureTransientScopedSingleton
New instance frequently?YesNoNo
Same instance within request?Not necessarilyYesYes
Same instance across requests?NoNoYes
Typical lifetimeResolutionScope/requestApplication/container
Thread safety concernUsually less shared stateUsually less shared across requestsHigh if mutable
DbContext
RepositoryUsually Scoped
Stateless lightweight serviceSometimesSometimes
Application-wide cache✅, if designed safely
Configuration serviceSometimesSometimesOften

33. Complete Mental Model

When you see:

builder.Services.AddTransient<A>();
builder.Services.AddScoped<B>();
builder.Services.AddSingleton<C>();

think:

                    DI CONTAINER
                         |
        +----------------+----------------+
        |                |                |
        v                v                v
    TRANSIENT         SCOPED          SINGLETON
        |                |                |
     New Object       One per Scope    One per Container
        |                |                |
        |           HTTP Request       Application
        |                |                |
        v                v                v
     A1, A2...          B1              C1
                       B2              C1
                       B3              C1

The key idea is that service lifetime is not merely about object creation—it determines how long the object can retain state and who can share that state.


Conclusion

Service Lifetime is a fundamental part of Dependency Injection in ASP.NET Core.

The three primary lifetimes are:

Transient → New instance when resolved

Scoped → One instance per scope/request

Singleton → One instance for the DI container lifetime

For a real-world e-commerce Web API, a common design is:

Controller
    |
    v
OrderService        → Scoped
    |
    +---- Repository → Scoped
    |
    +---- DbContext  → Scoped
    |
    +---- Payment    → Scoped
    |
    +---- Email      → Transient
    |
    +---- Cache      → Singleton

Choosing the correct lifetime is important for:

  • Performance

  • Memory management

  • Thread safety

  • Database consistency

  • Resource management

  • Scalability

  • Application stability

The most important interview rule to remember is:

Transient = new instance, Scoped = one instance per scope/request, Singleton = one instance for the container lifetime.

Once you understand this concept, Dependency Injection, DbContext, middleware, background services, repositories, caching, and ASP.NET Core application architecture become much easier to understand.

No comments:

Don't Copy

Protected by Copyscape Online Plagiarism Checker