Singleton - Pattern Clarity #7
Ivan Vydrin
Software Engineer | .NET & Azure Professional | AI/ML Enthusiast | Crafting Scalable and Resilient Solutions
The Singleton design pattern ensures that a class has only one instance while providing a global point of access to it. This approach is particularly useful when a centralized management of shared resources is required, ensuring consistency and avoiding the overhead of multiple instantiations.
For most cases, it is better to use the DI approach (if it is supported) by registering the singleton service instead of having a static instance. This approach gives you more flexibility for testing and maintenance, as the DI container can manage its lifecycle and dependencies. That said, it's still crucial to understand the original singleton pattern based on static code.
? Problem
Imagine you're developing a high-frequency trading platform where every transaction must be assigned a unique numeric identifier. Initially, each module of the system creates its own instance of a Unique ID Generator to produce sequential IDs. This design soon shows its flaws:
? Solution
The Singleton pattern offers an elegant remedy. By refactoring the Unique ID Generator into a Singleton, you restrict the class to a single instance, ensuring that all modules rely on one consistent source for generating unique IDs.
Key elements include:
This design centralizes the ID generation mechanism, ensuring that every transaction receives a unique, sequential identifier uniformly across the system. With only one Unique ID Generator in operation, synchronization issues are eliminated, and any updates to the ID generation logic propagate seamlessly across the entire platform.
?? Use Cases
?? Benefits & Drawbacks
Pros:
Cons:
In my experience, Redis works great for fast, centralized Singleton sync across application instances. But when you need real-time, two-way communication - like instant updates between services - a pub-sub approach is better. Last time I used web-sockets (which can also be implemented with other message brokers), it provided the flexibility needed for instant event notifications across distributed application instances.
?? How to implement
By embracing the Singleton design pattern, you can streamline critical aspects of your system architecture, ensuring that shared resources like logging, configuration, or connection pooling are handled efficiently and coherently. This design pattern is a powerful tool in the software engineer's toolkit, promoting both resource efficiency and system-wide consistency.
?? Handling concurrency
When using a static class as a Singleton, concurrency issues can arise, especially in multi-threaded environments. Since all threads share the same instance, simultaneous access might lead to race conditions or inconsistent states. There are multiple ways to handle it:
1. Using lock
public sealed class Singleton
{
private static Singleton _instance = null;
private static readonly object _lock = new object();
Singleton()
{ }
public static Singleton Instance
{
get
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
}
}
This implementation is thread-safe because it uses a dedicated, private lock to check whether the instance exists. This ensures that once one thread creates the instance, no other thread can do so simultaneously, and it maintains proper memory ordering. The downside is that the lock is acquired every time the instance is requested, which may slow things down.
?? I prefer locking on a private static variable rather than on the type itself. Locking on public objects can lead to performance issues or deadlocks. Using a dedicated lock keeps the code safer and simpler for writing thread-safe applications.
?? I would use this approach when there's a chance that the initial instantiation might fail at runtime and can be retried on subsequent access. For other scenarios, modern C# offers more elegant and efficient alternatives.
2. Using readonly
public sealed class Singleton
{
private static readonly Singleton _instance = new Singleton();
static Singleton() { }
private Singleton() { }
public static Singleton Instance => _instance;
}
This version is very simple yet thread-safe. The static readonly field ensures the instance is created only once, and the static constructor makes sure it's initialized when any static member is accessed. While it isn't as lazy as other methods (since any static member access triggers instance creation), it avoids the overhead of locks and keeps the code clean.
3. Using Lazy<T>
public sealed class Singleton
{
private static readonly Lazy<Singleton> lazy =
new Lazy<Singleton>(() => new Singleton());
public static Singleton Instance => lazy.Value;
private Singleton() { }
}
If you're using .NET 4 or later, you can simplify lazy instantiation with System.Lazy<T>. Just pass a lambda that creates your Singleton, and Lazy<T> handles the rest - thread safety included.
This approach is simple and efficient. You can even check if the instance is created using lazy.IsValueCreated. By default, it uses LazyThreadSafetyMode.ExecutionAndPublication, though you can experiment with other modes if needed.
However, if your application requires immediate instance availability or benefits from the simplicity of static initialization, a static readonly instance might be a better fit. Evaluate your specific needs to choose the most appropriate instantiation method.
?? Handling for scaled applications
While the Singleton pattern works well within a single process, scaling applications across multiple processes or machines introduces additional challenges:
?? Code Example
The practical example of implementing the Singleton pattern in C#.
public sealed class UniqueIdGenerator
{
// Lazy initialization ensures thread safety and lazy instantiation.
private static readonly Lazy<UniqueIdGenerator> _lazyInstance =
new Lazy<UniqueIdGenerator>(() => new UniqueIdGenerator());
// Global access point to the UniqueIdGenerator instance.
public static UniqueIdGenerator GetInstance() => _lazyInstance.Value;
// Internal counter for generating unique IDs.
// (!) The Singleton class should not have the state for distributed systems:
// better use a database or a distributed cache or state for that.
private int _lastId;
// Private constructor prevents external instantiation.
private UniqueIdGenerator()
{
_lastId = 0;
}
// Thread-safe method to get the next unique ID.
public int GetNextId()
{
return Interlocked.Increment(ref _lastId);
}
}
class Program
{
static void Main(string[] args)
{
// Access the singleton instance and use its method.
UniqueIdGenerator generator = UniqueIdGenerator.GetInstance();
Console.WriteLine("*first access to generator instance*");
int id = generator.GetNextId();
Console.WriteLine($"The next unique ID is: {id}");
// Access the same singleton instance.
UniqueIdGenerator generator2 = UniqueIdGenerator.GetInstance();
Console.WriteLine("*second access to generator instance*");
int id2 = generator2.GetNextId();
Console.WriteLine($"The next unique ID is: {id2}");
}
}
Console output:
*first access to generator instance*
The next unique ID is: 1
*second access to generator instance*
The next unique ID is: 2
Conclusion
While the Singleton pattern is a powerful tool for managing shared resources, it's important not to overuse it. That can lead to hidden dependencies and conflicts with SOLID principles - especially the Single Responsibility and Dependency Inversion principles. In many cases, using dependency injection to manage a singleton service provides greater flexibility and testability. Use Singletons judiciously and only when a truly global, single instance is essential.
My code examples capture the essence of the pattern, but they can be further improved with industry-standard practices. Incorporate robust error handling and observability - such as logging during instantiation - to aid in monitoring and troubleshooting in production environments.
Thank you for taking the time to read this article! "Pattern Clarity" is a weekly newsletter where I explore design patterns and share practical insights about them. I'd love to see your thoughts - feel free to share your comments, contribute your ideas, and leave feedback. Your input is always appreciated!
Entrepreneur | Founder @ XANT & Monoversity | Senior Software Enginer | Full Stack AI/ML Engineer | Engineering Intelligent SaaS & Scalable Software Solutions
3 周Great advice