Static Class vs Singleton in C#
Both a static class and a singleton give you "one shared thing" for the whole application, so they are often confused. But they are very different:
staticis a C# keyword. A static class has no objects at all: you call its members through the class name.- Singleton is a design pattern. A singleton class has exactly one object, and everybody uses that same object.
That one difference, "no object" vs "one object", explains everything else: interfaces, inheritance, testing, dependency injection and lazy loading. All outputs below come from running the code on .NET 8.
Static class: functions without an object
A static class is a container for members that don't need any per-object data, like a toolbox of helper functions. A GST calculator is a good example: the result depends only on the input.
public static class TaxCalculator
{
private const decimal GstRate = 0.18m;
public static decimal Gst(decimal amount) => amount * GstRate;
public static decimal WithGst(decimal amount) => amount + Gst(amount);
}
Console.WriteLine($"GST on 1000: {TaxCalculator.Gst(1000m)}");
Console.WriteLine($"Total with GST: {TaxCalculator.WithGst(1000m)}");
Rules of a static class:
- All its members must be static.
- You can't create an object of it (
new TaxCalculator()is a compile error). - It can't inherit from another class (except
object), can't be inherited, and can't implement interfaces. - It can have a static constructor, which runs automatically once, before the class is first used.
.NET is full of static classes: Math, Console, File, Convert, Path.
Singleton: exactly one object
A singleton is a normal class designed so that only one object can ever be created, and that object is shared. It suits something that has state or is expensive to create, such as application settings loaded from a file, or a connection to a cache server.
public sealed class AppSettings
{
// Lazy<T> creates the object on first use, and is thread-safe by default
private static readonly Lazy<AppSettings> _instance = new(() => new AppSettings());
public static AppSettings Instance => _instance.Value;
public string SupportEmail { get; }
// Private constructor: no one outside can call "new AppSettings()"
private AppSettings()
{
Console.WriteLine("AppSettings created (loading configuration...)");
SupportEmail = "contactus@makemycodes.com";
}
}
Console.WriteLine("Before first use");
var a = AppSettings.Instance;
var b = AppSettings.Instance;
Console.WriteLine($"Same object? {ReferenceEquals(a, b)}");
Console.WriteLine($"Support email: {a.SupportEmail}");
The three ingredients of every singleton:
- A private constructor, so no other code can create objects.
- A private static field holding the single instance.
- A public static property (
Instance) that returns it.
Notice that "AppSettings created" was printed only once, and only when Instance was first used, not when the program started. This is lazy initialisation: if the app never needs the settings, it never pays the cost of loading them. The class is also sealed, so nobody can create a subclass and get a second instance that way.
Is it thread-safe?
A naive singleton (if (_instance == null) _instance = new ...) can create two objects when two threads check at the same moment. Lazy<T> prevents that. To prove it, here 100 threads ask for the instance at the same time, and the constructor deliberately takes 50 ms:
var instances = new ConcurrentBag<ThreadSafeCache>();
Parallel.For(0, 100, _ => instances.Add(ThreadSafeCache.Instance));
Console.WriteLine($"Distinct instances: {instances.Distinct().Count()}, " +
$"constructor calls: {ThreadSafeCache.ConstructorCalls}");
The modern way: a singleton through dependency injection
In ASP.NET Core you rarely write the pattern by hand. Instead you write a normal class with an interface, and let the DI container make sure only one object exists:
public interface IExchangeRateService
{
decimal Rate(string currency);
}
public class ExchangeRateService : IExchangeRateService
{
public ExchangeRateService() => Console.WriteLine("ExchangeRateService created");
public decimal Rate(string currency) => currency == "USD" ? 88.25m : 0m; // sample rate
}
// Program.cs
builder.Services.AddSingleton<IExchangeRateService, ExchangeRateService>();
Asking the container twice gives the same object:
This combines the best of both worlds: one shared object, but no static Instance property hard-wired into the code. Any class can receive IExchangeRateService through its constructor, and a unit test can pass a fake instead:
public class FakeRates : IExchangeRateService
{
public decimal Rate(string currency) => 1m;
}
PrintRate(new FakeRates(), "USD"); // Test double rate: 1
That's impossible with a static class: code that calls TaxCalculator.Gst(...) directly can't be given a different implementation in a test. See AddTransient vs AddScoped vs AddSingleton and the Singleton pattern lesson for more.
Correcting two common myths
- "Static classes are stored on the stack." Not true. The stack only holds local variables of running methods. Static fields belong to the type and live on the heap for the whole lifetime of the application. The singleton object also lives on the heap, referenced by a static field. The real difference isn't where memory lives, but that a static class has no instance, while a singleton has one.
- "You can clone a singleton." You shouldn't. A singleton that can be cloned is no longer a singleton. Don't implement
ICloneableon it, and keep the constructor private.
Comparison table
| Point | Static class | Singleton |
|---|---|---|
| What it is | A language keyword | A design pattern |
| Number of objects | None | Exactly one |
| Members | Only static members | Instance members (plus the static Instance) |
| Constructor | Only a static constructor | Private instance constructor |
| Implement interfaces | No | Yes |
| Inheritance | Can't inherit or be inherited | Can inherit from a class; usually sealed itself |
| Pass as a parameter | No (there's no object) | Yes, like any object |
| Initialisation | Before first use (static constructor) | Can be lazy (Lazy<T>) |
| Dependency injection | Not possible | Yes (AddSingleton) |
| Unit testing | Hard to replace with a fake | Easy when used through an interface |
| Can implement IDisposable | No | Yes |
| Best for | Stateless helpers: math, formatting, extension methods | Shared state or expensive resources: settings, caches, clients |
Which one should I use?
- Static class: pure helper functions with no state and no need to swap the implementation, such as
TaxCalculator, string helpers or extension methods. - Singleton via DI (
AddSingleton): in ASP.NET Core, for shared services such as configuration, caches or HTTP clients. This is the preferred approach in modern applications. - Hand-written singleton (
Lazy<T>): in apps without a DI container, when you really need exactly one object.
Be careful with shared state. Whether it's a static field or a singleton, one object is used by all requests and threads at the same time. Never store per-user data in it, and make any changeable data thread-safe.
Interview questions
1. What is the difference between a static class and a singleton?
A static class is a keyword-based container with no instances and only static members. A singleton is a design pattern where a class has exactly one instance, created through a private constructor and exposed by a static property. A singleton can implement interfaces, be passed around, be lazily created and be injected through DI; a static class can't.
2. How do you make a singleton thread-safe?
Use Lazy<T> (thread-safe by default), a static readonly field initialised inline, or double-checked locking. In ASP.NET Core, register the class with AddSingleton and let the container handle it.
3. Why is a singleton class usually sealed?
So nobody can inherit from it and create additional instances through a subclass, which would break the "only one object" rule.
4. Can a static class implement an interface?
No. Interfaces describe instance members, and a static class has no instances.
5. Which is better for unit testing?
A singleton used through an interface (ideally registered with DI), because tests can supply a fake implementation. Calls to static methods are fixed at compile time and can't be replaced.
Summary
- Static class: no objects, only static members, no interfaces. Ideal for stateless helpers.
- Singleton: exactly one object, with a private constructor and a static
Instance. - Use
Lazy<T>for a lazy, thread-safe singleton; 100 threads still created only one object. - In ASP.NET Core, prefer
AddSingletonwith an interface: one object, and still testable. - Static data doesn't live on the stack, and a singleton should never be cloneable.