Understanding the Dependency Injection Lifecycle: Singleton, Scoped, and Transient With Detailed Examples
This article will help you gain a solid understanding of the lifecycle of singleton, scoped, and transient services when implementing dependency injection.
Join the DZone community and get the full member experience.
Join For FreeDevelopers may be aware of the lifecycle of service instances when using dependency injection, but many don’t fully grasp how it works. You can find numerous articles online that clarify these concepts, but they often just reiterate definitions that you might already know. Let me illustrate with a detailed example that simplifies the explanation.
When implementing dependency injection, developers have three options that determine the lifecycle of the instances:
- Singleton
- Scoped
- Transient
While most developers recognize these terms, a significant number struggle to determine which option to choose for a service's lifetime.
Definitions
Let me start with definitions:
- Singleton lifetime service instances are created once per application from the service container. A single instance will serve all subsequent requests. Singleton services are disposed of at the end of the application (i.e., upon application restart).
- Transient lifetime service instances are created per request from the service container. Transient services are disposed of at the end of the request.
- Scoped lifetime service instances are created once per client request. Transient services are disposed of at the end of the request.
When to Use
- Singleton - When you want to use single instances of services throughout the life cycle of the application
- Transient - When you want to use individual instances of services within the client request
- Scoped - When you want to use a single instance of service for each request
What is a client request? In very simple words, you can consider it as an API/REST call coming to your application by button clicks of the user to get the response.
Don’t worry, let's understand with an example.
Example
First, let's create interfaces/services and classes:
// we are declaring 3 services as below
Public interface ISingleton
Public interface ITransient
Public interface IScoped
Now let's write the implementation for each service Interface/service created above. We will try to understand the concept by trying to update the callMeSingleton
, callMeTransient
, and callMeScoped
variable.
- Singleton class implementation:
class SingletonImplementation: ISingleton
{
var callMeSingleton = ""
// other implementation
public SetSingleton(string value)
{
callMeSingleton = value;
}
// other implementation
}
- Transient class implementation:
class TransientImplementation: ITransient
{
var callMeTransient = ""
// other implementation
public SetTransient(string value)
{
callMeTransient = value;
}
// other implementation
}
- Scoped class implementation:
class ScopedImplementation: IScoped
{
var callMeScoped = ""
//other implementation
public SetScoped(string value)
{
callMeScoped = value;
}
//other implementation
}
Let's register (ConfigureServices
) with DI (Dependency Injection) to decide the life cycle of each service instance:
services.AddSingleton<ISingleton, SingletonImplementation>();
services.AddTransient<ITransient , TransientImplementation>();
services.AddScoped<IScoped , ScopedImplementation>();
Let's use/call these services from 3 different classes (ClassA
, ClassB
, and ClassC
) to understand the life cycle of each service:
ClassA
:
public class ClassA
{
private ISingleton _singleton;
//constructor to instantiate 3 different services we creates
public ClassA(ISingleton singleton,
ITransient _transient,
IScoped _scoped)
{
_singleton = singleton;
}
public void UpdateSingletonFromClassA()
{
_singleton.SetSingleton("I am from ClassA");
}
public void UpdateTransientFromClassA()
{
_transient.SetTransient("I am from ClassA");
}
public void UpdateScopedFromClassA()
{
_scoped.SetScoped("I am from ClassA");
}
// other implementation
}
ClassB
:
public class ClassB
{
private ISingleton _singleton;
//constructor to instantiate 3 different services we creates
public ClassB(ISingleton singleton,
ITransient _transient,
IScoped _scoped)
{
_singleton = singleton;
}
public void UpdateSingletonFromClassB()
{
_singleton.SetSingleton("I am from ClassB");
}
public void UpdateTransientFromClassB()
{
_transient.SetTransient("I am from ClassB");
}
public void UpdateScopedFromClassB()
{
_scoped.SetScoped("I am from ClassB");
}
// other implementation
}
ClassC
:
public class ClassC
{
private ISingleton _singleton;
//constructor to instantiate 3 different services we creates
public ClassC(ISingleton singleton,
ITransient _transient,
IScoped _scoped)
{
_singleton = singleton;
}
public void UpdateSingletonFromClassC()
{
_singleton.SetSingleton("I am from ClassC");
}
public void UpdateTransientFromClassC()
{
_transient.SetTransient("I am from ClassC");
}
public void UpdateScopedFromClassC()
{
_scoped.SetScoped("I am from ClassC");
}
// other implementation
}
Analysis
Let's analyze the results and behavior for each life cycle one by one from the above implementation:
Singleton
All the classes (ClassA
, ClassB
, and ClassC
) will use the same single instance of the SingletonImplementation
class throughout the lifecycle of the application. This means that properties, fields, and operations of the SingletonImplementation
class will be shared among instances used on all calling classes. Any updates to properties or fields will override previous changes.
For example, in the code above, ClassA
, ClassB
, and ClassC
are all utilizing the SingletonImplementation
service as a singleton instance and calling SetSingleton
to update the callMeSingleton
variable. In this case, there will be a single value of the callMeSingleton
variable for all requests trying to access this property. Whichever class accesses it last to update will override the value of callMeSingleton
.
ClassA
- It will have its same instance as other classes for serviceTransientImplementation
.ClassB
- It will have its same instance as other classes for serviceTransientImplementation
.ClassC
- It will have its same instance as other classes for serviceTransientImplementation
.
ClassA
, ClassB
, and ClassC
are updating the same instance of the SingletonImplementation
class, which will override the value of callMeSingleton
. Therefore, be careful when setting or updating properties in the singleton service implementation.
Singleton services are disposed of at the end of the application (i.e., upon application restart).
Transient
All the classes (ClassA
, ClassB
, and ClassC
) will use their individual instances of the TransientImplementation
class. This means that if one class calls for properties, fields, or operations of the TransientImplementation
class, it will only update or override its individual instance values. Any updates to properties or fields are not shared among other instances of TransientImplementation
.
Let's understand:
ClassA
- It will have its own instance of service ofTransientImplementation
.ClassB
- It will have its own instance of service ofTransientImplementation
.ClassC
- It will have its own instance of service ofTransientImplementation
.
Let's say you have a ClassD
which is calling transient service from ClassA
, ClassB
, and ClassC
instances. In this case, each class instance would be treated as different/separate instance and each class would have its own value of callMeTransient
. Read the inline comments below for ClassD
:
public ClassD
{
// other implementation
// Below line of code will update the value of callMeTransient to "I am from ClassA" for the intance of ClassA only.
// And it will not be changed by any next calls from Class B or B class
ClassA.UpdateTransientFromClassA();
// Below line of code will update the value of callMeTransient to "I am from ClassB" for the intance of ClassB only.
// And it will neither override the value for calssA instance nor will be changed by next call from Class C
ClassB.UpdateTransientFromClassB();
// Below line of code will update the value of callMeTransient to "I am from ClassC" for the intance of ClassC only.
// And it will neither override the value for calssA and classB instance nor will be changed by any next call from any other class.
ClassC.UpdateTransientFromClassC();
// other implementation
}
Transient services are disposed at the end of each request. Use Transient when you want a state less behavior within the request.
Scoped
All the classes (ClassA
, ClassB
, and ClassC
) will be using single instances of ScopedImplementation
class for each request. This means that calls for properties/fields/operations on ScopedImplementation
class will happen on single instance with in the scope of request. Any updates of properties/fields will be shared among other classes.
Let's understand:
ClassA
- It will have its instance of service ofTransientImplementation
.ClassB
- It will have its same instance of service ofTransientImplementation
asClassA
.ClassC
- It will have its same instance of service ofTransientImplementation
asClassA
andClassB
.
Let's say you have a ClassD
which is calling scoped service from ClassA
, ClassB
, and ClassC
instances. In this case, each class will have single instance of ScopedImplementation
class. Read the inline comments for ClassD
below.
public class ClassD
{
// other implementation
// Below code will update the value of callMeScoped to "I am from ClassA" for the instance of ClassA
// But as it is Scoped life cycle so it is holding single instance ScopedImplementation of
// Then it can be overridden by next call from ClassB or ClassC
ClassA.UpdateScopedFromClassA();
// Below code will update the value of callMeScoped to "I am from ClassB" for single instance ScopedImplementation
// And it will override the value of callMeScoped for classA instance too.
ClassB.UpdateScopedFromClassB();
// Now if Class A will perform any operation on ScopedImplementation,
// it will use the latest properties/field values which are overridden by classB.
// Below code will update the value of callMeScoped to "I am from ClassC"
// And it will override the value of callMeScoped for classA and ClassB instance too.
ClassC.UpdateScopedFromClassC();
// now if Class B or Class A will perform any operation on ScopedImplementation , it will use the latest properties/field values which are overridden by classC
// other implementation
}
Scoped services are disposed at the end of each request. Use Scoped when you want a stateless behavior between individual requests.
Trivia Time
The lifecycle of a service can be overridden by a parent service where it gets initialized. Confused? Let me explain:
Let's take the same example from above classes and initialize the Transient and Scoped services from SingletonImplementation
(which is a singleton) as below. That would initiate the ITransient
and IScoped
services and overwrite the lifecycle of these to singleton life cycle as parent service. In this case your application would not have any Transient or Scoped services (considering you just have these 3 services we were using in our examples).
Read through the lines in the below code:
public class SingletonImplementation: ISingleton
{
// constructor to add initialize the services.
private readonly ITransient _transient
private readonly IScoped _scoped
SingletonImplementation(ITransient transient, IScoped scoped)
{
_transient = transient;
// Now _transient would behave as singleton service irrespective of how it was registered as Transient
_scoped = scoped;
// now scoped would behave as singleton service irrespective of it being registered as Scoped
}
var callMeSingleton = ""
// other implementation
}
Summary
I hope the above article is helpful in understanding the topic. I would recommend try it yourself with the context set above and you will never be confused again. Singleton is the easiest to understand because once you create its instance, it will be shared across applications throughout the lifecycle of the application. On the similar lines of Singleton, Scoped instances mimic the same behavior but only throughout the lifecycle of a request across application. Transient is totally stateless, for each request and each class instance will hold its own instance of serivice.
Opinions expressed by DZone contributors are their own.
Comments