Skip to main content
CleanCodeMastery

Singleton Pattern: One Principal for the Whole School

Understand the Singleton design pattern with a school principal story, simple TypeScript and C# code, thread safety, and why many seniors call it an anti-pattern.

27 min read Updated June 11, 2026beginner
design-patternscreational-patternssingletonglobal-statethread-safetydependency-injectiontypescriptcsharp

๐ŸŽฏ One school, one principal

Welcome to Green Valley School. It has hundreds of students, dozens of teachers, many classrooms, two playgrounds โ€” but how many principals?

Exactly one. Her name is Principal Meena Madam.

There is one Principal Madam, one principal's office on the first floor, and one official notice board outside that office, polished daily by Raju bhaiya, the school peon. When the school needs an important decision โ€” exam dates, holiday announcements, permission for the annual function โ€” everyone goes to that same office. Anita Madam (maths) goes there. Vikram Sir (sports) goes there. Even little Pinky from Class 6A goes there when she wants permission for the science club. Nobody says, "Let me quickly appoint my own second principal for my class." Two principals announcing two different exam dates? Total chaos. Impossible!

Notice three things about how Green Valley manages this:

  1. You cannot create a principal yourself. There is no shop where Pinky can "buy" a new principal. The post is protected.
  2. There is one well-known way to reach the principal. Everyone knows where the office is. You do not need someone to personally introduce you.
  3. Whoever asks, gets the same principal. Class 6A and Class 10B both reach the very same person, the very same notice board.

This is the Singleton pattern in real life. One special object for the whole program, creation strictly controlled, and a single famous "door" through which everyone reaches it.

The same idea sits on the roof of Pinky's apartment building, Shanti Heights: one water tank serves every flat. Flat 101 and flat 504 open different taps, but the water comes from the same tank. If flat 302 wastes water, flat 101 feels it. One electricity meter per house (two meters would double-count your units!), one captain for a cricket team, one water tank per building โ€” some things are correct only when there is exactly one.

Here is a normal Tuesday at Green Valley, as a journey:

Figure 1: One office, many visitors, same principal

Hold on to this school story. We will write the principal's office in code soon. And later โ€” this is important โ€” we will also discuss honestly why many senior developers say, "Use this pattern very, very carefully."

๐Ÿ’ก What is the Singleton pattern?

The Singleton pattern is a creational design pattern. Here is its plain definition:

Singleton guarantees that a class has only one instance during the whole life of the program, and it provides a single global access point (usually a static method like getInstance()) to reach that instance.

So the class takes on two jobs at once:

  • It controls its own creation. The constructor is hidden (made private), so no outside code can write new Principal(). The class creates its one instance itself โ€” just like the school board, not the students, appoints Meena Madam.
  • It provides access. A static method or property hands out that one instance to anyone who asks, from anywhere in the program โ€” the famous office door on the first floor.
๐Ÿ’ก

One-line memory trick: "One post, one person, one office door." Singleton = private constructor + static instance + public getInstance().

Singleton is one of the 23 classic patterns from the Gang of Four book (1994). It is the easiest pattern to learn and, funnily, also the most criticised pattern ever โ€” we will see both sides today.

College corner: notice that Singleton quietly violates the Single Responsibility Principle by design. The class manages its own lifetime (job 1) and does its real work (job 2). That double duty is the seed of most later complaints: lifetime management belongs to the application's composition root or a container, not to the class itself. Keep this thought โ€” it returns in the anti-pattern section.

โš ๏ธ The problem it solves

Why would anyone need such a strange class? Two real needs push developers here.

Need 1: Some things must exist only once.

Imagine a program where every part creates its own database connection pool:

// The dangerous "everyone makes their own" way
class ReportService {
  private pool = new ConnectionPool(); // pool #1 (100 connections)
}
 
class BillingService {
  private pool = new ConnectionPool(); // pool #2 (another 100!)
}
 
class EmailService {
  private pool = new ConnectionPool(); // pool #3 (!!)
}
// The database planned for 100 connections... now faces 300.
// It starts rejecting connections. The app crashes at peak time.

A plain constructor cannot say "no" to a second call. Every new gives a fresh object. But for some resources โ€” the log file, the parsed configuration, the connection pool, the cache โ€” a second copy is not just wasteful, it is wrong. Two log objects writing to one file can mix their lines. Two settings objects can disagree with each other. Two principals can announce two different exam dates. Two water tanks on one roof with one inlet pipe? The plumber will laugh at you.

Need 2: That one thing must be reachable from many places.

The logger is needed in almost every file. Passing it by hand through fifty layers of constructors feels tiring. A global variable would be reachable โ€” but globals are fragile: any code can overwrite them, and nothing guarantees they are initialised before first use.

Singleton tries to answer both needs in one stroke: hide the constructor (so only one instance can ever exist) and expose a static access door (so everyone can reach it).

Figure 2: Without Singleton, every caller creates its own copy; with Singleton, all callers share one

How big is the waste, really? Look at the open connections when every service builds its own heavy pool versus everyone sharing one:

Figure 3: Resource cost when 10 services each need the connection pool

Ten services, each opening a 100-connection pool, means one thousand connections hammering a database sized for one hundred. The shared pool keeps it at exactly one hundred. This chart is the whole business case for "exactly one instance".

๐Ÿ› ๏ธ How it works, step by step

The recipe is small. You can write it in any language in five steps:

  1. Make the constructor private. This locks the front door. Outside code can no longer write new Principal() โ€” the compiler itself will refuse, like the school board rejecting Pinky's application to appoint her own principal.
  2. Add a private static field inside the class to hold the single instance. It starts empty (null).
  3. Add a public static method โ€” traditionally named getInstance() โ€” that everyone will call.
  4. Inside getInstance(), create lazily. On the very first call, the instance does not exist yet, so create it and store it in the static field. On every later call, just return the stored one. "Lazy" means created only when first needed, not at program start.
  5. Make it thread-safe if needed. If two threads call getInstance() at the exact same moment when the instance is still empty, both can pass the "is it null?" check and create two instances โ€” breaking the whole promise! Languages give safe tools for this (we will see C#'s Lazy<T> below).
Figure 4: Class structure of the Singleton pattern

Note the funny self-arrow: the class holds an instance of itself in a static field. That is the signature of Singleton.

Watch what happens when two teachers visit, one after the other:

Figure 5: First call creates, second call reuses

The whole lifetime of a singleton has only three interesting moments โ€” not created yet, created on first demand, and then the same instance returned forever:

Figure 6: Lifecycle of the single instance

Look at the loop on SameInstanceReturned in Figure 6 โ€” that loop is the entire promise of the pattern. After the first call, nothing new is ever created. And notice there is no arrow back to NotCreated: a classic singleton, once born, lives until the program dies. Remember this โ€” it is also why tests struggle with it later.

๐Ÿงช Real-life code example: the principal's office in TypeScript

Let us code Green Valley School. Follow the comments.

// The one and only principal's office.
class PrincipalOffice {
  // Step 2: the private static field holding the single instance.
  // It is empty until someone first needs the office.
  private static instance: PrincipalOffice | null = null;
 
  // The official notice board (the office's state).
  private notices: string[] = [];
 
  // Step 1: PRIVATE constructor. Nobody outside can call `new`.
  private constructor() {
    console.log("๐Ÿซ Principal's office is being set up (runs only ONCE)...");
  }
 
  // Step 3 + 4: the public static door, with LAZY creation.
  public static getInstance(): PrincipalOffice {
    if (PrincipalOffice.instance === null) {
      // First visitor ever โ€” set up the office now.
      PrincipalOffice.instance = new PrincipalOffice();
    }
    // Every visitor, first or later, gets the SAME office.
    return PrincipalOffice.instance;
  }
 
  public announce(notice: string): void {
    this.notices.push(notice);
    console.log(`๐Ÿ“ข New notice: ${notice}`);
  }
 
  public readNotices(): string[] {
    return [...this.notices];
  }
}
 
// ---------- Client code: a normal school day ----------
 
// Anita Madam visits the office.
const officeForMaths = PrincipalOffice.getInstance();
officeForMaths.announce("Maths exam on Monday, 9 AM.");
 
// Vikram Sir visits "another" office... or does he?
const officeForSports = PrincipalOffice.getInstance();
officeForSports.announce("Annual sports day on 26 January.");
 
// Are these two different offices? Let's check!
console.log("Same office?", officeForMaths === officeForSports);
 
// Pinky reads the board โ€” sees BOTH notices,
// because there is only ONE board in ONE office.
console.log("Notice board:", officeForSports.readNotices());
 
// And if someone naughty tries:  new PrincipalOffice();
// โŒ TypeScript error: Constructor of class 'PrincipalOffice' is private.

Expected output:

๐Ÿซ Principal's office is being set up (runs only ONCE)...
๐Ÿ“ข New notice: Maths exam on Monday, 9 AM.
๐Ÿ“ข New notice: Annual sports day on 26 January.
Same office? true
Notice board: [ 'Maths exam on Monday, 9 AM.', 'Annual sports day on 26 January.' ]

Read the output carefully โ€” it proves all three promises:

  1. The setup message printed only once, even though getInstance() was called twice. That is lazy creation: built on the first call, reused afterwards.
  2. officeForMaths === officeForSports is true โ€” both variables point to the same object in memory.
  3. The notice from Anita Madam is visible to Vikram Sir and to Pinky, because everyone shares one notice board.

โš ๏ธ What about two visitors at the exact same moment?

JavaScript and TypeScript normally run your code on a single thread, so two calls cannot truly collide โ€” our simple if (instance === null) check is safe there.

But in languages like C#, Java, or C++, many threads run at once. Picture this race:

Thread A: checks instance โ€” it is null      โ”€โ”
Thread B: checks instance โ€” it is also null โ”€โ”ค both passed the check!
Thread A: creates office #1                  โ”‚
Thread B: creates office #2                  โ”‚ โ† TWO principals now! ๐Ÿ˜ฑ

Two offices, two notice boards, total confusion โ€” exactly what Singleton promised to prevent. The classic fixes are:

  • Eager creation: build the instance at class-load time, before any thread can race. Simple and safe, but you lose laziness (it is built even if never used).
  • Lock every call: take a lock before checking. Safe, but every call pays the locking cost forever.
  • Double-checked locking: check first without the lock, lock only if still empty, check again inside. Fast โ€” but famously easy to get subtly wrong without memory barriers.

College corner โ€” why double-checked locking burned experts: the danger is not the logic, it is the memory model. The line instance = new PrincipalOffice() is not one atomic action; the compiler or CPU may reorder it into (1) allocate memory, (2) publish the pointer, (3) run the constructor. A second thread can then observe a non-null instance whose constructor has not finished โ€” and happily use a half-built object. Java fixed this only when volatile got stronger semantics in the Java 5 memory model (JSR-133); C# needs volatile or careful use of Interlocked for the same reason. The safe modern idioms push the problem onto the runtime: C#'s Lazy<T> (which handles safe publication internally), Java's class-holder idiom (the JVM guarantees class initialisation is both lazy and thread-safe), Java's enum singleton, Go's sync.Once, Rust's OnceLock. The lesson generalises beyond Singleton: never hand-roll one-time initialisation from memory; use the language's blessed primitive.

The modern advice in one line: do not hand-roll this. Which brings us to C#.

๐Ÿงช The same idea in C# โ€” thread-safe with Lazy<T>

C# gives a beautiful one-liner for safe lazy singletons. Lazy<T> guarantees the factory runs exactly once, even if a hundred threads ask at the same instant.

public sealed class PrincipalOffice
{
    // Lazy<T> creates the instance on FIRST access,
    // and .NET guarantees thread safety for us. No locks to write!
    private static readonly Lazy<PrincipalOffice> _lazy =
        new(() => new PrincipalOffice());
 
    public static PrincipalOffice Instance => _lazy.Value;
 
    private readonly List<string> _notices = new();
 
    // Private constructor: outside code cannot use `new`.
    private PrincipalOffice()
    {
        Console.WriteLine("Principal's office set up (only once).");
    }
 
    public void Announce(string notice) => _notices.Add(notice);
 
    public IReadOnlyList<string> ReadNotices() => _notices;
}
 
// Usage โ€” from anywhere, on any thread:
PrincipalOffice.Instance.Announce("PTM on Saturday at 10 AM.");
 
var a = PrincipalOffice.Instance;
var b = PrincipalOffice.Instance;
Console.WriteLine(ReferenceEquals(a, b)); // True โ€” the same office

sealed stops anyone from subclassing the singleton (a subclass could sneak in a second instance). static readonly plus Lazy<T> handles the entire thread-safety puzzle in one line. This is the idiomatic modern C# singleton.

And for completeness, the Python flavour. In Python, the most honest singleton is simply a module โ€” Python imports every module exactly once and caches it, so module-level objects are natural singletons:

# water_tank.py โ€” the module IS the singleton.
# Python runs this file only once, no matter how many files import it.
 
class _WaterTank:
    def __init__(self):
        self.level = 1000  # litres
 
    def use(self, litres: int) -> None:
        self.level -= litres
 
    def refill(self, litres: int) -> None:
        self.level += litres
 
# The one tank on the roof of Shanti Heights:
tank = _WaterTank()
 
# Any other file:
#   from water_tank import tank
#   tank.use(50)
# Everyone gets the same tank object โ€” guaranteed by the import system.

No private constructor gymnastics needed. The import system already gives "create once, share everywhere". (Python folks also discuss the Borg/monostate idea in faif/python-patterns โ€” many objects sharing one state โ€” a fun cousin with the same dangers.)

๐Ÿ’ก Where you see it in real software

Singleton (and "singleton-like" single instances) appear all over real systems:

  • Application configuration. A program parses its settings file once and shares the result everywhere. Parsing twice wastes time; two disagreeing configs would be a bug.
  • Loggers. Logging frameworks like Java's Log4j or Python's logging module hand you the same logger object for the same name every time โ€” one shared writer per log destination, so log lines do not get mixed up.
  • Database connection pools. The pool is sized to the database's limits, so there must be exactly one. Creating pools per-class is the classic outage story (remember Figure 3).
  • Spring Framework beans (Java). Here is the interesting twist: in Spring, singleton is the default scope for every bean โ€” one instance per container โ€” but the container manages it and injects it into your classes. You get single-instance behaviour without writing getInstance() anywhere. This is widely considered the right modern way (more on this below).
  • ASP.NET Core services. Same idea: services.AddSingleton<MyService>() registers a class with singleton lifetime in the DI container. One instance, but delivered through constructors, not global statics.
  • Runtime objects. Java's Runtime.getRuntime() is a textbook Singleton shipped inside the JDK itself.
  • Learning repositories. The iluwatar/java-design-patterns repo shows five Java variants (eager, lazy, double-checked, holder idiom, enum).

Which of these are truly unique by nature? In a typical backend codebase, the honest split looks something like this:

Figure 7: What single-instance objects usually are, in a typical backend

That last slice is the painful one โ€” objects that someone made a singleton only for convenience. The next two sections are about exactly that slice.

๐Ÿ“Š When to use it and when not to

SituationUse Singleton?
A truly unique, process-wide resource (one log file, one parsed config, one connection pool)โœ… Maybe โ€” but prefer DI with singleton lifetime (see below)
Small script or throwaway tool where wiring DI is overkillโœ… Yes โ€” pragmatic and fine
Mostly read-only, set-once values needed everywhereโœ… Acceptable โ€” immutable singletons cause far less trouble
"Passing this object around feels boring"โŒ No โ€” laziness is not a design reason; hidden access will cost you later
The class holds mutable state used by many modulesโŒ Dangerous โ€” invisible coupling between far-away parts of code
You will write unit tests for code using this classโŒ Avoid classic Singleton โ€” tests cannot swap in a fake easily
You "might need more than one someday" (one per tenant, one per region)โŒ No โ€” Singleton hard-codes "exactly one" and is painful to undo
Big, long-lived team projectโŒ Prefer dependency injection with singleton-scoped registration

Want it as a picture? Judge your case on two axes: how truly unique the resource is by nature, and how much its state changes:

Figure 8: Where Singleton is defensible

A shopping cart as a singleton (bottom-left) is the classic disaster: every user would share one cart. A parsed, read-only config (top-right) is about as safe as singletons get.

โš ๏ธ Why many seniors call Singleton an anti-pattern

Now the honest part. Ask any experienced developer about Singleton and you will see a careful frown. Singleton is the only GoF pattern that is routinely called an anti-pattern โ€” a solution that looks helpful but quietly creates bigger problems. Important: the criticism is not against the idea of "one instance" (that is sometimes genuinely required โ€” one water tank really is correct for one building). It is against the technique: a class giving global static access to itself.

Here are the four big complaints, in simple words:

1. It is global state in a fancy coat. For decades, programmers learned to avoid global variables โ€” anything can read or change them from anywhere, so the program becomes impossible to reason about locally. A mutable singleton is exactly a global variable wearing a class costume. Change it in module A, and module Z mysteriously behaves differently. Debugging such "spooky action at a distance" eats whole evenings. In Shanti Heights language: if anyone in any flat can secretly open the tank's outlet valve, no single flat can ever explain why the water pressure dropped.

2. It hides dependencies. Look at this function:

function generateReportCard(student: Student): ReportCard {
  // ...looks like it depends only on `student`...
  const config = SchoolConfig.getInstance();  // surprise dependency!
  const logger = Logger.getInstance();        // another surprise!
  // ...
}

The signature says "give me a student, I give you a report card." It lies. The function secretly reaches out to two singletons. A reader cannot know the true dependencies without reading every line of the body. Constructors and parameters stop being honest contracts.

3. It makes testing painful. Good unit tests want a fresh, isolated world for every test. But a singleton is shared static state that survives between tests โ€” remember Figure 6: there is no arrow back to NotCreated. Test 1 posts a notice; test 2 unexpectedly sees it; tests pass alone but fail together, in confusing order-dependent ways. Worse: you usually cannot substitute a fake. If OrderService internally calls PaymentGateway.getInstance(), then testing "place order" hits the real payment gateway. Ouch.

4. Thread-safety is easy to get wrong. As we saw, hand-rolled lazy creation has a race condition, and double-checked locking trapped even expert programmers for years. And after creation, the singleton's own mutable state still needs its own synchronisation โ€” one notice board scribbled on by fifty threads is still a mess, even if there is only one board.

๐Ÿšจ

The honest summary: classic Singleton = global mutable state + hidden dependencies + hard testing. Use it knowingly, in small doses, in small programs. In serious projects, reach for dependency injection first. If a senior reviews your code and sees getInstance() sprinkled everywhere, expect questions โ€” and they will be fair questions.

So what should you reach for instead? Here is the menu, from simplest to most powerful:

AlternativeHow it worksWhen it fits
Manual dependency injectionCreate the one instance in main(), pass it through constructorsSmall and medium apps; always a safe default
DI container with singleton lifetimeRegister once (AddSingleton, Spring bean); the container injects it everywhereTeam projects, web backends โ€” the modern standard
Module-level instance (Python, JS/TS modules)The module system itself guarantees one shared instance per processScripts and apps in module-based languages
Plain function parameterJust pass the object to the few functions that need itWhen only two or three call sites need it โ€” simplest of all
Monostate / BorgMany instances, one shared stateRarely; mostly a curiosity โ€” it shares Singleton's downsides
Honest global constantAn exported immutable valueRead-only config-like data; immutability removes most danger

๐Ÿ’ก The better way: dependency injection

Here is the wonderful news โ€” you can keep the benefit ("only one logger, one config, one pool") and drop almost all the costs. The recipe is called dependency injection (DI), and it is simpler than its scary name:

  1. Create exactly one instance near the program's starting point (the main function โ€” often called the composition root).
  2. Pass it into the classes that need it, through their constructors.
// At startup โ€” created ONCE, like appointing one principal:
const config = new SchoolConfig("settings.json");
const logger = new Logger(config);
const office = new PrincipalOffice(logger);
 
// Dependencies are now EXPLICIT and visible in constructors:
const examService = new ExamService(office, logger);
const sportsService = new SportsService(office);

There is still only one PrincipalOffice โ€” but now:

  • ExamService declares openly what it needs. No lies in the signature.
  • In a test, you can hand it a FakePrincipalOffice in one line. Testing becomes easy.
  • No global static state exists at all.

The school board (your main function) appoints Meena Madam once and introduces her to every department. No teacher conjures a principal out of thin air, yet everyone works with the same one.

College corner โ€” "singleton" becomes a lifetime, not a pattern: DI containers (Spring, ASP.NET Core, Angular's injector, NestJS) turn "how many instances exist" into configuration. In ASP.NET Core, AddSingleton, AddScoped (one per web request), and AddTransient (new every time) are just three lifetime choices for the very same class. The class itself stays plain โ€” public constructor, explicit dependencies, fully testable โ€” and the container enforces uniqueness at the composition level. This separation (class logic vs lifetime policy) is the textbook fix for the SRP violation we flagged in the first College corner. One caution from real projects: a singleton-scoped service must never hold per-user or per-request state, and it must be thread-safe, because all requests share it. The pattern's old dangers do not vanish; they just move into a smaller, well-labelled box.

โš ๏ธ Common mistakes students make

โš ๏ธ

Mistake 1: Using Singleton just to avoid passing parameters. "Calling getInstance() is easier than passing the object" is the most common trap. The short-term ease becomes long-term hidden coupling. If a class needs something, say so in its constructor.

โš ๏ธ

Mistake 2: Forgetting the race condition in multi-threaded languages. A plain "if instance is null, create it" is fine in single-threaded JavaScript, but in C#, Java, or C++ two threads can both pass the check and create two instances. Use Lazy<T>, sync.Once, the holder idiom, or eager initialisation โ€” never hand-rolled double-checked locking from memory.

โš ๏ธ

Mistake 3: Confusing Singleton with a static class. A static class is just a bundle of functions: it cannot implement an interface, cannot be passed as a parameter, cannot be swapped. A singleton is a real object โ€” it can do all of those. If you find yourself making everything static "for convenience", pause and rethink.

โš ๏ธ

Mistake 4: Stuffing the singleton with unrelated jobs. Because the singleton is reachable from everywhere, it slowly becomes the dumping ground: config + cache + counters + helper functions, all in one giant class. This violates the Single Responsibility Principle twice over โ€” the class already does two jobs (its real work + managing its own lifetime), do not add a third and fourth.

๐Ÿ“Š Compare with cousins

Singleton sits among the creational patterns, but its goal is the opposite of most of them โ€” others help you create many objects flexibly; Singleton stops you from creating more than one!

PatternMain question it answersHow many instances?Key difference from Singleton
Singleton"How do I guarantee only ONE instance?"Exactly oneโ€”
Prototype"How do I get another one like this?"As many copies as you wantOpposite goal: Prototype multiplies, Singleton restricts
Factory Method"Which subclass decides what gets created?"ManyCreates new objects each call; can return a singleton if it wants
Abstract Factory"How do I create families of related objects?"ManyThe factory object itself is often implemented as a Singleton
Builder"How do I assemble a complex object step by step?"ManyBuilds fresh complex objects; nothing to do with uniqueness
Flyweight"How do I share many small immutable objects to save memory?"Many shared, deduplicatedFlyweights are many immutable shared objects; Singleton is one (often mutable) object

One more relative worth naming: Facade. A facade object (a simple front-door to a complex subsystem) is very often made a Singleton, because one front door per subsystem is enough.

To revise the whole topic in one glance, here is Singleton as a mind map:

Figure 9: Singleton at a glance

๐ŸŽฏ Quick revision box

+--------------------------------------------------------------+
|                 SINGLETON PATTERN โ€” REVISION                 |
+--------------------------------------------------------------+
| Idea       : Exactly ONE instance + one global access door   |
| Story      : One principal, one office, one notice board     |
|              (and one water tank for the whole building)     |
|                                                              |
| Recipe     : 1. private constructor  (lock the front door)   |
|              2. private static instance field                |
|              3. public static getInstance()                  |
|              4. lazy creation on first call                  |
|              5. thread-safety (Lazy<T> / sync.Once / eager)  |
|                                                              |
| Proof      : getInstance() twice -> SAME object (a === b)    |
| Lifecycle  : NotCreated -> Created -> SameInstanceReturned   |
|                                                              |
| Real world : config, logger, connection pool,                |
|              Spring beans (DI-managed!), Runtime.getRuntime  |
|                                                              |
| โš ๏ธ Honest warning โ€” why seniors frown:                       |
|   - global mutable state in disguise                         |
|   - hidden dependencies (signatures lie)                     |
|   - tests cannot swap a fake; state leaks between tests      |
|   - thread-safe lazy init is easy to get wrong               |
|                                                              |
| Better way : dependency injection โ€” create once at startup,  |
|              PASS it in; DI container singleton scope        |
+--------------------------------------------------------------+

๐Ÿงช Practice exercise

Three tasks, from easy to thought-provoking โ€” Pinky-level to Meena-Madam-level. Try them before peeking back!

  1. The water tank. Write a WaterTank singleton in TypeScript with a private constructor, a getInstance() method, and a level (litres) starting at 1000. Add use(litres) and refill(litres) methods. From three different "flats" (three variables obtained via getInstance()), use some water, then print the level from a fourth variable. It must show the combined effect โ€” proving everyone shares the one tank on the roof.

  2. Break it, then fix it. Take your WaterTank and pretend two threads call getInstance() at the same time when the instance is null. Write down, step by step on paper, how two tanks could get created. Then write the C# version using Lazy<WaterTank> and explain in two sentences why the race disappears. Bonus for college students: explain why a naive double-checked lock could still hand out a half-constructed tank without proper memory barriers.

  3. The DI makeover (the most important one). Write a BillingService that first uses WaterTank.getInstance() internally. Then refactor: remove getInstance() from inside the service and instead accept the tank through the constructor โ€” new BillingService(tank). Finally, write a tiny FakeWaterTank and show how the refactored version lets you test BillingService without touching the real tank. Feel the difference โ€” that feeling is why seniors prefer dependency injection.

If task 3 made sense to you, congratulations โ€” you now understand not only the Singleton pattern but also its biggest criticism and its modern replacement. That is more than many working programmers can say. Shabash, keep going!

Frequently asked questions

What is the Singleton pattern in one line?
Singleton is a creational pattern that makes sure a class has exactly one instance in the whole program, and gives everyone a single well-known way to reach it, usually a static getInstance() method.
How does Singleton stop people from creating a second object?
The class makes its constructor private, so outside code cannot call new. The class itself creates the one instance and hands it out through a static method or property.
Why do many developers call Singleton an anti-pattern?
Because it creates hidden global state. Dependencies become invisible, unit tests cannot easily replace the singleton with a fake, and state leaks between tests. Dependency injection gives the same one-instance benefit without these problems.
What is lazy initialization in Singleton?
Lazy means the instance is created only on the first call to getInstance(), not at program start. It saves startup time, but in multi-threaded programs you must guard the creation so two threads do not create two instances.
Is Singleton the same as a static class?
No. A static class is just a bag of functions and can never be passed around or implement an interface. A singleton is a real object, so it can implement interfaces, be passed as a parameter, and (in theory) be swapped for another implementation.
What should I use instead of Singleton in big projects?
Create one instance at the start of the program and pass it into the classes that need it (dependency injection). DI containers can even register a class with a singleton lifetime, so you get one instance without global static access.

Further reading

Related Lessons