The Singleton Pattern
Intent: Ensure a class has exactly one instance and provide a global point of access to it.
The problem
Some resources should truly exist only once in an application: a database connection pool, a logging service, a hardware interface, or a configuration manager. Creating multiple instances could waste resources, cause inconsistent state, or break the intended behaviour.
Without Singleton, any part of the code could call new DatabasePool() — there is no enforcement of uniqueness.
Lazy initialisation (naive)
public class DatabasePool {
private static DatabasePool instance;
private DatabasePool() {
// private constructor prevents external instantiation
}
public static DatabasePool getInstance() {
if (instance == null) {
instance = new DatabasePool();
}
return instance;
}
}
This is not thread-safe. Two threads can both see instance == null and both construct a DatabasePool, producing two instances.
Thread-safe implementations
1. Synchronised method
public static synchronized DatabasePool getInstance() {
if (instance == null) {
instance = new DatabasePool();
}
return instance;
}
Simple and correct, but the synchronized incurs a performance penalty on every call to getInstance(), even after the instance is created. Only the first call needs synchronisation.
2. Eager initialisation
public class DatabasePool {
private static final DatabasePool INSTANCE = new DatabasePool();
private DatabasePool() {}
public static DatabasePool getInstance() {
return INSTANCE;
}
}
Thread-safe via Java’s class-loading guarantees (the JVM ensures the static field is initialised exactly once, and class initialisation locks prevent concurrent access). The drawback: the instance is created even if it is never used.
3. Double-checked locking with volatile (canonical thread-safe lazy)
public class DatabasePool {
private static volatile DatabasePool instance;
private DatabasePool() {}
public static DatabasePool getInstance() {
if (instance == null) { // first check (no lock)
synchronized (DatabasePool.class) {
if (instance == null) { // second check (with lock)
instance = new DatabasePool();
}
}
}
return instance;
}
}
The volatile keyword prevents instruction reordering that could cause a partially constructed object to be visible to another thread. After the instance is created, all subsequent calls skip the synchronized block entirely.
4. Enum-based Singleton (Java’s recommended approach)
public enum DatabasePool {
INSTANCE;
public void connect() {
// ...
}
}
Usage: DatabasePool.INSTANCE.connect()
This approach is thread-safe (guaranteed by the JVM), safe against deserialisation attacks (which can otherwise create a second instance), and safe against reflection-based attacks. It is the approach recommended by Joshua Bloch in Effective Java.
When Singleton makes sense
- Truly one-of-a-kind resources: hardware interfaces, device drivers, application-wide configuration.
- Coordination points: a central logging service, a thread pool manager.
- Caches: a single cache instance shared across the application.
Criticisms of Singleton
The Tripos likes to ask you to evaluate Singleton — know both sides.
1. Global mutable state
A Singleton effectively introduces a global variable. Any part of the code can access and mutate it, making the program harder to reason about — changes in one module can silently affect another.
2. Hidden dependencies
A class that calls DatabasePool.getInstance() has a dependency that is not visible in its constructor parameters. You cannot tell by looking at a class’s public API what it depends on. Compare:
// Hidden dependency — invisible to the caller
class ReportGenerator {
void generate() {
DatabasePool.getInstance().query(...);
}
}
// Explicit dependency — visible and injectable
class ReportGenerator {
private final DatabasePool pool;
ReportGenerator(DatabasePool pool) {
this.pool = pool;
}
}
3. Testing difficulty
Because the Singleton is accessed globally, you cannot easily substitute a mock or test double in unit tests. The test is forced to use the real Singleton, which may have side effects (talking to a real database, writing to a real file).
4. God class tendency
Over time, the Singleton tends to attract unrelated responsibilities because it is so easy to access — “I’ll just add this method to the Singleton.” It becomes a dumping ground for miscellaneous functionality, violating SRP.
5. Violates SRP
The Singleton class has two responsibilities: (a) whatever business logic it performs, and (b) managing its own instantiation (ensuring exactly one instance exists).
Tripos approach to Singleton questions
A typical Tripos question presents a scenario and asks something like: “A developer proposes using the Singleton pattern for the database connection. Evaluate this decision.”
Your answer should:
- Identify what Singleton provides: exactly one instance, global access point.
- Assess whether this is needed: Does creating multiple connection pools really cause problems, or could one just create a single instance and pass it via dependency injection?
- Name the concrete drawbacks in the scenario’s context: testing, hidden dependencies, global mutable state.
- Suggest alternatives if appropriate: dependency injection, a factory that always returns the same instance, or simply creating one instance at startup and passing it where needed.
Summary table for Tripos
| Approach | Thread-safe | Lazy | Serialisation-safe | Complexity |
|---|---|---|---|---|
| Naive null-check | No | Yes | No | Low |
| Synchronised method | Yes | Yes | No | Low |
| Eager initialisation | Yes | No | No | Low |
| Double-checked locking | Yes | Yes | No | Medium |
| Enum | Yes | No | Yes | Low |