Singleton Design Pattern in C#
1. What is Singleton Design Pattern?
Singleton is a Creational Design Pattern that ensures a class has only one instance throughout the application and provides a common way to access that instance.
Simple definition:
Singleton = Create only one object of a class and reuse the same object everywhere.
For example:
Application
│
├── Service A ──┐
├── Service B ──┼──► Same Singleton Object
└── Service C ──┘
Instead of:
Service A → Object 1
Service B → Object 2
Service C → Object 3
Singleton gives:
Service A ──┐
Service B ──┼──► Object 1
Service C ──┘
2. Why Do We Need Singleton?
Suppose we have a simple application counter.
public class AppCounter
{
public int Count { get; private set; }
public void Increment()
{
Count++;
}
}
If different parts of the application create their own objects:
var counter1 = new AppCounter();
var counter2 = new AppCounter();
counter1.Increment();
Console.WriteLine(counter1.Count);
Console.WriteLine(counter2.Count);
Output:
1
0
Why?
Because:
counter1 → AppCounter Object #1 → Count = 1
counter2 → AppCounter Object #2 → Count = 0
They are different objects.
3. Requirement
Suppose our requirement is:
The entire application should maintain one common counter.
We want:
Application
│
├── Module A ──┐
│ │
├── Module B ──┼──► AppCounter
│ │ Count
└── Module C ──┘
Everyone should use the same instance.
This is where Singleton can be useful.
4. Simple Singleton Program
Let's create a very simple Singleton.
public class AppCounter
{
private static AppCounter? _instance;
private AppCounter()
{
}
public static AppCounter Instance
{
get
{
if (_instance == null)
{
_instance = new AppCounter();
}
return _instance;
}
}
public int Count { get; private set; }
public void Increment()
{
Count++;
}
}
Then:
public class Program
{
public static void Main()
{
AppCounter counter1 = AppCounter.Instance;
counter1.Increment();
AppCounter counter2 = AppCounter.Instance;
counter2.Increment();
Console.WriteLine(counter1.Count);
Console.WriteLine(counter2.Count);
Console.WriteLine(
ReferenceEquals(counter1, counter2));
}
}
Output:
2
2
True
The important output is:
True
It proves:
counter1
│
└────► Same AppCounter Object
▲
┌────────────────┘
counter2
5. Full Simple Program
using System;
public class AppCounter
{
// Holds the single instance
private static AppCounter? _instance;
// Private constructor
private AppCounter()
{
Console.WriteLine("AppCounter object created.");
}
// Provides access to the single instance
public static AppCounter Instance
{
get
{
if (_instance == null)
{
_instance = new AppCounter();
}
return _instance;
}
}
public int Count { get; private set; }
public void Increment()
{
Count++;
}
}
public class Program
{
public static void Main()
{
AppCounter counter1 = AppCounter.Instance;
counter1.Increment();
AppCounter counter2 = AppCounter.Instance;
counter2.Increment();
Console.WriteLine($"Counter 1: {counter1.Count}");
Console.WriteLine($"Counter 2: {counter2.Count}");
Console.WriteLine(
$"Same Object: {ReferenceEquals(counter1, counter2)}");
}
}
Output
AppCounter object created.
Counter 1: 2
Counter 2: 2
Same Object: True
Notice:
AppCounter object created.
appears only once.
6. How Does Singleton Work?
Three things are particularly important.
1. Static Instance Variable
private static AppCounter? _instance;
This variable stores the single object.
Because it is static, it belongs to the class rather than to an individual object.
Conceptually:
AppCounter Class
│
└── static _instance
│
▼
AppCounter Object
7. Private Constructor
This is one of the most important parts:
private AppCounter()
{
}
Normally, if the constructor were public:
public AppCounter()
{
}
anyone could write:
var a = new AppCounter();
var b = new AppCounter();
var c = new AppCounter();
That would create three objects and defeat the purpose of Singleton.
Therefore, the constructor is:
private AppCounter()
Now this is not allowed outside the class:
var counter = new AppCounter();
The compiler will reject it because the constructor is inaccessible.
So:
Private constructor prevents normal external object creation.
8. Public Static Instance
The application still needs some way to get the object.
That's why we have:
public static AppCounter Instance
The client uses:
AppCounter.Instance
instead of:
new AppCounter()
9. What Happens the First Time?
Suppose we execute:
AppCounter counter1 = AppCounter.Instance;
The getter executes:
if (_instance == null)
{
_instance = new AppCounter();
}
Initially:
_instance = null
Therefore:
Is _instance null?
│
YES
↓
new AppCounter()
↓
Store object in _instance
↓
Return _instance
Now:
_instance
│
▼
AppCounter Object
10. What Happens the Second Time?
Now:
AppCounter counter2 = AppCounter.Instance;
Again:
if (_instance == null)
But this time:
_instance != null
because the object already exists.
Therefore:
Is _instance null?
│
NO
↓
Don't create another object
↓
Return existing _instance
So:
counter1 ─────┐
│
▼
AppCounter Object
▲
│
counter2 ─────┘
11. Complete Execution Flow
First call:
AppCounter.Instance
Flow:
AppCounter.Instance
↓
Is _instance null?
↓
Yes
↓
new AppCounter()
↓
Store in _instance
↓
Return object
Second call:
AppCounter.Instance
↓
Is _instance null?
↓
No
↓
Return existing object
That's the basic Singleton mechanism.
12. Why Is static Important?
Consider:
private static AppCounter? _instance;
static means the variable belongs to the class itself.
You don't need an AppCounter object to access:
AppCounter.Instance
This is important because if we needed an object first:
var counter = new AppCounter();
we would already have broken the Singleton concept.
Therefore we access the singleton through the class:
AppCounter.Instance
13. Lazy Creation
Our example creates the object only when somebody first requests it.
if (_instance == null)
{
_instance = new AppCounter();
}
This is called:
Lazy Initialization
Meaning:
Don't create the object until it is actually needed.
For example:
Application Starts
↓
No AppCounter created
Some code calls AppCounter.Instance
↓
AppCounter created
14. Important Problem With Our Simple Version
The previous example is good for understanding the pattern, but it has a thread-safety problem.
Imagine two threads execute this simultaneously:
if (_instance == null)
{
_instance = new AppCounter();
}
Possible situation:
Thread A Thread B
_instance == null _instance == null
↓ ↓
true true
↓ ↓
new AppCounter() new AppCounter()
Potentially, two objects could be created.
For a real multithreaded application, we need a thread-safe implementation.
15. Recommended Simple C# Singleton
A cleaner implementation uses Lazy<T>:
public sealed class AppCounter
{
private static readonly Lazy<AppCounter> _instance =
new Lazy<AppCounter>(() => new AppCounter());
private AppCounter()
{
Console.WriteLine("AppCounter created.");
}
public static AppCounter Instance
{
get
{
return _instance.Value;
}
}
public int Count { get; private set; }
public void Increment()
{
Count++;
}
}
Or more concisely:
public sealed class AppCounter
{
private static readonly Lazy<AppCounter> _instance =
new(() => new AppCounter());
private AppCounter()
{
}
public static AppCounter Instance => _instance.Value;
public int Count { get; private set; }
public void Increment()
{
Count++;
}
}
Lazy<T> gives us lazy initialization and handles thread-safe instance initialization by default.
Why use Lazy<AppCounter>?
It gives two important benefits:
Lazy initialization: the AppCounter isn't created until it is first needed.
Thread-safe initialization: Lazy<T>'s default mode safely ensures one initialization when multiple threads try to access .Value concurrently.
One additional point: while Lazy<T> makes creation of the Singleton instance thread-safe, the mutable Count++ operation itself is not thread-safe. In a multithreaded application such as Web API, we'd use Interlocked.Increment() or another synchronization mechanism for the counter state.
16. Why sealed?
Notice:
public sealed class AppCounter
sealed prevents another class from inheriting from AppCounter.
For example:
public class MyCounter : AppCounter
is not allowed.
For Singleton implementations, sealing the class is commonly used to prevent inheritance from complicating the one-instance guarantee.
17. Singleton Structure
The pattern can be visualized as:
Singleton Class
┌────────────────────┐
│ │
│ private constructor│
│ │
│ static instance │
│ │
│ Instance property │
│ │
└─────────┬──────────┘
│
▼
Single Object
▲
┌─────┼─────┐
│ │ │
Client A B C
18. Real-World Uses
Singleton-style lifetimes are useful when one shared instance is appropriate, for example:
- Application-wide configuration service
- In-memory shared cache service
- Metrics collector
- Feature/configuration registry
- Expensive stateless service that is safe to share
However, in modern ASP.NET Core, you normally let the DI container manage the singleton lifetime instead of manually implementing Instance.
19. Singleton in ASP.NET Core
This is particularly important for your Web API work.
ASP.NET Core provides Singleton lifetime through DI:
builder.Services.AddSingleton<AppCounter>();
Then a controller can receive it:
[ApiController]
[Route("api/[controller]")]
public class CounterController : ControllerBase
{
private readonly AppCounter _counter;
public CounterController(AppCounter counter)
{
_counter = counter;
}
[HttpGet]
public IActionResult Get()
{
_counter.Increment();
return Ok(_counter.Count);
}
}
In this case, you do not need to manually write:
private static AppCounter? _instance;
or:
public static AppCounter Instance
ASP.NET Core DI manages the lifetime.
ASP.NET Core DI Container
│
▼
AppCounter Object
▲
┌────┼────┐
│ │ │
Request 1 │ Request 3
│
Request 2
The same registered singleton service instance is reused across requests within that application/service-provider lifetime.
20. Singleton Pattern vs AddSingleton()
These are related, but don't confuse them.
Traditional Singleton Pattern
The class itself controls creation:
public static AppCounter Instance => ...
Client:
AppCounter counter = AppCounter.Instance;
ASP.NET Core DI Singleton
The DI container controls the lifetime:
builder.Services.AddSingleton<AppCounter>();
Client uses constructor injection:
public CounterController(AppCounter counter)
{
_counter = counter;
}
For modern ASP.NET Core applications, DI-managed singleton lifetime is usually preferable because dependencies remain explicit and testing is easier.
21. Singleton vs Scoped vs Transient
Since you're working with ASP.NET Core Web API, this distinction is essential.
builder.Services.AddSingleton<MyService>();
builder.Services.AddScoped<MyService>();
builder.Services.AddTransient<MyService>();
Singleton
One shared instance
↓
Request 1 ──┐
Request 2 ──┼── Same Object
Request 3 ──┘
Scoped
Typically one instance per HTTP request:
Request 1 → Object A
Request 2 → Object B
Request 3 → Object C
Within Request 1, consumers in the same scope can receive the same scoped instance.
Transient
A new instance is generally created each time the service is resolved:
Resolve → Object A
Resolve → Object B
Resolve → Object C
22. Important Warning in Web API
A Singleton service lives much longer than a request.
Therefore, be careful about injecting shorter-lived scoped dependencies into it.
For example, don't directly constructor-inject an EF Core DbContext registered as scoped into a singleton:
public class MySingletonService
{
private readonly AppDbContext _context;
public MySingletonService(AppDbContext context)
{
_context = context;
}
}
With the usual:
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddSingleton<MySingletonService>();
this creates a lifetime mismatch.
Conceptually:
Singleton
│
└── tries to hold Scoped DbContext
A singleton can outlive the request scope that a scoped dependency belongs to.
23. Thread Safety Is Important
Another major point:
Singleton instance creation being thread-safe does not automatically make all of the singleton object's mutable state thread-safe.
For example:
public int Count { get; private set; }
public void Increment()
{
Count++;
}
In a concurrent Web API, many requests may call Increment() simultaneously.
Count++ is not an atomic compound operation.
For a production counter, you could use:
private int _count;
public int Increment()
{
return Interlocked.Increment(ref _count);
}
This is an important practical distinction:
Singleton creation thread safety
≠
Singleton business-state thread safety
24. Advantages
1. Single Instance
Ensures one shared instance according to the singleton's defined lifetime.
2. Shared State
When shared mutable state is genuinely required, different consumers can access the same object.
3. Controlled Object Creation
Traditional Singleton controls construction through:
private AppCounter()
and:
AppCounter.Instance
4. Avoids Repeated Expensive Construction
If an object is expensive to initialize and safe to share, reusing one instance can avoid repeated initialization.
5. Lazy Initialization
With implementations such as:
Lazy<AppCounter>
the object can be created only when first needed.
6. Built-in DI Support in ASP.NET Core
ASP.NET Core provides:
AddSingleton<T>()
so you usually don't need to manually implement the classic Singleton mechanics.
25. Disadvantages
Singleton should not be used simply because "one object sounds efficient."
Potential problems include:
- Global/shared mutable state
- Thread-safety issues
- Hidden dependencies with traditional static access
- Harder unit testing when code directly calls static singleton accessors
- Lifetime problems
- Accidental retention of data for too long
For example:
AppCounter.Instance.Increment();
creates a dependency on a globally accessible object.
Constructor injection:
public MyService(AppCounter counter)
makes the dependency more explicit and easier to replace during testing.
26. When Should We Use Singleton?
Consider Singleton when:
One shared instance is genuinely required
+
Object is safe to share
+
Its dependencies have compatible lifetimes
Good candidates can include stateless or thread-safe shared services.
Be cautious with:
Per-user data
Per-request data
EF Core DbContext
Mutable collections without synchronization
Request-specific information
Those generally should not simply be stored in a singleton.
27. Traditional Singleton Flow
Remember it this way:
Client asks for Instance
↓
Does instance already exist?
↓
┌───┴───┐
│ │
No Yes
│ │
↓ │
Create │
Object │
│ │
└───┬───┘
↓
Return same object
Key Points
- Singleton is a Creational Design Pattern.
- It ensures a class has a single shared instance according to the intended application/runtime scope.
- Traditional Singleton normally uses a private constructor.
- The private constructor prevents normal external
newcreation. - A static field/property provides access to the singleton instance.
Lazy<T>is a clean way to implement lazy, thread-safe initialization in C#.sealedis commonly used to prevent inheritance from complicating the Singleton guarantee.- Singleton is useful only when one shared instance is genuinely appropriate.
- Shared mutable state requires careful thread-safety handling.
- Thread-safe Singleton creation does not automatically make its internal mutable state thread-safe.
- In ASP.NET Core, prefer DI-managed lifetime using
AddSingleton<T>()in most application code. Singleton,Scoped, andTransientare the three commonly used ASP.NET Core DI lifetimes.- Avoid directly injecting scoped services such as a typical EF Core
DbContextinto a Singleton. - Avoid storing request-specific or user-specific mutable data in a Singleton.
- Singleton can reduce repeated construction of expensive, safely shareable objects.
- Overusing Singleton can introduce global state, coupling, testing difficulties, and concurrency problems.
One-line interview answer
Singleton is a creational design pattern that ensures a class has one shared instance and provides a controlled way to access that instance; in ASP.NET Core, this lifetime is usually managed through Dependency Injection using
AddSingleton().
In real-world ASP.NET Core / Web API projects, Singleton is normally used for a service that is expensive to create, stateless or safely shared, and does not contain request/user-specific state.
Common industry scenarios
| Scenario | Singleton? | Example |
|---|---|---|
| Application configuration | ✅ | Settings loaded once and reused |
| In-memory lookup/reference data | ✅ | Country codes, status mappings |
| Thread-safe application cache | ✅ | Shared reference-data cache |
| Metrics/telemetry collector | ✅ | Application-wide counters/metrics |
| Expensive reusable service | ✅ | Parser/rules engine initialized once |
| Feature/configuration provider | ✅ | Shared application configuration |
HttpClient infrastructure |
✅ conceptually | Long-lived handlers managed by IHttpClientFactory |
EF Core DbContext |
❌ | Usually Scoped |
| Current user information | ❌ | Request-specific |
| Shopping cart/session data | ❌ | User-specific |
| Request processing service with mutable state | ❌ | Usually Scoped/Transient |
Practical example: reference-data cache
Suppose your Web API frequently needs a list of countries.
Without caching, every request might do:
GET /api/customer
↓
CustomerService
↓
Database
↓
SELECT * FROM Countries
Thousands of requests could repeatedly retrieve data that rarely changes.
You could create a shared, thread-safe reference-data service:
public class ReferenceDataCache
{
private readonly Dictionary<int, string> _countries = new()
{
{ 1, "India" },
{ 2, "USA" },
{ 3, "UK" }
};
public string? GetCountry(int id)
{
return _countries.GetValueOrDefault(id);
}
}
Register it:
builder.Services.AddSingleton<ReferenceDataCache>();
Inject it:
public class CustomerService
{
private readonly ReferenceDataCache _cache;
public CustomerService(ReferenceDataCache cache)
{
_cache = cache;
}
public string? GetCountry(int countryId)
{
return _cache.GetCountry(countryId);
}
}
ASP.NET Core creates one ReferenceDataCache instance and reuses it:
DI Container
│
▼
ReferenceDataCache
Object #1
▲ ▲ ▲
│ │ │
Request 1 ────────┘ │ │
Request 2 ───────────────┘ │
Request 3 ──────────────────────┘
A very important production rule
Before registering your own service as:
AddSingleton<MyService>();
ask:
Can this same object safely be used simultaneously by many requests?
If yes, Singleton may be appropriate.
If the class contains mutable fields such as:
public string CurrentUser { get; set; }
public int CurrentOrderId { get; set; }
it is generally a bad Singleton.
Imagine:
Request 1 → User = Syed
↘
Same Singleton Object
↗
Request 2 → User = John
Concurrent requests can interfere with shared mutable state.
Another important example: DbContext
Don't do:
builder.Services.AddSingleton<AppDbContext>();
DbContext is designed around a unit-of-work and is not thread-safe. In Web API it is normally registered as Scoped:
builder.Services.AddDbContext<AppDbContext>();
Conceptually:
Request 1 → DbContext #1
Request 2 → DbContext #2
Request 3 → DbContext #3
Also avoid a singleton depending directly on a scoped DbContext:
Singleton Service
↓
Scoped DbContext ❌
because the singleton has a longer lifetime.
What I see most often in ASP.NET Core
For business/application services, Scoped is often more common than Singleton, especially when the service depends on EF Core:
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<ICustomerService, CustomerService>();
builder.Services.AddScoped<IPaymentService, PaymentService>();
Singleton is more appropriate for application-wide infrastructure or immutable/thread-safe shared services:
builder.Services.AddSingleton<IReferenceDataCache, ReferenceDataCache>();
builder.Services.AddSingleton<IMetricsCollector, MetricsCollector>();
builder.Services.AddSingleton<IRuleEngine, RuleEngine>();
Easy rule to remember
Singleton
↓
Application-wide + thread-safe shared service
Scoped
↓
One instance per HTTP request
↓
Business services / DbContext
Transient
↓
New instance each resolution
↓
Small lightweight stateless services
Interview answer: In production ASP.NET Core applications, Singleton is typically used for thread-safe, application-wide services that are safe and useful to reuse across requests, such as shared configuration/reference-data providers, caches, metrics collectors, or expensive reusable stateless components. It should not hold request-specific or user-specific mutable state, and it should not directly depend on scoped services such as DbContext.
ASP.NET Core Singleton Example — Shared Configuration
A practical scenario is an application-wide configuration service containing values such as:
- Application name
- Support email
- Maximum upload size
- Maintenance mode
These values are shared across the application and don't contain per-user/per-request state.
1. appsettings.json
{
"ApplicationSettings": {
"ApplicationName": "My Portfolio API",
"SupportEmail": "support@example.com",
"MaxUploadSizeMB": 10,
"MaintenanceMode": false
}
}
2. Create Configuration Model
public class ApplicationSettings
{
public string ApplicationName { get; set; } = string.Empty;
public string SupportEmail { get; set; } = string.Empty;
public int MaxUploadSizeMB { get; set; }
public bool MaintenanceMode { get; set; }
}
3. Register It as Singleton
In Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var settings = builder.Configuration
.GetSection("ApplicationSettings")
.Get<ApplicationSettings>();
builder.Services.AddSingleton(settings!);
var app = builder.Build();
app.MapControllers();
app.Run();
The important line is:
builder.Services.AddSingleton(settings!);
The configuration is read and converted into an ApplicationSettings object during startup.
That same object is then available throughout the application.
appsettings.json
↓
IConfiguration
↓
ApplicationSettings object
↓
AddSingleton()
↓
ASP.NET Core DI Container
↓
Same object shared with consumers
4. Inject It Into a Controller
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class ConfigurationController : ControllerBase
{
private readonly ApplicationSettings _settings;
public ConfigurationController(
ApplicationSettings settings)
{
_settings = settings;
}
[HttpGet]
public IActionResult Get()
{
return Ok(new
{
_settings.ApplicationName,
_settings.SupportEmail,
_settings.MaxUploadSizeMB,
_settings.MaintenanceMode
});
}
}
ASP.NET Core automatically passes the singleton object into:
public ConfigurationController(
ApplicationSettings settings)
You don't need to manually create it.
5. Test the API
Call:
GET /api/configuration
Response:
{
"applicationName": "My Portfolio API",
"supportEmail": "support@example.com",
"maxUploadSizeMB": 10,
"maintenanceMode": false
}
6. What Happens Internally?
When the application starts:
var settings = builder.Configuration
.GetSection("ApplicationSettings")
.Get<ApplicationSettings>();
ASP.NET Core reads:
"ApplicationSettings": {
"ApplicationName": "My Portfolio API",
"SupportEmail": "support@example.com",
"MaxUploadSizeMB": 10,
"MaintenanceMode": false
}
and creates:
ApplicationSettings Object
--------------------------------
ApplicationName = My Portfolio API
SupportEmail = support@example.com
MaxUploadSizeMB = 10
MaintenanceMode = false
Then:
builder.Services.AddSingleton(settings!);
puts that specific instance into the DI container.
Now imagine three controllers use it:
DI Container
│
▼
ApplicationSettings
Object #1
▲ ▲ ▲
│ │ │
Controller A ┘ │ │
Controller B ──────┘ │
Controller C ────────────┘
All receive the same object.
7. Prove It Is the Same Object
Create two services.
Service A
public class ServiceA
{
private readonly ApplicationSettings _settings;
public ServiceA(ApplicationSettings settings)
{
_settings = settings;
}
public ApplicationSettings GetSettings()
{
return _settings;
}
}
Service B
public class ServiceB
{
private readonly ApplicationSettings _settings;
public ServiceB(ApplicationSettings settings)
{
_settings = settings;
}
public ApplicationSettings GetSettings()
{
return _settings;
}
}
Register them:
builder.Services.AddTransient<ServiceA>();
builder.Services.AddTransient<ServiceB>();
Controller:
[ApiController]
[Route("api/[controller]")]
public class TestController : ControllerBase
{
private readonly ServiceA _serviceA;
private readonly ServiceB _serviceB;
public TestController(
ServiceA serviceA,
ServiceB serviceB)
{
_serviceA = serviceA;
_serviceB = serviceB;
}
[HttpGet]
public IActionResult Get()
{
var settings1 = _serviceA.GetSettings();
var settings2 = _serviceB.GetSettings();
return Ok(new
{
SameObject = ReferenceEquals(settings1, settings2)
});
}
}
Response:
{
"sameObject": true
}
Even though ServiceA and ServiceB are separate objects:
ServiceA ──────┐
│
▼
ApplicationSettings #1
▲
│
ServiceB ──────┘
They received the same singleton configuration object.
8. But There Is an Important Production Detail
For configuration specifically, ASP.NET Core has the Options Pattern, which is usually preferable to manually doing this:
builder.Services.AddSingleton(settings!);
For example:
builder.Services.Configure<ApplicationSettings>(
builder.Configuration.GetSection("ApplicationSettings"));
and then:
public ConfigurationController(
IOptions<ApplicationSettings> options)
{
_settings = options.Value;
}
There are three commonly used options interfaces:
IOptions<T>
IOptionsSnapshot<T>
IOptionsMonitor<T>
They have different configuration reload/lifetime behavior.
So for an interview:
Can configuration be shared using a Singleton?
Yes.
But for normal ASP.NET Core configuration, I would say:
Use the built-in Options Pattern rather than creating a custom Singleton configuration service unless you have a specific reason.
Key point
Custom application-wide shared object
↓
AddSingleton<T>()
Configuration from appsettings.json
↓
Options Pattern
↓
IOptions<T> / IOptionsMonitor<T>
For static configuration that does not need to change while the application is running, the Singleton example above is simple and valid. For configuration that should react to changes, IOptionsMonitor<T> is generally the more appropriate direction.