Skip to content

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:

  • static is 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)}");
Output
GST on 1000: 180.00
Total with GST: 1180.00

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}");
Output
Before first use
AppSettings created (loading configuration...)
Same object? True
Support email: contactus@makemycodes.com

The three ingredients of every singleton:

  1. A private constructor, so no other code can create objects.
  2. A private static field holding the single instance.
  3. 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}");
Output
Distinct instances: 1, constructor calls: 1

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:

Output
ExchangeRateService created
Same object? True
USD to INR: 88.25

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 ICloneable on it, and keep the constructor private.

Comparison table

PointStatic classSingleton
What it isA language keywordA design pattern
Number of objectsNoneExactly one
MembersOnly static membersInstance members (plus the static Instance)
ConstructorOnly a static constructorPrivate instance constructor
Implement interfacesNoYes
InheritanceCan't inherit or be inheritedCan inherit from a class; usually sealed itself
Pass as a parameterNo (there's no object)Yes, like any object
InitialisationBefore first use (static constructor)Can be lazy (Lazy<T>)
Dependency injectionNot possibleYes (AddSingleton)
Unit testingHard to replace with a fakeEasy when used through an interface
Can implement IDisposableNoYes
Best forStateless helpers: math, formatting, extension methodsShared 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 AddSingleton with an interface: one object, and still testable.
  • Static data doesn't live on the stack, and a singleton should never be cloneable.