Singleton - Pattern Clarity #7
Singleton - Creational Pattern

Singleton - Pattern Clarity #7

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:

  • Inconsistent ID generation: different instances of the generator can produce duplicate or out-of-sequence IDs, undermining the integrity of transaction records.
  • Resource overuse: multiple generators maintaining separate counters lead to unnecessary resource consumption and complicate synchronization.
  • Synchronization challenges: without a centralized mechanism, ensuring that every generated ID is unique becomes complex, increasing the risk of conflicts and errors.
  • Fragmented tracking: scattered ID generators make it difficult to maintain a coherent, system-wide order for transaction identifiers.

Problem: Multiple Unique ID generators.

? 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:

  • Private constructor: prevents external instantiation.
  • Static instance holder: maintains the sole instance of the Unique ID Generator.
  • Global access method: provides a static method (commonly called Instance or getInstance()) that returns the single instance, creating it only when necessary through lazy initialization.

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.

Solution: Singleton Unique ID generator.

?? Use Cases

  • Unique ID generation: when every component needs a unique identifier, a Singleton ensures all IDs come from one centralized, sequential source - eliminating duplicates.
  • Configuration management: a Singleton configuration manager provides a single, authoritative point of access for global settings, ensuring consistency and simplifying maintenance.
  • Centralized event dispatching: in event-driven systems, a Singleton event bus guarantees that notifications are managed uniformly, preventing fragmented event streams.
  • License or Feature Toggle Management: a Singleton license manager or feature toggle controller offers consistent enforcement across the application, ensuring that changes are applied uniformly.

?? Benefits & Drawbacks

Pros:

  • Controlled access: guarantees that only one instance exists, reducing the risk of conflicting behaviors.
  • Resource efficiency: prevents unnecessary instantiation, which can save memory and processing power.
  • Simplified management: offers a global access point, making it easier to maintain and update the shared resource.

Cons:

  • Hidden dependencies: overuse of a Singleton can lead to tight coupling, making unit testing and maintenance more challenging.
  • Concurrency challenges: in multi-threaded applications, improper Singleton implementations can lead to race conditions unless thread safety is ensured.
  • Reduced flexibility: once a class is designed as a Singleton, modifying its behavior or extending its functionality may require significant rework.
  • Distributed systems problem: the Singleton is not really single across the system for scaled applications. Additional actions are needed to handle it (distributed cache / state / pub-sub approach).

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

  1. Create a static holder: define a private static variable within your class to hold the sole instance.
  2. Provide a public accessor: implement a public static method that returns this instance.
  3. Lazy initialization: implement the mechanism to create the instance when it's needed only.
  4. Private constructor: make the constructor private so that the class cannot be instantiated from outside.
  5. Refactor client code: update all parts of your code that previously created new instances to use the static accessor method instead.

Singleton class diagram.
Singleton class diagram.

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.

?? Lazy<T> Class (System) | Microsoft Learn

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:

  • In-Memory limitation: a local Singleton is confined to a single process's memory space. In distributed systems, each process might create its own instance, defeating the purpose of having a single, global instance.
  • Distributed coordination: for truly global singletons in a scaled environment, consider using distributed caches, databases, or coordination services. These can help maintain a single logical instance across multiple nodes.

?? GitHub Redis

?? GitHub Consul

  • Dependency injection (DI): when using DI frameworks, the Singleton scope is also usually limited to the application's process. Ensure that your architecture accounts for this when deploying to environments with multiple instances (like microservices or cloud applications).
  • State management: centralize state management externally when needed. By offloading critical shared state to an external system, you can maintain consistency without relying solely on an in-memory Singleton by having actually one instance per each machine.


?? 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!


Elliot One

Entrepreneur | Founder @ XANT & Monoversity | Senior Software Enginer | Full Stack AI/ML Engineer | Engineering Intelligent SaaS & Scalable Software Solutions

3 周

Great advice

要查看或添加评论,请登录

Ivan Vydrin的更多文章

  • Prototype - Pattern Clarity #10

    Prototype - Pattern Clarity #10

    The Prototype design pattern allows you to create new objects by cloning existing instances, avoiding the overhead of…

  • Types of Machine Learning

    Types of Machine Learning

    Machine Learning (ML) is a subset of Artificial Intelligence (AI) that enables computer systems to learn from data and…

    2 条评论
  • Chain of Responsibility - Pattern Clarity #9

    Chain of Responsibility - Pattern Clarity #9

    The Chain of Responsibility design pattern allows you to pass a request through a series of handlers (objects), where…

  • Thread Safety in .NET: lock, Semaphore and Mutex

    Thread Safety in .NET: lock, Semaphore and Mutex

    Thread Safety matters: in multi-threaded .NET applications, multiple threads often access shared data concurrently.

    2 条评论
  • Vector Databases in AI/ML: the next-gen infrastructure for intelligent search

    Vector Databases in AI/ML: the next-gen infrastructure for intelligent search

    Traditional databases struggle to handle AI-generated data like images, text, and audio embeddings. These…

    1 条评论
  • State - Pattern Clarity #8

    State - Pattern Clarity #8

    The State design pattern allows an object to alter its behavior when its internal state changes, making the object…

    4 条评论
  • Agentic AI: the rise of autonomous agents

    Agentic AI: the rise of autonomous agents

    Artificial Intelligence is evolving from simple tools into agentic systems that can act with autonomy and purpose…

    6 条评论
  • Exploring Types of Chatbots: an AI/ML perspective

    Exploring Types of Chatbots: an AI/ML perspective

    Chatbots have become an essential part of modern digital interactions, transforming customer service, sales, education,…

  • Observer - Pattern Clarity #6

    Observer - Pattern Clarity #6

    The Observer behavioral pattern establishes a one-to-many dependency between objects, so that when one object (the…

    4 条评论
  • Let's build a Free Web Chat Bot - Part 2/2

    Let's build a Free Web Chat Bot - Part 2/2

    In today's fast-moving digital world, smart and intuitive conversations give businesses a real edge. Azure AI Services…

社区洞察

其他会员也浏览了