Hello! Welcome to your next lesson on design patterns.
In our previous lesson, we explored the Factory Method and Abstract Factory patterns. Their primary goal was to make your system more flexible by decoupling client code from the specific classes of objects it creates. Today, we're looking at a creational pattern with the opposite intent: the Singleton pattern, which is all about restricting instantiation.
Your goal for this lesson is to implement the Singleton pattern and evaluate its use cases and drawbacks. This is a critical lesson for developing an architectural mindset, as the Singleton is one of the most well-known, yet also one of the most controversial, design patterns. Understanding not just how to use it, but when (and when not to) is a mark of a mature software designer.
1. What is the Singleton Pattern?
The Singleton pattern's intent is straightforward: ensure a class has only one instance and provide a single, global point of access to it.
Imagine you have a class that manages application-wide configuration settings. It's crucial that all parts of your application read from and write to the same configuration object. If multiple instances were created, you could have conflicting settings and unpredictable behavior. The same logic applies to resources that are inherently singular, like a connection to a specific hardware device or a system-wide logger.

To understand the problems the Singleton aims to solve, let's turn to a great guide on the topic.
The article 'Singleton' on Refactoring.guru clearly lays out the two main problems this pattern addresses. Understanding these problems is key to grasping its purpose.
Please read the 'Intent' and 'Problem' sections. Pay attention to how Singleton is presented as a solution for both controlling access to a shared resource and providing a more robust alternative to a global variable.
2. How to Implement the Singleton
The implementation of a Singleton relies on a few key class design choices that work together to enforce the single-instance rule.
There are three essential components:
- A private constructor: This prevents external code from using the
newoperator to create instances of the class directly. - A private, static field: This field will hold the one and only instance of the class.
- A public, static creation method (commonly named
getInstance()): This method acts as the sole gateway. The first time it's called, it creates the new instance and stores it in the static field. On every subsequent call, it simply returns the existing instance.
This structure is visualized in the following diagram.
Now, let's see how this structure translates into code. Since you work with various languages, we'll look at a resource that provides examples in C#, Java, and Python. Focus on the pattern itself, which is consistent across languages.
What Is a Singleton? A Detailed Overview
The article 'What Is a Singleton? A Detailed Overview' provides a clear, step-by-step walkthrough of building a Singleton. It also shows implementations in several popular languages.
Read the sections 'A More Detailed Look at the Singleton Design Pattern' and 'How Is a Singleton Implemented in Different Programming Languages?'. Observe how the basic implementation is first built with a private constructor and a static property, and then refined with lazy initialization and thread-safety considerations in different languages.
3. A Critical Consideration: Thread Safety
The simple, lazy-initialization approach we've seen has a major flaw in a multi-threaded environment. If two threads call getInstance() at nearly the same time when the instance is still null, both might pass the if (instance == null) check. This race condition results in two different instances being created, completely violating the pattern's core principle.
Solving this is non-negotiable for any production-ready Singleton.
What is the Singleton Pattern? (Software Design Patterns)
This video from Be A Better Dev does an excellent job of illustrating this multi-threading problem and demonstrating a common solution.
Watch from 05:10 to 09:57. The video walks through the race condition scenario and then shows how to fix it using a 'synchronized' block in Java. The concept of locking to ensure atomic creation is applicable in any language. The IDE walkthrough at the end visually confirms that only one instance is created.
The solution shown in the video (using a synchronized keyword or a lock) ensures that only one thread can execute the instance creation block at a time. This is often referred to as thread-safe lazy initialization. The Python example in the stackify.com article you read earlier demonstrates a similar technique known as double-checked locking.
Test your understanding!
Write the pseudocode for a thread-safe Logger Singleton class. Include the private static instance, the private constructor, and the public static getInstance() method. Make sure the creation logic is protected against race conditions.
Show answer
Here is one possible pseudocode implementation:
class Logger
// The private static field to hold the single instance.
// 'volatile' is often used to ensure changes are visible across threads.
private static volatile Logger instance = null
// A lock object for synchronization.
private static object lock = new object()
// The private constructor.
private Logger() {
// Initialization logic for the logger
}
// The public static method to get the instance.
public static Logger getInstance() {
// First check (avoids locking on every call)
if (instance == null) {
// Acquire a lock to ensure only one thread can create the instance.
lock (lock) {
// Second check (ensures instance isn't created by another thread
// that was waiting for the lock).
if (instance == null) {
instance = new Logger()
}
}
}
return instance
}
public void log(message) {
// write message to log file...
}
end class
This pattern is the "double-checked locking" idiom, which is efficient because it only incurs the cost of locking during the initial creation.
4. The Great Debate: Use Cases vs. Drawbacks
So far, the Singleton seems like a useful tool. However, it is heavily criticized and often labeled an anti-pattern. To develop your architectural judgment, you must understand both sides of the argument.
The video 'Singleton - Good or Bad?' provides a balanced and critical look at the pattern. It covers valid use cases but spends significant time on the serious drawbacks.
Watch from 03:01 to 06:31. First, you'll see common use cases like logging and caching. Then, pay close attention to the four major disadvantages discussed: data inconsistency in multi-threaded environments, hidden dependencies, untestability, and violations of SOLID principles (Single Responsibility and Dependency Inversion).
Let's summarize the key arguments against the Singleton:
- Violates the Single Responsibility Principle (SRP): A Singleton class is responsible for its own business logic and for managing its own lifecycle (creation and access). This conflates two separate concerns.
- Creates Hidden Dependencies and Tight Coupling: When a class calls
MySingleton.getInstance()inside one of its methods, it creates a hard-coded, static dependency on theMySingletonclass. This dependency isn't visible in the class's constructor or public interface. This makes the code harder to understand, maintain, and test because you can't easily swap out the Singleton for a different implementation or a mock object. - Acts like a Global Variable: While providing a single access point, Singletons can be used as a global "bucket" to store state, leading to unpredictable interactions between disparate parts of an application. This makes debugging difficult.
- Hinders Unit Testing: Because of the tight coupling, it's very difficult to write unit tests for code that uses a Singleton. You can't easily provide a "mock" or "fake" version of the Singleton for a test (e.g., a mock logger that writes to memory instead of a file). Modern test frameworks rely on dependency injection to provide these test doubles, a practice that Singletons actively work against.
5. A Pragmatic Guideline for Usage
Given the severe drawbacks, when is it ever acceptable to use a Singleton? The consensus among many experienced architects is "rarely." Often, a regular object whose lifecycle is managed by a Dependency Injection (DI) container and configured to have a "singleton scope" is a much better solution. This provides a single instance without the drawbacks of static global access and tight coupling.
However, a simple rule of thumb can help you decide.
Let's conclude with a pragmatic rule for when a Singleton might be a valid choice, from the same critical video.
Watch the final section from 07:58 to 09:13. The video proposes a clear ground rule: use a Singleton only if you need exactly one instance and the creation of a second instance would be actively dangerous or nonsensical. If you're just using it for convenience, there's a better way.
This is the key takeaway for your architectural mindset: is the Singleton pattern solving a true constraint of the problem domain (e.g., a single physical print spooler), or is it just being used as a convenient shortcut for passing a shared object around? If it's the latter, prefer explicit dependency injection.
Test your understanding!
For each of the following scenarios, would you use the Singleton pattern? Justify your answer based on its drawbacks and the "dangerous to have more than one" rule.
- A
UserProfileobject that holds the currently logged-in user's data. - An object that manages a connection pool to a database.
- A class that manages access to the physical file system of the operating system.
Show answer
-
UserProfileobject: No. While you only have one logged-in user per session, using a Singleton for this is a classic example of using it as a convenient global variable. It creates tight coupling—any part of the app can suddenly depend on the user's state. It's much better to pass theUserProfileobject explicitly (via dependency injection) to the components that need it. This makes dependencies clear and the code easier to test. -
Database Connection Pool: Probably Not. While you want a single, shared pool, a Singleton is not the best way to achieve this. This is a perfect use case for a Dependency Injection container. The container can create the pool object once and then "inject" that same instance into every service that needs a database connection. This gives you all the benefits of a single instance without the drawbacks of global state and tight coupling.
-
File System Access: A plausible candidate. The operating system itself only presents one file system. Having multiple objects that could potentially hold conflicting state about file handles or permissions could be considered "dangerous." In some very low-level, foundational libraries, a Singleton might be used to represent this truly singular, global resource. However, even here, wrapping it in an interface and using dependency injection is often a cleaner, more testable design.
Conclusion
You've now implemented the Singleton and, more importantly, critically evaluated its place in modern software design. While simple on the surface, its implications for testability and maintainability are profound.
Key Takeaways:
- The Singleton pattern guarantees one instance of a class and provides global access to it.
- Implementation requires a private constructor, a private static instance, and a public static
getInstance()method. - Thread safety is a critical, non-negotiable requirement for any production Singleton, typically solved with locking.
- The pattern is often considered an anti-pattern because it violates the Single Responsibility Principle, creates hidden dependencies, hinders unit testing, and acts as a global state.
- A better alternative for managing shared services is usually a normal class managed by a Dependency Injection container.
- Only consider a Singleton when having more than one instance would be genuinely dangerous or logically incorrect, not just for convenience.
Preview of the Next Lesson:
We are now finished with our tour of the most common creational patterns. In the next lesson, we will move on to Structural Patterns, which deal with how classes and objects are composed to form larger, more flexible structures. We will start by implementing two highly useful patterns: Adapter and Facade.