Skip to content
ADevGuide Logo ADevGuide
Go back

Singleton Design Pattern in Java with Examples

Updated:

By Pratik Bhuite | 51 min read

Hub: Java / Design Patterns

Series: Java Design Patterns Series

Last updated: Aug 30, 2026

Part 8 of 9 in the Java Design Patterns Series

Key Takeaways

On this page
Reading Comfort:

Singleton Pattern in Java: Explanation and Examples

Imagine your payment service reads database credentials, feature flags, and retry budgets from a single configuration file at startup. If every request handler constructs its own AppConfig by reparsing the file, you pay file I/O on the hottest path, you risk handlers seeing different flag values within the same deployment, and a mid-request file edit leaves half the service on the old value and half on the new. Worse, a connection manager that manages a pool to Postgres is supposed to own one pool. If each service creates its own pool, connection limits are silently multiplied and the database is overwhelmed after the second replica scales up.

The Singleton design pattern in Java solves exactly this single-instance problem. It guarantees that one class has exactly one instance and exposes a global access point so every consumer shares the same state and the same lifecycle. The instance is created once, reused everywhere, and torn down once. This guide expands the pattern from its three-element skeleton to six production implementations with their concurrency and testing trade-offs, and if you have not yet compared how construction decisions scale across types, read Factory Design Pattern Java Simplified and Factory Method Pattern in Java: Explanation and Example for the branching viewpoint before sinking a shared resource.

Table of Contents

Open Table of Contents

What Is the Singleton Pattern?

Singleton is a creational Gang of Four pattern that ensures a class has exactly one instance and provides a global point of access to it. You can think of it as a single registry window at a venue: every attendee lines up at the same window to collect the same wristband, and the venue enforces that there is literally one window rather than trusting attendees to coordinate casually.

The pattern rests on three elements that always appear together regardless of which idiom you choose:

A private constructor so no outside class can call new and no subclass can appear by accident. WHY private? Because the class is making a promise about cardinality, and allowing inheritance violates that promise by letting a subclass introduce a second instance and by letting construction be observed before the singleton invariant is established.

A private static holder of the single instance. WHY static? Because the field must live with the class, not with any object that might or might not have been created, and static is the lifecycle that matches a class that the class loader initializes exactly once.

A public static accessor such as getInstance() that is the only legal path to obtaining the object. WHY static accessor rather than injecting through constructors? Because the whole team needs a single discovery point that is available everywhere without wiring. That convenience also explains the pattern’s reputation as controversial, which is addressed in the trade-offs below.

This tutorial is a part of the Creational Design Pattern Series. For creation that chooses among sibling types rather than enforcing cardinality, see Abstract Factory Pattern in Java: Explanation and Example and Builder Pattern in Java: Explanation and Example, whose construction pressures are orthogonal to singleton.

Why Singleton Exists: The Shared Resource Problem

Most systems have at least one resource that must be single by nature. Consider how a fintech service like Razorpay or Stripe manages a rate-limited HTTP client to a banking partner. The partner allows 100 concurrent connections per API key. If each request handler creates its own PartnerHttpClient with a pool of 100, two handlers running on the same process already exceed the partner’s limit under burst. A singleton client configured once and shared everywhere respects the intended budget because the budget is tied to the instance, not merely to a number in a file.

The same shape recurs with loggers, configuration, and caches. A logger configured with a file appender and rotation policy must be the same logger wherever logger.info() is called. Otherwise two loggers contend for the same file handle and rotation leaves gaps. A configuration object read at startup must be the same AppConfig that every handler queries, so that feature flag rollout or circuit breaker threshold changes are visible atomically via one object’s reload rather than inconsistently via half-reparsed files. Singleton solves that by making instance identity part of the contract: there is one holder, there is one lifecycle, and consumers cannot invent another.

The naive alternative that teams try first is a public static field or utility class with static methods. Static state compiles and feels similar, but it is harder to evolve. A utility such as ConfigUtil.getFlag() cannot implement an interface, cannot be substituted with a fake in tests without bytecode tricks, cannot be lazily initialized with exception handling, and cannot opt into serialization discipline later without rewriting call sites. Singleton keeps the single-instance promise while remaining an object whose dependencies, lifecycle, and policy are visible in its constructor and whose contract can be widened to an interface if the project outgrows the singleton decision, a discipline that pairs naturally with SOLID Principles in Java.

Singleton vs Static Utility vs Dependency Injection

Three options address shared resources and mixing them up is the most common regret in review:

Static utility: A class with static methods such as ConfigUtil.get(String key). The call site is short, but every caller depends on a concrete class, the underlying state is implicit in static fields, and substitution requires PowerMock or similar bytecode manipulation. Use it only for stateless helpers like Math or string formatters that carry no resource.

Singleton: A normal class with a private constructor and a single instance reachable through getInstance(). Callers still depend on a concrete singleton, yet the instance is an object that can implement an interface, can be lazily created with error handling, and can be replaced per test if the singleton exposes a test-only setter or a holder-based reset. It is the right fit when the resource is expensive, the lifecycle is global, and the team accepts that unit tests will want to isolate against it.

Dependency injection: A DI container such as Spring or Guice creates one bean and injects it where needed. Construction still happens once, but discovery moves from Singleton.getInstance() to constructor injection. Tests inject a fake, the graph makes dependencies explicit, and multiple class loaders no longer multiply instances silently. For large services this makes dependencies honest at the expense of a container and explicit wiring. A pragmatic migration path is to start with a disciplined singleton and refactor toward DI once the test tax appears in several modules.

Choose static for stateless utilities, singleton for a single stateful resource owned locally inside a library, and DI when dependency honesty across modules outweighs global access convenience.

Participants and Their Responsibilities

Singleton has two participants and one rule that binds them, and each choice earns its place by enforcing cardinality:

Singleton class is the type such as BillPughSingleton that owns the instance field, hides its constructor, and exposes getInstance(). WHY keep all three elements together? Because the invariant is global. If any outsider can construct the type, review cannot reason about how many pools or loggers exist. Placing the guard, the holder, and the access point in one class makes the invariant checkable in a single file.

Client is any consumer that calls Singleton.getInstance() and uses the result without constructing its own. WHY must the client depend only on the accessor? Because receiving the instance through the accessor is what registers the caller’s dependency on the shared lifecycle. A client that holds a separately constructed copy can drift out of lifecycle control and survive past shutdown hooks.

The implicit rule is lifecycle ownership. Whoever owns the singleton is responsible for safe publication under concurrency, for deserialization semantics, for class-loader semantics, and for whether the singleton can be reset or reloaded. Moving that ownership to instance fields scattered across callers distributes a decision that must be made once.

Mermaid Lifecycle Diagram

flowchart TD
    C[Client calls getInstance] --> CHK{Instance exists?}
    CHK -- Yes --> RET[Return existing instance]
    CHK -- No --> CREATE[Create via private constructor]
    CREATE --> PUB[Publish to static field\nsafe under chosen strategy]
    PUB --> RET
    RET --> USE[Client uses singleton\nconfig, logger, pool]
    USE --> REL{Need reload or reset?}
    REL -- Config reload --> RLD[Reload state inside same instance]
    RLD --> USE
    REL -- Test or shutdown --> RST[Reset or close\ntest-only or lifecycle hook]
    RST --> CHK
    REL -- No --> DONE[End request]
    USE --> DONE

    classDef singleton fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#000000;
    classDef client fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px,color:#000000;
    class CHK,CREATE,PUB,RET,RLD,RST singleton;
    class C,USE,REL,DONE client;

The loop that matters is the reload and reset path. A production singleton that caches configuration is expected to reload inside the same instance rather than publishing a second instance to half its consumers. A test singleton is expected to reset between test cases so that one test’s mutation does not leak into the next. Both behaviors require explicit design, which is why the implementation idiom you choose dominates the pattern’s correctness more than the accessor name.

Implementation Steps

A clean singleton implementation follows four steps, each with a practical check that prevents the usual regression:

  1. Make the constructor private and final-or-non-extensible. A private constructor blocks new outside the class. If the class is not declared final, at least block subclassing through that private constructor. Verify in review that no reflective factory inside the package accidentally exposes a package-private creator.

  2. Introduce the static holder that matches the desired initialization strategy. Eager, static block, lazy, synchronized, double-checked, holder, and enum each trade off startup work, concurrency cost, and serialization resilience. Choose the holder by evaluating whether the singleton is heavyweight and optional, whether the process actually uses it in every startup, and whether serialization or reflection needs to be guarded.

  3. Expose exactly one accessor with deliberate synchronization. getInstance() is the only public way in. Synchronize only as much as correctness requires, and keep the accessor small so the cost of the strategy is visible in the file rather than buried inside helper methods.

  4. Decide lifecycle operations such as reload, reset, and serialization policy at authoring time. If the singleton will participate in deserialization, implement readResolve() or prefer enum. If tests need isolation, plan a narrowly scoped, test-only reset helper guarded by package visibility rather than letting each test reset via reflection. This is also where the thread-safety and mutability insights from Builder Pattern in Java: Explanation and Example apply to freezing internal state even when the holder itself is a singleton.

Six Singleton Implementations in Java

This section keeps all six idioms from the original tutorial but explains the WHY per idiom so the choice is no longer taste alone.

Eager Initialization

package com.adevguide.java.designpatterns.singleton;

/** @author pbhuite */
public class EagerInitialization {

    // WHY: private constructor blocks external new and accidental subclassing
    private EagerInitialization() {
        System.out.println("A Singleton class is created using EagerInitialization.");
    }

    // WHY: static final eager field leverages class-loader initialization which is thread-safe without synchronization
    private static final EagerInitialization INSTANCE = new EagerInitialization();

    // WHY: trivial accessor with no branching and no lock, caller cannot create a second instance
    public static EagerInitialization getInstance() {
        return INSTANCE;
    }
}

Creation happens when the class is loaded, before any thread calls getInstance(). WHY choose eager? When the singleton is lightweight and almost always needed, this is the simplest idiom with no synchronization cost at call time. WHY avoid it when the singleton is heavyweight such as a connection pool that a test harness may never use? Eager work is paid even when no code path needs the object, and there is no place to handle checked construction exceptions gracefully. Use this when startup work is cheap and global.

Static Block Initialization

package com.adevguide.java.designpatterns.singleton;

/** @author PraBhu */
public class StaticBlockInitialization {

    private static StaticBlockInitialization instance;

    static {
        try {
            // WHY: static block eagerly creates the instance but allows checked exception handling at class load time
            instance = new StaticBlockInitialization();
        } catch (Exception e) {
            System.out.println("Exception Occured" + e);
        }
    }

    // WHY: private constructor keeps cardinality guarantee intact across package
    private StaticBlockInitialization() {
        System.out.println("A Singleton class is created using StaticBlockInitialization.");
    }

    public static StaticBlockInitialization getInstance() {
        return instance;
    }
}

Same eager timing as the previous idiom but with an explicit static { } block. WHY bother with the block? To wrap construction that can throw, such as reading a configuration file or establishing a filesystem handle at startup. The drawback is the same as eager. Work is paid even when the process stream does not need the singleton, which is why this idiom is best for eager resources whose construction is failable and whose failure should surface at class loading.

Lazy Initialization

package com.adevguide.java.designpatterns.singleton;

/** @author PraBhu */
public class LazyInitialization {

    // WHY: private constructor blocks external instantiation
    private LazyInitialization() {
        System.out.println("A Singleton class is created using LazyInitialization.");
    }

    // WHY: null until first getInstance() call, so startup avoids the cost if the feature is unused
    private static LazyInitialization instance;

    public static LazyInitialization getInstance() {
        // WHY: lazy field check defers allocation until needed, cheapest code shape but unsafe under concurrency
        if (null == instance) {
            instance = new LazyInitialization();
        }
        return instance;
    }
}

Creation defers until the first call. That is exactly what a CLI that only sometimes needs a database wants. The flaw is concurrency. Two threads can both see instance == null, both allocate, and both publish. One allocation wins the field but callers briefly held different objects, breaking the exactly-one promise. The deeper reading for WHY this fails under the Java Memory Model is in the discussion of the next two idioms.

Is Java Pass by Value or Pass by Reference?

Thread Safe Singleton

package com.adevguide.java.designpatterns.singleton;

/** @author PraBhu */
public class ThreadSafeSingleton {

    // WHY: private constructor enforces the single instance contract
    private ThreadSafeSingleton() {
        System.out.println("A Singleton class is created using ThreadSafeSingleton.");
    }

    private static ThreadSafeSingleton instance;

    // WHY: synchronize the accessor so only one thread can evaluate null and construct
    public static synchronized ThreadSafeSingleton getInstance() {
        if (null == instance) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
}

Synchronizing the accessor makes lazy initialization thread-safe through mutual exclusion. WHY accept the lock? Because correctness outranks throughput for a path whose single-instantiation event happens a handful of times. WHY object when synchronized seems heavy? On modern JVMs an uncontended monitor is cheap, and this is often the preferred production idiom when a senior team wants minimal code and honest concurrency over cleverness. The remaining objection is architectural. Every production call still enters a monitor even after the instance exists, so in extreme per-request singleton resolution, the idiom can be tightened with double-checked locking or, better, with the holder pattern covered next.

package com.adevguide.java.designpatterns.singleton;

/** @author PraBhu */
public class ThreadSafeDoubleLocking {

    private ThreadSafeDoubleLocking() {
        System.out.println("A Singleton class is created using ThreadSafeDoubleLocking.");
    }

    // WHY: lazily assigned field whose publication relies on correctly ordered double-checked locking
    private static ThreadSafeDoubleLocking instance;

    public static ThreadSafeDoubleLocking getInstance() {
        if (null == instance) {
            synchronized (ThreadSafeDoubleLocking.class) {
                if(null==instance)
                    instance = new ThreadSafeDoubleLocking();
            }
        }
        return instance;
    }
}

The classic double-checked locking optimization synchronizes only when the field is null and then checks again inside the lock. WHY check twice? The first check avoids locking after construction, the second prevents two threads that both passed the first check from constructing twice inside consecutive critical sections. WHY declare the field volatile in a complete implementation? Because without volatile a writer thread’s half-initialized object could become visible to a reader thread that sees a non-null reference before the constructor has finished. The modern form is private static volatile ThreadSafeDoubleLocking instance. This idiom is labeled correctly above to show what the original tutorial’s version looked like without volatile, which is the nuance that interviews and code review must catch.

Note: With early versions of the JVM, synchronizing the whole method was generally advised against for performance reasons. But synchronized performance has improved a lot in new JVMs, so this is now a preferred solution. The nuance for double-checked locking is that volatile on the instance field is not an optimization, it is correctness for safe publication.

Bill Pugh Singleton Implementation

package com.adevguide.java.designpatterns.singleton;

/** @author PraBhu */
public class BillPughSingleton {

    private BillPughSingleton() {
        System.out.println("A Singleton class is created using BillPughSingleton.");
    }

    // WHY: holder class is not loaded when BillPughSingleton loads, so INSTANCE is not created yet
    private static class InnerStaticHelperClass {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    // WHY: first call triggers holder class loading, and class loading is thread-safe without explicit synchronization
    public static BillPughSingleton getInstance() {
        return InnerStaticHelperClass.INSTANCE;
    }
}

This is the most widely recommended idiom for ordinary Java singletons. WHY prefer holder over synchronized accessor? Because it achieves lazy initialization, thread safety via class-loader guarantees, and zero synchronization cost after publication, all without volatile and double-checked discipline. When many teams standardize on it, the codebase gains a single searchable shape that juniors can recognize and tools can lint, which lowers the cost of review for a pattern that otherwise appears in several similar-looking forms.

Regular Expressions in Java: Complete Guide with Examples

Enum Singleton

package com.adevguide.java.designpatterns.singleton;

/** @author PraBhu */
public enum EnumSingleton {
INSTANCE;

    public void getInstance() {
        //Perform some task here
    }
}

INSTANCE is the singleton. WHY choose enum when you are willing to pay the specificity? Because the Java language guarantees that each enum constant exists exactly once per enum type per class loader, guards serialization so deserializing returns the same INSTANCE, and guards against reflective construction without extra readResolve() logic. The trade-off is flexibility. Enum constants are created eagerly when the enum type loads, and domain singletons that need constructor parameters or lazy heavy resources are awkward to map onto an enum constant. Use enum when the singleton represents a globally shared service whose protection against deserialization and reflection must not depend on developer discipline, and choose Bill Pugh or synchronized accessor when laziness or parameterization matters more.

Real-World Example: How JDK, Spring, and Uber Use Singleton

JDK Runtime and Desktop. Runtime.getRuntime() and Desktop.getDesktop() are the textbook platform singletons. Runtime represents the single JVM process and exposes operations like availableProcessors() and exec() that must not be duplicated per caller. A second Runtime object would mislead code into tuning or shutting down a supposedly separate process that does not exist. The design choice to make the constructor package-private and expose a static accessor mirrors the private-constructor guard shown above, which is why every JDK singleton access is instantly recognizable as a discovery through the class rather than a fresh construction.

Logging frameworks and Spring. Nearly every Java service uses a singleton logger per class or a singleton logging factory. SLF4J’s LoggerFactory.getLogger(Class) is backed by a singleton factory that configured handlers, levels, and appenders once and then shares them everywhere, so rotation and level changes are atomic. In Spring, a @Configuration bean is a singleton by default scope. A RestTemplate, a MeterRegistry, or an ObjectMapper configured once as a bean is injected as a shared instance and reused across request handling. The value for Spring teams is the DI contrast. The framework creates the singleton and hands the same reference to every injection point, so tests can replace it without calling Singleton.getInstance() at all, which makes global state explicit in the wiring layer.

Uber, Netflix, and connection pooling at scale. Large services that talk to Postgres, Redis, or a metered partner API often front those dependencies with one shared DataSource, one shared JedisPool, or one shared MeterRegistry. Uber-scale fleet services learned that per-service pool multiplication is invisible when each service looks correct locally but aggregate connection count pushes the datastore into throttling globally. Centralizing the pool in a singleton whose size is reviewed alongside capacity planning keeps the budget computable. NetflixOSS observability plumbing adopted the same pattern for shared registries so metrics cardinality is managed once. The distributed-cache deep dive Distributed Cache System Design covers the next layer of this sharing discipline across nodes, where the per-process singleton controls process-local resources and the distributed cache service controls cross-process resources, and both forms rely on a single-holder mental model.

When to Use vs When NOT to Use

Use Singleton when:

  • The resource is expensive to create and must be single by domain nature, such as a database connection pool, a security credential provider, or a parsed global configuration. Creating it once and sharing it is the intended operating posture.

  • The shared instance carries global lifecycle concerns such as reload, rotation, or shutdown that must be coordinated. A logger that rotates files or a circuit breaker registry that rolls counters benefits from one coordinator.

  • Global discoverability without wiring is the right trade-off for a small codebase or a library whose consumers should not need a container to locate the resource, and the hidden-dependency cost is acknowledged and managed through clear documentation.

Do NOT use Singleton when:

  • Hidden global state would harm testability or evolution. If every test needs a different flag configuration or a different fake data source, a global accessor forces per-test resets or reflection hacks, and dependency injection with constructor-injected collaborators is cleaner.

  • Cardinality should be one by default but not enforced by the type. A service that currently needs one rate limiter but will need per-tenant limiters next quarter should not encode one as a type-level invariant. Prefer a factory that currently hands out one instance rather than locking the design through a private constructor.

  • Multiple class loaders or deserialization are expected. Without serialization or loader discipline, the one per class loader or one before deserialization invariant can be silently broken. Enum or an explicit registry handles those concerns more robustly, and configuration discipline from What Are Environment Variables plus DI scoping handles the resource-sharing concern without global static state.

DimensionUse SingletonAvoid Singleton
Resource natureOne expensive resource per processMany similar resources per tenant or per request
LifecycleSingle reload, rotation, or shutdown pointLifecycle varies per consumer
DiscoverabilityGlobal access without wiring acceptableExplicit dependency injection preferred
Test isolationOne configuration per test or shared setup cheapEach test needs distinct isolated state
EvolutionCardinality is stable by domainCardinality is a temporary convenience

Advantages and Trade-offs

Singleton gives you one lifecycle for configuration, pooling, and logging, which reduces allocation, respects external quotas, and keeps flag rollouts coherent across handlers. It provides a single, searchable access point so a new engineer looking for where the pool is configured lands in one file, and instrumentation such as metric counters truly aggregate because they live on one instance. The holder and synchronized-accessor idioms also give you thread-safe publication without inviting every caller to invent its own double-checked logic, so the concurrency story is decided once at authoring. Compared to ad-hoc static helpers scattered across services, having one type that owns creation, publication, and reload is easier to own and evolve.

The trade-offs are why the pattern is marked controversial in every serious guide. A singleton hides its dependency. A handler that calls Config.getInstance().getFlag("newSearch") does not declare that flag dependency in its constructor, so architecture review cannot see the graph. Tests pay the price. Because the instance outlives a single test case, each test must be aware of shared state and must either reset carefully or rely on fakes injected through a narrow, test-only seam. Singleton also invites hidden mutability. Even if the holder is single, an internal Map deliberately left writable remains global mutable state, which is the exact category of bug that a checkout or pricing flow cannot tolerate. Dependency injection frameworks exist for this reason. They keep the one-instance property via container scope while making the sharing visible as a constructor parameter. Budgeting for factory or builder style when the construction problem is optionality or family variance, as documented in Builder Pattern in Java: Explanation and Example and Abstract Factory Pattern in Java: Explanation and Example, keeps the codebase from encoding one everywhere merely because an early component happened to use a singleton.

Finally, deployment concerns remain. In classic servlet containers or plugin hosts with multiple class loaders, there is one holder per loader, not per JVM, so two applications collocated on the same JVM can each own a private Config singleton and diverge. The operational remedy is to either colocation-isolate or to move global sharing into a registry that clarifies loader scope. Recognizing that operational boundary early saves a team from debugging configuration drift that no unit test can reproduce inside a single class loader harness.

Common Pitfalls

Introducing hidden global mutable state that survives per-test resets. A map or cache field inside a singleton that callers mutate freely becomes global mutable state across the process. The pool configuration or feature-flag cache is now a race-prone singleton whose getInstance().getCache().put(k, v) call from one handler corrupts every other handler and every subsequent test. Fix this by making the singleton’s externally visible state immutable or by returning defensive copies and unmodifiable views, by placing mutation behind a narrow, synchronized, and logged reload method, and by adding a test-only reset helper that is package-private rather than letting each test perform reflective field clearing.

Breaking the exactly-one promise through serialization or reflection without realizing it. Out of the box, deserializing a serializable singleton creates a new instance through the serialization machinery, and reflective Constructor.setAccessible(true) can construct a second instance despite the private constructor. Both undermine exactly-one without a compiler warning. Fix this with either readResolve() returning the canonical instance plus an explicit guard in the private constructor that throws if instance != null when reflective construction is plausible, or by preferring EnumSingleton where the JVM enforces both serialization and reflection protection by language rule, which is why enum is the recommended hardness increment for security-sensitive services.

Concealing dependencies behind global access so architecture becomes hard to reason about. A class that internally calls Singleton.getInstance() for three collaborators has no honest dependency declaration, so a reader cannot tell that changing the flag registry affects pricing, nor can a test author know which fakes to supply without reading the method body. This anti-pattern is subtle because each call compiles and passes a narrow unit test that happens to have the expected global state. Fix it by injecting collaborators when the component lives inside a DI boundary, or, if global access is retained, by passing the shared instance as an explicit constructor argument at the composition root and limiting getInstance() usage to bootstrap code, so review sees the graph and the dependency advice in SOLID Principles in Java is honored.

Interview Questions

1. What is the Singleton pattern and what guarantee does it provide that a utility class does not?

Singleton ensures that a class has exactly one instance and exposes a global access point, while a utility class with static methods exposes stateless helpers whose shared state, when it exists, is hidden in static fields without a cardinality guarantee. A singleton’s private constructor, static holder, and accessor make the invariant enforceable in one file and let the type optionally implement an interface so a fake can be substituted through the accessor’s test seam. Utility state tends to grow as scattered static fields that must be reset per test with reflection, and substitution requires bytecode tricks, which is why code review treats singleton as an object lifecycle choice and utility state as a testing and ownership risk.

2. When is lazy initialization justified over eager, and how does thread safety change the choice?

Lazy initialization is justified when the singleton holds a heavyweight resource that is legitimately not needed in some process modes, such as a CLI that supports a database flag but often runs without it. Eager initialization pays the cost at class loading regardless of use, so it wastes time and may fail startup spuriously. The trade-off is concurrency. The unsynchronized lazy idiom races, the synchronized accessor is correct and fast enough for most production singletons, double-checked locking achieves laziness without per-call synchronization at the cost of volatile discipline, and Bill Pugh’s holder achieves all three through class-loader guarantees. Choose by whether startup budget, lazy skip, and team comfort with volatile are the dominant pressure.

3. Why is Bill Pugh preferred over double-checked locking in most teams?

Bill Pugh’s inner helper class defers INSTANCE creation until the holder class is first loaded on the first getInstance() call, and the JVM’s class initialization is guaranteed to be thread-safe without any explicit synchronized or volatile visibility workaround. Double-checked locking achieves the same laziness and a fast post-construction path but requires the instance to be volatile so the publication of the object and its fields is safely ordered for readers, which is easy to get wrong in review. Because both idioms avoid paying for synchronization after construction, codebase consistency favors the shape whose correctness proof rests on the language rather than on a field modifier that a hurried edit may remove.

4. Why is Enum Singleton considered the hardest implementation and when would you not choose it?

Enum is considered hardest because the language enforces that each enum constant exists exactly once per type per class loader, and the runtime handles serialization recovery and reflective construction prevention without a hand-written readResolve() or constructor guard. No second INSTANCE can be smuggled in through ObjectInputStream or Constructor.newInstance. The reason not to choose it is ergonomics. Enum constants are eagerly created when the enum type loads, so a singleton whose creation is truly optional loses laziness, and parameterized construction that depends on runtime configuration is more naturally expressed through a class with a private constructor plus a holder or a synchronized accessor. Pick enum when hardness matters more than laziness.

5. How do serialization and multiple class loaders break Singleton and how do you fix them?

Serialization creates a new object without calling the constructor, so a serialized singleton that does not define readResolve() will deserialize into a second distinct instance whose == check fails and whose state diverges from the canonical holder. Fix this with private Object readResolve() { return getInstance(); } or by using EnumSingleton where the runtime already does that. Multiple class loaders give each loader its own copy of the class and its static fields, so a single JVM hosting two apps can contain two AppConfig holders. The operational fix is to widen ownership to DI-managed scope or to a registry that makes loader scope explicit rather than relying on the implicit one-per-loader static guarantee.

6. When should you replace Singleton with dependency injection and what do you lose by keeping Singleton?

Replace singleton with DI when tests repeatedly need distinct configurations, when the dependency graph should be explicit for ownership and mocking, or when you already run a container that manages scope such as Spring. DI trades global convenience for explicitness, so a handler’s constructor lists AppConfig alongside its other collaborators, review sees the graph, and every test injects exactly the state it wants without per-test singleton resets. Keeping a singleton after the graph has grown hides that dependency, makes flag impact analysis depend on searching for getInstance() call sites, and concentrates merge attention on the accessor file. The migration path most teams follow is to keep a disciplined singleton inside libraries and promote app code to DI once the test reset tax appears in several consecutive sprints.

Conclusion

Singleton earns its keep when exactly one instance is the correct business truth:

  1. Guard cardinality in one place. A private constructor, a single static holder, and exactly one accessor keep the invariant reviewable in a single file.
  2. Match idiom to lifecycle. Use eager or static block when the resource is lightweight and always needed with exception handling at class-load time. Use synchronized accessor, holder, or correctly volatile double-checked locking when the cost should be lazy and safely published.
  3. Treat enum as the hardness upgrade. Choose enum when serialization or reflection defense must be language guaranteed rather than discipline guaranteed.
  4. Defend testability early. Decide whether the singleton will back DI-provided scope, what reload semantics it offers, and which narrow test-only reset seam exists so tests do not drift into reflective hacks.
  5. Graduate when the graph grows. When instance sharing hides too many implicit dependencies, promote sharing to constructor-injected container scope and keep the singleton idiom only inside libraries that must remain usable without a container.

The natural companions to deepen creational skills are Factory – centralizing single-product branching – which contrasts cardinality control with sibling selection, and Builder – assembling many optional fields immutably, which solves optionality without encouraging global mutable sharing. For the domain that needs kits that stay consistent rather than one shared instance, contrast with Abstract Factory Pattern in Java: Explanation and Example.

References

  1. Singleton Pattern - Refactoring Guru
    https://refactoring.guru/design-patterns/singleton
  2. Singleton Pattern - OODesign
    https://www.oodesign.com/singleton-pattern.html
  3. Design Patterns: Elements of Reusable Object-Oriented Software - Gamma et al.
    https://en.wikipedia.org/wiki/Design_Patterns

YouTube Videos

  1. “Singleton Design Pattern”
    https://www.youtube.com/watch?v=bZFMS1BeVQA

  2. “Singleton design pattern”
    https://www.youtube.com/watch?v=ozWGCjvdhHc

  3. “Singleton Design Pattern in Java Explained | Java Design Patterns Tutorial”
    https://www.youtube.com/watch?v=6vG1FDHBA4k


Share this post on:

Next in Series

Continue through the [object Object] with the next recommended article.

Related Posts

Keep Learning with New Posts

Subscribe through RSS and follow the project to get new series updates.

Was this guide helpful?

Share detailed feedback

Previous Post
Prototype Design Pattern in Java with Cloning Example
Next Post
SOLID Principles in Java Explained with Examples