Monday, January 13, 2025
Singleton

Updated: January 2025
📝 Brief
Singleton is a creational design pattern 🏁 that ensures that only one instance of a class is created and provides a global 🌎 point of access to that instance.
❓ Problem
The Singleton pattern solves two problems at the same time:
-
🚀 Ensure that class has single instance.
But Why 🤔 might someone want to limit how many instance of a class can be created ? Well, the most common reason for this is to control access to some shared resource—for example, a database or a file 📂.
-
🌎 Provide a global access point to that instance.
Remember, providing a global access point to a Singleton can be very unsafe in concurrent or multi-threaded environments. If its internal state is directly modified or its instance is exposed without proper safeguards, it can lead to race conditions, inconsistent data, or even application crashes.
We will explore 🚀 this in more detail as we move forward in this article 😃.
💡 Solution
Let’s talk about how we can implement the Singleton pattern:
-
🔒 Restricting Direct Instantiation:
First, we need to make sure no one can create instances of our class directly. This means hiding the constructor. Why? Because if anyone could justnew
up an instance, we’d lose control over the “single instance” rule! -
🚪 Providing Controlled Access:
Next, we give users a safe doorway to access the Singleton. A static or global method (often called something likeGetInstance
) will handle this. If the instance doesn’t exist yet, this method creates it. Otherwise, it returns the already created one. -
🧵🔒 Making It Thread-Safe:
In multi-threaded environments, things get tricky. Imagine two threads checking if the instance exists at the same time—boom, we end up with two instances! To prevent this, we’ll add a synchronization mechanism (likesync.Once
in Go) to guarantee that the instance is created only once, no matter how many threads try to access it.
🧬 Blueprint
🛠️ Implementation & Analysis
Note:
-
If you choose aggressive initialization (i.e., initializing directly within the constructor instead of creating a separate method and calling it only when necessary) for every class in your application, it may slow down the initial app startup time.
-
However, this is only possible in languages that support constructors (e.g., C#), not in Golang, which relies on factory functions instead.
-
Private Constructor: Make the class's constructor private to prevent direct instance creation by other objects.
-
Create Storage: Add a private static field to hold the singleton instance.
-
Get Instance Method: Declare a public static method to provide access to the singleton instance. Use lazy initialization in this method, creating the instance only if it doesn’t already exist, ensuring resources are allocated only when needed.
For certain applications, eager initialization in the constructor may be preferred, especially for critical or resource-heavy instances.
-
Update Client Code: Replace any direct instance creation in the codebase with calls to the static GetInstance method.
Lets understand with an example.
In this example, we’ll implement a simple Counter using the Singleton pattern. Our goal is to ensure that only one instance of the Counter exists, and we’ll demonstrate how lazy initialization works—only creating the instance when it’s first needed. You’ll also see how we can manipulate the shared state of the singleton instance and ensure that all parts of the application access and modify the same counter value.
Step 1: Defining the Singleton Instance
In Go, we start by defining a global variable to store the singleton instance. In our example, the singleton will be a Counter
struct.
Here, instance
is a *Counter
pointer. Initially, it’s set to nil
, and we’ll populate it with an actual instance of Counter
when it’s first needed.
Step 2: Lazy Initialization of the Instance
Now, let's define a function to retrieve the singleton instance. The key idea behind the Singleton pattern is lazy initialization, where we only create the instance if it hasn’t been created yet. This is done by checking if instance
is nil
.
- If
instance
isnil
, we initialize it with aCounter
object that starts with a value provided byinitialCounter
. - Once the instance is created, future calls to
GetInstance
will return the same instance, ensuring that no new instances are created.
Step 3: Adding Methods to the Singleton
Let’s add some methods to the Counter
struct to modify and access the internal state.
Increment
: This method increases thecounter
by 1 and prints the updated value.GetCounter
: This simply returns the current value ofcounter
.
With these methods, we can manipulate and access the singleton instance’s state.
Step 4: Using the Singleton in the Main Function
Now that we’ve set up our Singleton, let’s see it in action in the main
function.
What Happens Here?
- First Call: When
GetInstance(10)
is called, it initializes theCounter
with a value of10
and returns the instance. We then callIncrement
, which increases the counter to11
. - Second Call: When
GetInstance(100)
is called, it doesn’t create a new instance. Instead, it returns the existing instance (from the first call). We then callIncrement
again, increasing the counter to12
. - Singleton Check: We print whether
counter1
andcounter2
are the same instance. Since they reference the same object, the output will betrue
. - Final Counter Value: The final counter value will be
12
, confirming that bothcounter1
andcounter2
share the same instance and changes made through one reference affect all.
🧑💻 Putting It All Together
Here’s the complete Singleton implementation:
🧵 Ensuring Thread Safety
Our Singleton implementation worked fine in a single-threaded environment, but what happens when multiple threads enter the picture? Let’s break it down:
-
Instance Creation:
Imagine two threads callingGetInstance
at the exact same time. Both threads check ifinstance
isnil
. Finding it empty, both proceed to create a newCounter
instance. Now, we have two instances. Oops! There goes the Singleton guarantee. -
State Modification:
OurIncrement
andGetCounter
methods directly manipulate and access thecounter
. But what happens if two threads try to increment the counter at the same time? You guessed it—race conditions, inconsistent data, and a whole lot of chaos.
Clearly, we need a fix.
🚧 Step 1: Fixing Instance Creation
To ensure only one Counter
instance is created—no matter how many threads call GetInstance
—we can use sync.Once
.
Think of sync.Once
as a bodyguard for our Singleton. It makes sure that no matter how many threads try to create the instance, only one thread succeeds.
Here’s how it looks:
🔑 What changed?
- The
once.Do
block is executed exactly once, ensuring thatinstance
is created only once, no matter how many threads callGetInstance
.
🚧 Step 2: Fixing State Modification
Now let’s address the second issue: race conditions.
We’ll add a sync.Mutex
to our Counter
struct. This ensures that only one thread can modify or access the counter
at a time.
Here’s the updated code:
🔑 What changed?
- The
mutex.Lock
andmutex.Unlock
calls ensure that each thread gets exclusive access to thecounter
when reading or updating it.
🧑💻 Putting It All Together
Here’s the complete thread-safe Singleton implementation:
🌍 Practical Example
In this section, let’s build a real-world example to better understand the Singleton pattern. Imagine we’re creating a logging system for an application. The log file should be accessible globally, and there should only ever be one instance of the logger managing the log file.
Why is this important?
- Consistency: All parts of the application write to the same log file.
- Efficiency: We avoid the overhead of creating multiple loggers or opening/closing files repeatedly.
- Centralized Control: A single instance ensures global access and synchronized logging in a multi-threaded environment.
We’ll implement this logger step-by-step while ensuring:
- Thread safety using
sync.Once
. - Controlled access to shared resources using
sync.Mutex
.
🛠 Step 1: Define the Singleton Instance
First, we need a structure to represent our logger and a global variable to hold the singleton instance. We also introduce sync.Once
to ensure that the instance is created only once, no matter how many threads call for it.
🛠 Step 2: Create the Singleton
Now, let’s write a function to retrieve the singleton instance. If the instance doesn’t exist, we initialize it and open the log file for writing.
🛠 Step 3: Add Logging Functionality
Next, we add a Log
method to write messages to the log file. To ensure thread safety, we use a sync.Mutex
to lock the operation.
🛠 Step 4: Use the Logger in main
Finally, let’s demonstrate the singleton in action.
What Does This Example Show?
-
Singleton Behavior:
- The
Logger
instance is created only once. - Both
logger
andlogger2
point to the same instance.
- The
-
Thread Safety:
- The
sync.Once
ensures the instance is created only once, even in multi-threaded environments. - The
sync.Mutex
ensures no two threads write to the log file simultaneously.
- The
-
Practicality:
- This pattern ensures that all parts of the application write to the same log file, maintaining consistency and avoiding resource conflicts.
⚖️ Pros & Cons
Pros | Cons |
---|---|
✅ You can be 100% sure there’s only one instance of the class in your app, which keeps things simple. | ❌ It breaks the Single Responsibility Principle—because it’s handling more than just object creation. |
✅ Having a global access point to that instance makes it super easy to interact with whenever needed. | ❌ It might hide some underlying design issues, especially when different parts of your app get too familiar with each other. |
✅ The Singleton object gets created only when it’s actually needed, so you save on resources. | ❌ If you’re working in a multithreaded environment, you'll need to be extra careful to avoid multiple threads creating their own instances. |
❌ Testing is a pain because of the private constructor and static methods, so you might have to get creative to mock it for tests. |
🕒 When to Use
-
Apply the Singleton pattern when you need a single, shared instance of a class throughout your program, like a central database object used by various components.
-
Use the Singleton pattern when you need stricter control over global variables.
You might want to use the Singleton pattern when you need more control over global variables.
The cool thing about Singleton is that it guarantees there’s only one instance of a class in your entire app. Unlike regular global variables, nothing—except the Singleton itself—can create or replace that instance.
🔗 How It’s Related to Other Patterns
-
Facade: Sometimes, a Facade can actually be turned into a Singleton. Why? Because a single facade object is usually enough to handle all the interactions.
So, if you're already using a Facade and only need one instance of it, you might as well make it a Singleton.
-
Flyweight: The Flyweight pattern might remind you of Singleton because it’s all about sharing objects. But here's the twist: while a Singleton only has one instance, Flyweight can have multiple instances, each with different intrinsic states.
So, even though they share some similarities, they serve different purposes. Also, a Singleton object can be mutable, but Flyweight objects are typically designed to be immutable.
🎯 Try Yourself
How would you create a thread-safe Singleton instance of a counter in a multi-threaded environment, allowing the counter to be incremented from different threads without causing any race conditions?