We open the catalog with the pattern everybody knows, the one that fits in ten lines... and the only one that much of the profession considers an anti-pattern in most of its uses today. Singleton is the course's best two-for-one lesson: first you will learn to implement it properly — which in Java has more to it than it seems, thanks to concurrency — and then you will learn why you should think twice before using it. In PideYa we will study it through a plausible case: the platform's global configuration.

Contents

  1. Intent and problem in PideYa
  2. Structure of the pattern
  3. Classic lazy implementation (and its bug)
  4. Solutions to the concurrency problem
  5. Variants compared
  6. Why it is the most criticized pattern
  7. The alternative: dependency injection
  8. When to use it and when not to
  9. Common mistakes, exercises, and conclusion

Intent and problem in PideYa

Intent (GoF): ensure a class has only one instance and provide a global point of access to it.

Notice that the intent has two halves — uniqueness and global access — and it pays to judge them separately: the first is often legitimate; the second is the source of nearly all the trouble.

The case in PideYa: the platform has a global configuration — restaurant commission percentage, maximum delivery radius, payment gateway URL, maintenance mode — loaded from a file at startup. In the previous lesson we saw the symptom: every service did new PideYaConfig() and loaded its own copy of the file. Real consequences:

  • Repeated cost: the file gets parsed once per service (and it would be worse if the source were a remote database).
  • Inconsistency: the operations team turned on maintenance mode and only the reloaded copies saw it; two services kept accepting orders.

We want one configuration to exist, shared by everyone. The naive solution — a global variable — does not exist as such in Java, and even if it did, it would not stop anyone from still calling new. Singleton solves both things by making the class itself control its instantiation.

Structure of the pattern

classDiagram
    class Singleton {
        -static instance : Singleton
        -Singleton()
        +static getInstance() Singleton
        +operation()
    }
    Singleton --> Singleton : creates and stores\nits single instance

Three ingredients, all three essential:

Element Role
Private constructor Nobody outside the class can call new: the front door is locked
Private static field holding the instance The class stores itself; it is the "vault" of uniqueness
Public static method getInstance() The single access point; creates the instance on first use and reuses it afterwards

Classic lazy implementation (and its bug)

The textbook version, with lazy initialization (lazy: the instance is not created until someone asks for it for the first time):

public class PideYaConfig {

    private static PideYaConfig instance;   // (1) null until first use

    private final Properties values;

    private PideYaConfig() {                // (2) private constructor
        this.values = loadFromFile("pideya.properties");
    }

    public static PideYaConfig getInstance() {  // (3) access point
        if (instance == null) {                 // (4) first time?
            instance = new PideYaConfig();      // (5) create and remember
        }
        return instance;
    }

    public BigDecimal getRestaurantCommission() {
        return new BigDecimal(values.getProperty("restaurant.commission"));
    }

    public int getDeliveryRadiusKm() {
        return Integer.parseInt(values.getProperty("delivery.radius.km"));
    }

    private Properties loadFromFile(String path) { /* ... */ return new Properties(); }
}

Step-by-step explanation:

  • (1) The static field starts out as null. Being static, it belongs to the class, not to any object: there is exactly one per JVM (per class loader, to be precise).
  • (2) The private constructor locks the door on external new. It is what turns "a convention" into "a guarantee".
  • (4)-(5) The first call creates the instance; subsequent calls return the one already created. The cost of loading the file is paid once, and only if someone ever needs the configuration.

Usage from anywhere in PideYa:

BigDecimal commission = PideYaConfig.getInstance().getRestaurantCommission();

The bug: this version is correct in a single-threaded program... and PideYa, like any web server, handles many concurrent requests. If two threads enter getInstance() at the same time while instance is still null, both pass the if at step (4) and each one creates its own instance. Result: two live configurations, exactly the problem we came to solve, except now intermittent and nearly impossible to reproduce. This is the canonical example of a race condition applied to creation; the general study of concurrency is left for the concurrency patterns — here we only need to solve this specific case.

Solutions to the concurrency problem

Option A: synchronized on the method

public static synchronized PideYaConfig getInstance() {
    if (instance == null) {
        instance = new PideYaConfig();
    }
    return instance;
}

synchronized guarantees that only one thread executes the method at a time: correct and simple. Its drawback is that the lock is paid on every call, forever, when it was only needed on the first. In a class queried thousands of times per second, it is an unnecessary toll (although in practice, with modern JVMs, smaller than the classic literature suggests).

Option B: double-checked locking with volatile

The historical attempt to pay for the lock only the first time:

public class PideYaConfig {

    private static volatile PideYaConfig instance;  // volatile is MANDATORY!

    private PideYaConfig() { /* ... */ }

    public static PideYaConfig getInstance() {
        if (instance == null) {                     // 1st check: no lock
            synchronized (PideYaConfig.class) {
                if (instance == null) {             // 2nd check: with lock
                    instance = new PideYaConfig();
                }
            }
        }
        return instance;
    }
}
  • The first check avoids the lock in 99.99% of calls (the instance already exists).
  • The second check, now inside the synchronized block, covers the case where two threads passed the first one simultaneously: the second to enter finds the instance already created and does not duplicate it.
  • volatile is not optional: without it, the JVM may reorder the object's construction so that another thread sees the reference assigned before the constructor has finished, receiving a half-built object. This mistake was so common that double-checked locking was broken in Java until the Java 5 memory model gave volatile its current guarantees.

It works, but look at how much subtlety just to initialize an object. That is why the next two idioms are preferable.

Option C: holder idiom (the recommended one in classic Java)

public class PideYaConfig {

    private PideYaConfig() { /* ... */ }

    private static class Holder {                 // inner class: not loaded
        static final PideYaConfig INSTANCE =      // until someone uses it
                new PideYaConfig();
    }

    public static PideYaConfig getInstance() {
        return Holder.INSTANCE;                   // triggers loading of Holder
    }
}

The move is elegant: the JVM guarantees that class initialization is atomic and thread-safe (the class loader itself ensures it), and that a static inner class is not loaded until its first use. Combining both facts we get lazy + thread safety without writing a single line of synchronization: the JVM does the dirty work. It is the recommended idiom when you truly want lazy.

Option D: enum singleton (the Effective Java one)

public enum EventLog {

    INSTANCE;

    private final List<String> events = new CopyOnWriteArrayList<>();

    public void record(String event) {
        events.add(Instant.now() + " " + event);
    }
}

// Usage from anywhere in PideYa:
EventLog.INSTANCE.record("Order 4412 confirmed");

Here we show it with PideYa's second classic case: a simple event/logging registry. A single-value enum is, technically, the most robust singleton possible in Java: the JVM guarantees uniqueness, it is thread-safe, and it also withstands two attacks that break all the previous ones: serialization (deserializing a regular singleton creates a second instance unless you implement readResolve()) and reflection (with setAccessible(true) a private constructor can be invoked; with enums, the JVM forbids it). Joshua Bloch recommends it in Effective Java as "the best way to implement a singleton". Its limits: it cannot extend another class, it is not lazy in the strict sense (it is created when the enum is loaded), and many teams find it stylistically jarring.

Variants compared

Variant Lazy? Thread-safe? Complexity Notes
Classic lazy Yes No Minimal Only valid in single-threaded contexts; on a server, a latent bug
Eager (direct static final) No Yes Minimal If the instance is cheap and always used, perfectly respectable
synchronized Yes Yes Low Lock toll on every call
Double-checked + volatile Yes Yes High Correct only since Java 5 and only with volatile; easy to copy wrong
Holder idiom Yes Yes Low The recommended one for lazy in Java
Enum When the enum loads Yes Minimal The most robust (serialization and reflection); cannot inherit

Why it is the most criticized pattern

If Singleton were only "how to guarantee one instance", it would be an innocent idiom. The problem is its second half: the global access point. PideYaConfig.getInstance() can be invoked from any line of any class, and that has system-wide consequences:

  • It is global state under another name. Everything engineering learned about why global variables are harmful (invisible coupling, action at a distance, fragile initialization order) applies in full. If the singleton is also mutable (think of a setMaintenanceMode(true)), any part of the program can alter the behavior of any other without a single signature giving it away.
  • It hides dependencies. Look at these two signatures:
// What does this service depend on? Impossible to know without reading the body:
public class DeliveryService {
    public Courier assign(Order o) {
        int radius = PideYaConfig.getInstance().getDeliveryRadiusKm(); // surprise!
        // ...
    }
}

// Here the dependency is in the signature, in plain sight:
public class DeliveryService {
    private final PideYaConfig config;
    public DeliveryService(PideYaConfig config) { this.config = config; }
}

With the singleton, the dependency is a secret of the method body; with the constructor, it is a public contract. It is the practical DIP violation we saw in the principles lesson: a rigid dependency on something concrete — and hidden, on top of that.

  • It ruins testability. A test of DeliveryService with the singleton is forced to use the real configuration (what if the test needs a 1 km radius? what if another test changed it earlier?). State shared between tests produces that well-known plague: tests that pass alone and fail together depending on execution order. With the dependency injected, each test builds its own fake configuration and everyone is happy.
  • Uniqueness is usually a deployment requirement, not a class requirement. "There is only one configuration" is true today, in this process. The day PideYa wants parallel tests with different configurations, or multi-tenancy (one configuration per country), the restriction hard-wired into the class becomes the wall you crash into.

The alternative: dependency injection

The modern correction separates the intent's two halves: keep uniqueness but remove global access. A single instance is created at application startup (the "composition root", the one place that knows how to assemble the system) and is injected into whoever needs it:

public class Main {
    public static void main(String[] args) {
        // Composition root: here is where we decide what exists and how many times
        PideYaConfig config = PideYaConfig.loadFrom("pideya.properties");

        DeliveryService delivery = new DeliveryService(config);
        CheckoutService checkout = new CheckoutService(config, delivery);
        // ...
    }
}

PideYaConfig no longer has getInstance(): it is a normal, testable class of which — as it happens — only one instance is created, because its creator decides so. Uniqueness goes from being a property hard-wired into the class to a system configuration decision, which is where it belongs. Containers like Spring industrialize exactly this: when you declare a bean with singleton scope (the default scope), Spring guarantees a single instance within the container and injects it wherever needed, with no private constructors or statics: it is the "singleton without the Singleton pattern". As we saw in the history lesson, this is a case of a pattern absorbed by frameworks.

When to use it and when not to

Reasonable uses (few and specific):

  • Technical resources with no business state, cross-cutting and stable: a logging registry like EventLog, technical caches, an identifier generator. The more immutable and the more technical (far from the domain), the more harmless.
  • Small applications without an injection container, where setting up DI infrastructure would be over-engineering (lesson 01-06).
  • As an internal implementation detail of another piece (for example, the single instance of a factory, as we will see in the comparison).

Avoid it when:

  • The object holds mutable business state (a singleton CurrentCart is a multi-user bug waiting to happen).
  • The class takes part in logic you want to test with doubles: inject.
  • You work with a framework that has a container (Spring, Jakarta EE): use the container's singleton scope, not the pattern.
  • The "uniqueness" is actually "one per context" (per country, per tenant, per request): the class-level restriction will get in your way soon.

Relation to other patterns (mention only): the factories of Factory Method and Abstract Factory are often implemented as singletons, since they usually have no state; Facade and the pools of Flyweight also frequently show up as a single instance.

Common Mistakes and Tips

  • Copying double-checked locking without volatile. It is this pattern's classic mistake: it compiles, works in demos, and fails once a month in production. If you need thread-safe lazy, use the holder idiom and move on.
  • The "convenience singleton": turning any class into a singleton just to avoid passing it as a parameter. That "saving" of one parameter is paid for with hidden dependencies and brittle tests. Passing dependencies through the constructor is not bureaucracy: it is executable documentation.
  • A mutable singleton without synchronization. If the instance is shared by all the server's threads, so is its internal state: either it is immutable, or its mutability must be protected (note the CopyOnWriteArrayList in EventLog).
  • Forgetting serialization. If your classic singleton implements Serializable, every deserialization manufactures a new instance unless you define readResolve(). The enum singleton is immune.
  • Tip: before writing getInstance(), ask yourself "who should decide that this is unique?". If the answer is "whoever assembles the application" (it almost always is), inject instead of globalizing.

Exercises

Exercise 1: find the flaws

This singleton showed up in a PideYa code review. Point out every problem:

public class RestaurantCache {
    private static RestaurantCache instance;
    private Map<Long, Restaurant> cache = new HashMap<>();

    public RestaurantCache() { }

    public static RestaurantCache getInstance() {
        if (instance == null) {
            instance = new RestaurantCache();
        }
        return instance;
    }

    public void save(Restaurant r) { cache.put(r.getId(), r); }
    public Restaurant find(long id) { return cache.get(id); }
}

Exercise 2: rewrite with the holder idiom

Rewrite RestaurantCache (corrected) using the holder idiom and a thread-safe internal structure.

Exercise 3: from singleton to injection

The courier assignment service uses PideYaConfig.getInstance() in three methods. Rewrite it to receive the configuration through its constructor and write (in pseudocode or Java) a unit test that pins the delivery radius to 2 km without touching any file, verifying that a courier 5 km away is not assignable.

Solutions

Solution 1: (a) the constructor is public: anyone can call new and break uniqueness; (b) the lazy initialization is not thread-safe: two concurrent threads can create two caches; (c) HashMap is not thread-safe and the instance will be shared by the whole server: corruption or infinite loops under load (it should be ConcurrentHashMap); (d) the cache field is not even final. As a bonus: a cache of business entities as a global singleton deserves the lesson's question: shouldn't it be an injected dependency?

Solution 2:

public class RestaurantCache {

    private final Map<Long, Restaurant> cache = new ConcurrentHashMap<>();

    private RestaurantCache() { }

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

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

    public void save(Restaurant r) { cache.put(r.getId(), r); }
    public Restaurant find(long id) { return cache.get(id); }
}

Private constructor, uniqueness and laziness guaranteed by the class loader, and ConcurrentHashMap for the shared state.

Solution 3:

public class CourierAssignmentService {

    private final PideYaConfig config;

    public CourierAssignmentService(PideYaConfig config) {
        this.config = config;
    }

    public boolean isAssignable(Courier c, Order o) {
        return c.distanceKmTo(o.getDeliveryAddress()) <= config.getDeliveryRadiusKm();
    }
}

// Test: no files, no global state, no execution order that matters
@Test
void courierOutsideRadiusIsNotAssignable() {
    PideYaConfig config = PideYaConfig.fromValues(Map.of("delivery.radius.km", "2"));
    CourierAssignmentService service = new CourierAssignmentService(config);

    Courier farAway = courierAtDistanceKm(5);

    assertFalse(service.isAssignable(farAway, sampleOrder()));
}

The key: by injecting, the test builds its own configuration (here with a fromValues factory method for tests) and depends on no global state. With getInstance() this test would be impossible to isolate.

Conclusion

Singleton has taught you two things of unequal value. The technical one: in Java, lazy, thread-safe creation has its idioms (holder for lazy, enum for maximum robustness, and double-checked locking as a museum piece you must know how to read). And the design one, which will serve you for your whole career: the pattern's intent mixes a legitimate need (uniqueness) with a toxic mechanism (global access), and modern practice separates them: a single instance decided at the composition root and injected as a visible dependency. When in doubt, inject.

The next pattern attacks the family's central problem, the one we opened the module with: that switch deciding which notifier to instantiate, repeated across three PideYa services, is finally going to find its place. See you in Factory Method.

© Copyright 2026. All rights reserved