Skip to content
Part IA Michaelmas Term

The Singleton Pattern

Design patterns overview showing Singleton structure

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.

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:

  1. Identify what Singleton provides: exactly one instance, global access point.
  2. 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?
  3. Name the concrete drawbacks in the scenario’s context: testing, hidden dependencies, global mutable state.
  4. 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

ApproachThread-safeLazySerialisation-safeComplexity
Naive null-checkNoYesNoLow
Synchronised methodYesYesNoLow
Eager initialisationYesNoNoLow
Double-checked lockingYesYesNoMedium
EnumYesNoYesLow