| Lesson 8 | Singleton Implementation |
| Objective | 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.
A conventional Singleton has three essential elements:
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.
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:
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).
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);
}
}
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.
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.
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:
Avoid Singletons when:
A class with only static data/functions can look “singleton-like,” but it is not equivalent. Static-only designs:
A Singleton still provides a single access point, but it does so through an object with a controllable lifecycle and a cohesive interface.
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