Design Patterns «Prev Next»

Lesson 8 Singleton Implementation
Objective Write a Class that uses the Singleton Pattern.

Write a Class that Uses the Singleton Pattern

This lesson focuses on implementing a Singleton correctly in modern code. The goal is not simply “make it global,” but to guarantee a single, well-controlled instance and expose it through a stable access point (for example, getInstance() / Instance), while staying thread-safe and testable.

In practice, the Singleton’s implementation is highly language-dependent. Modern languages and runtimes offer safer primitives (for example, lazy initialization helpers and guaranteed thread-safe static initialization) that eliminate older locking-heavy or error-prone patterns.

Singleton Structure

A conventional Singleton has three essential elements:

  1. Non-public constructor so callers cannot freely instantiate the class.
  2. One stored instance held by the class (or guaranteed by the runtime).
  3. One access point (a static method/property) that returns the single instance.

A common legacy signature you will see is public static Singleton getInstance(). The name varies, but the responsibilities remain the same: create once (if needed) and always return the same instance.

C# Implementation

In modern C#, the most straightforward approach is Lazy<T>, which provides thread-safe, lazy initialization without manual locking. This implementation is compact and robust.

using System;

public sealed class AppConfig
{
    private static readonly Lazy<AppConfig> _instance =
        new Lazy<AppConfig>(() => new AppConfig());

    // Private constructor prevents external instantiation.
    private AppConfig()
    {
        // Load configuration, initialize defaults, etc.
    }

    public static AppConfig Instance => _instance.Value;

    public string GetSetting(string key)
    {
        // Example behavior (replace with real config retrieval)
        return key switch
        {
            "Theme" => "Dark",
            _ => ""
        };
    }
}

Notes:

  • Sealed reduces accidental subclassing that could complicate “single instance” assumptions.
  • Lazy<T> is thread-safe by default and defers creation until first use.
  • If your singleton holds resources (file handles, network connections), ensure you manage lifecycle explicitly.

Java Implementation

In Java, two modern implementations are widely used: (1) the Initialization-on-demand holder idiom (lazy, thread-safe, simple), and (2) enum Singleton (excellent for serialization safety).

Option A: Initialization-on-demand holder idiom

public final class LoggerService {

    private LoggerService() { }

    public static LoggerService getInstance() {
        return Holder.INSTANCE;
    }

    private static class Holder {
        private static final LoggerService INSTANCE = new LoggerService();
    }

    public void log(String message) {
        System.out.println(message);
    }
}

Option B: Enum Singleton

public enum SystemClock {
    INSTANCE;

    public long nowMillis() {
        return System.currentTimeMillis();
    }
}

Prefer the holder idiom when you want a “normal class” API. Prefer an enum singleton when you want maximal correctness around serialization and reflection edge-cases and can accept the enum style.

C++ Implementation

In modern C++ (C++11+), function-local static initialization is thread-safe and avoids many pitfalls associated with global initialization order. This is the most common production-safe baseline (“Meyers’ Singleton”).

#include <string>

class Metrics {
public:
    static Metrics& instance() {
        static Metrics inst;     // Thread-safe initialization in C++11+
        return inst;
    }

    void record(const std::string& name) {
        // Record a metric (placeholder)
        last_ = name;
    }

    std::string last() const { return last_; }

    Metrics(const Metrics&) = delete;
    Metrics& operator=(const Metrics&) = delete;

private:
    Metrics() = default;
    ~Metrics() = default;

    std::string last_;
};

If you require explicit, one-time initialization with arguments, consider std::call_once + std::once_flag, but keep the public API narrow and avoid turning Singleton into a general-purpose dependency bucket.

Testing and Maintainability

The most common modern criticism of Singleton is not “it can’t be implemented,” but that it can introduce hidden dependencies and shared state that makes tests brittle. Mitigate that with one of these approaches:

  • Prefer dependency injection (DI) for most services; reserve Singleton for truly unique infrastructure concerns.
  • If you keep a Singleton, keep it small: a narrow interface, minimal mutable state, and no unrelated responsibilities.
  • For test suites, consider wrapping the singleton behind an interface so you can substitute a fake implementation.

When You Should Not Use a Singleton

Avoid Singletons when:

  • You primarily need “shared access” rather than “exactly one instance” (DI or a scoped service is often better).
  • You need multiple instances in different contexts (for example, per-tenant, per-request, per-test, per-environment).
  • You are modeling resources that should be pooled (for example, database connections). Use a pool manager, not one connection.

Static Members Are Not the Same as a Singleton

A class with only static data/functions can look “singleton-like,” but it is not equivalent. Static-only designs:

  • Do not support polymorphism well (static functions cannot be virtual).
  • Make lifecycle management (init/cleanup) harder to centralize and test.
  • Encourage implicit global state without a clear access boundary.

A Singleton still provides a single access point, but it does so through an object with a controllable lifecycle and a cohesive interface.

Next Step

The best way to internalize implementation details is to write and test one yourself. Use the exercise to implement a singleton, verify it returns the same instance, and consider thread-safety and testability tradeoffs.

Singleton Pattern - Exercise

SEMrush Software 8 SEMrush Banner 8