Skip to content

Singleton

GoF Design Patterns — Creational · See also: Factory Method · Builder

Introduction

Singleton ensures that a class has only one instance and provides a global access point to it. It is one of the most recognizable GoF patterns and also one of the most frequently misapplied. This article covers the pattern honestly: the legitimate use cases are real, but so are the pitfalls. In modern application code, Dependency Injection frameworks handle lifecycle management more safely than hand-written Singletons, and the pattern's most common use in application code is as a global variable bag — an anti-pattern that damages testability and introduces hidden coupling.

Understanding Singleton matters not primarily to implement it, but to recognize it, evaluate whether it is appropriate, and know when a DI framework makes it unnecessary.

Intent

Ensure a class has only one instance and provide a global point of access to that instance.

Structure

Participants

ParticipantRole
SingletonDefines a private constructor so no external code can call new, and exposes a static getInstance() method that creates the instance on first call and returns the cached instance on subsequent calls.
ClientCalls Singleton.getInstance() to obtain the single instance. The client has no way to create its own copy.

The Broken Implementation — Naive Double-Checked Locking

Double-checked locking was a widely copied pattern in Java before Java 5. Without volatile, it is broken due to the Java Memory Model allowing partial object construction to be visible across threads.

java
// ANTI-PATTERN: Broken double-checked locking (pre-Java 5 / missing volatile)
public class ConfigurationRegistry {

    // Without volatile, another thread can observe a non-null but incompletely
    // constructed instance due to instruction reordering by the JIT compiler.
    private static ConfigurationRegistry instance; // ← missing volatile

    private final Map<String, String> config = new HashMap<>();

    private ConfigurationRegistry() {
        // Imagine loading from a file or environment here
        config.put("db.url", "jdbc:postgresql://localhost/prod");
        config.put("db.pool.size", "20");
    }

    public static ConfigurationRegistry getInstance() {
        if (instance == null) {             // First check (no lock)
            synchronized (ConfigurationRegistry.class) {
                if (instance == null) {     // Second check (with lock)
                    instance = new ConfigurationRegistry();
                    // The JIT can reorder: instance is assigned BEFORE
                    // the constructor body completes. Another thread entering
                    // the first check sees instance != null and reads a
                    // partially initialized object. ← undefined behaviour
                }
            }
        }
        return instance;
    }

    public String get(String key) {
        return config.get(key);
    }
}

The fix for this specific implementation is adding volatile to the field declaration. But there are better approaches.

Correct Implementation 1 — Enum Singleton

The enum-based Singleton is the approach recommended by Effective Java (Bloch, Item 3). The JVM guarantees that enum values are initialized exactly once, are serialization-safe, and are protected from reflection attacks.

java
// ConfigurationRegistry.java — enum Singleton (preferred for most cases)
public enum ConfigurationRegistry {

    INSTANCE;

    private final Map<String, String> config;

    // Enum constructor — called exactly once by the JVM
    ConfigurationRegistry() {
        config = new HashMap<>();
        loadConfiguration();
    }

    private void loadConfiguration() {
        // Load from environment variables, AWS SSM Parameter Store, etc.
        String dbUrl = System.getenv().getOrDefault("DB_URL",
                "jdbc:postgresql://localhost/appdb");
        String poolSize = System.getenv().getOrDefault("DB_POOL_SIZE", "10");

        config.put("db.url", dbUrl);
        config.put("db.pool.size", poolSize);
    }

    public String get(String key) {
        return config.getOrDefault(key, "");
    }

    public String getOrDefault(String key, String defaultValue) {
        return config.getOrDefault(key, defaultValue);
    }

    // Reload support — call this in tests to reset state
    public synchronized void reload() {
        config.clear();
        loadConfiguration();
    }
}

// Usage
String dbUrl = ConfigurationRegistry.INSTANCE.get("db.url");

The enum approach is:

  • Thread-safe by JVM guarantee — no synchronization code required.
  • Serialization-safe — deserializing an enum always returns the existing constant, not a new instance.
  • Reflection-safeConstructor.newInstance() throws an exception for enums.

Correct Implementation 2 — Initialization-on-Demand Holder Idiom

When you need a class (not an enum) for flexibility — e.g., to implement an interface — the Holder idiom provides lazy initialization without any synchronization overhead:

java
// ConnectionPoolManager.java — Initialization-on-Demand Holder idiom
public final class ConnectionPoolManager {

    private final DataSource dataSource;
    private final int maxPoolSize;

    // Private constructor — external instantiation is impossible
    private ConnectionPoolManager() {
        this.maxPoolSize = Integer.parseInt(
                System.getenv().getOrDefault("DB_POOL_SIZE", "20"));

        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(System.getenv().getOrDefault(
                "DB_URL", "jdbc:postgresql://localhost/appdb"));
        config.setMaximumPoolSize(maxPoolSize);
        config.setConnectionTimeout(30_000);
        config.setIdleTimeout(600_000);

        this.dataSource = new HikariDataSource(config);
        System.out.println("[ConnectionPoolManager] Pool initialized, maxSize=" + maxPoolSize);
    }

    // The JVM initializes Holder only when getInstance() is first called.
    // Class initialization is thread-safe by the JVM class-loading guarantee.
    // No synchronization needed, no volatile needed.
    private static final class Holder {
        static final ConnectionPoolManager INSTANCE = new ConnectionPoolManager();
    }

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

    public Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    public DataSource getDataSource() {
        return dataSource;
    }

    public int getMaxPoolSize() {
        return maxPoolSize;
    }
}

The Holder idiom is:

  • Lazily initializedConnectionPoolManager is not created until getInstance() is first called.
  • Thread-safe without locks — JVM class loading is inherently synchronized.
  • A concrete class — can implement interfaces, unlike enums.

Anti-Pattern 1 — Singleton as a Global Variable Bag

The most common misuse of Singleton in application code is treating it as a namespace for global mutable state. Any class in the application can read or write to it with no record of the dependency.

java
// ANTI-PATTERN: Singleton as a mutable global state bag
public class ApplicationContext {

    private static ApplicationContext instance;

    // Mutable fields — any class can change these without the owner knowing
    public UserSession currentUser;
    public String lastErrorMessage;
    public boolean maintenanceMode;
    public List<String> recentOperations = new ArrayList<>();

    private ApplicationContext() {}

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

// Callers write to global state with no traceability
public class OrderController {
    public void createOrder(OrderRequest req) {
        ApplicationContext.getInstance().recentOperations.add("CREATE_ORDER");
        // ... business logic
    }
}

public class ReportingService {
    public void generate() {
        // Reads global state — who wrote this? When? Under what thread?
        if (ApplicationContext.getInstance().maintenanceMode) {
            return;
        }
        // ...
    }
}

Why this is harmful:

  • Hidden coupling — callers depend on the Singleton without declaring it in their constructors or method signatures.
  • Invisible state mutation — any code path can change maintenanceMode or lastErrorMessage; tracing the origin of a bad value requires a full-codebase search.
  • Thread safety is an afterthought — ArrayList and public mutable fields are not thread-safe.
  • Untestable — unit tests cannot isolate a component that reads from global state unless the Singleton itself is reset between tests.

Anti-Pattern 2 — Stateful Singleton

A Singleton whose state changes after initialization is a concurrency hazard. Two threads calling methods on it simultaneously can corrupt the internal state if the class is not carefully synchronized.

java
// ANTI-PATTERN: Stateful Singleton — accumulates mutable state over time
public class RequestCounter {

    private static RequestCounter instance;
    private int count = 0;                   // Not thread-safe
    private Map<String, Integer> byEndpoint  // Also not thread-safe
            = new HashMap<>();

    private RequestCounter() {}

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

    // Two threads calling this simultaneously corrupt `count`
    public void increment(String endpoint) {
        count++;
        byEndpoint.merge(endpoint, 1, Integer::sum);
    }

    public int getCount() { return count; }
}

If a Singleton must carry mutable state, every field that can be written by more than one thread must use thread-safe types (AtomicInteger, ConcurrentHashMap) and every compound operation must be properly synchronized.

Spring Beans Are Not the Same as Singleton Pattern

Spring beans are singleton-scoped by default — one instance per ApplicationContext. This is not the GoF Singleton pattern:

DimensionGoF SingletonSpring Singleton Bean
Lifecycle controlThe class controls its own lifecycleSpring controls the lifecycle
TestabilityHard — global state is hard to resetEasy — create a new ApplicationContext with mock beans
Multiple instancesImpossible by designPossible — create two ApplicationContexts
Dependency injectionClasses reach out via getInstance()Spring injects via constructor
RecommendationAvoid in application codeUse freely — it is the intended default

Spring beans solve the core problem that drives misuse of Singleton: managing shared, expensive objects (database connections, HTTP clients, service instances) with a single shared lifecycle. Prefer Spring (or any DI container) over hand-written Singletons in application code.

Legitimate Use Cases

Singleton is appropriate when both conditions hold:

  1. Exactly one instance is a hard constraint of the domain (e.g., the system must not open two connection pools to the same database).
  2. A DI framework is not managing the component's lifecycle.

AWS SDK client registry (legitimate Singleton)

AWS SDK v2 clients are expensive to construct (they create thread pools and establish TLS sessions). A registry Singleton built at startup and used read-only throughout the application lifetime is a valid use of the pattern:

java
// AwsClientRegistry.java — read-only registry, initialized once
public enum AwsClientRegistry {

    INSTANCE;

    private final S3Client s3;
    private final SqsClient sqs;
    private final DynamoDbClient dynamo;

    AwsClientRegistry() {
        Region region = Region.of(
                System.getenv().getOrDefault("AWS_REGION", "us-east-1"));

        AwsCredentialsProvider credentials = DefaultCredentialsProvider.create();

        this.s3 = S3Client.builder()
                .region(region)
                .credentialsProvider(credentials)
                .build();

        this.sqs = SqsClient.builder()
                .region(region)
                .credentialsProvider(credentials)
                .build();

        this.dynamo = DynamoDbClient.builder()
                .region(region)
                .credentialsProvider(credentials)
                .build();

        System.out.println("[AwsClientRegistry] Clients initialized for region: " + region);
    }

    public S3Client s3()          { return s3; }
    public SqsClient sqs()        { return sqs; }
    public DynamoDbClient dynamo() { return dynamo; }
}

// Usage anywhere in the application — clients are shared, not re-created
public class OrderRepository {

    public void save(Order order) {
        AwsClientRegistry.INSTANCE.dynamo().putItem(
                PutItemRequest.builder()
                        .tableName("Orders")
                        .item(Map.of(
                                "PK", AttributeValue.fromS("ORDER#" + order.getId()),
                                "status", AttributeValue.fromS(order.getStatus())
                        ))
                        .build());
    }
}

Testability — Resettable Instance for Tests

The hardest problem with Singletons is that global state bleeds between test cases. The safest mitigation is to design a reset hook from the start, restricted to test scope:

java
// ConfigurationRegistry.java — with test-safe reset hook
public enum ConfigurationRegistry {

    INSTANCE;

    private Map<String, String> config = new HashMap<>();

    ConfigurationRegistry() {
        loadFromEnvironment();
    }

    private void loadFromEnvironment() {
        config.put("db.url", System.getenv().getOrDefault("DB_URL",
                "jdbc:postgresql://localhost/appdb"));
        config.put("feature.newCheckout", System.getenv()
                .getOrDefault("FEATURE_NEW_CHECKOUT", "false"));
    }

    public String get(String key) {
        return config.getOrDefault(key, "");
    }

    public boolean isEnabled(String featureKey) {
        return Boolean.parseBoolean(config.getOrDefault(featureKey, "false"));
    }

    /**
     * TEST USE ONLY — resets the registry to a controlled state.
     * Call in @BeforeEach or @AfterEach to prevent state leakage between tests.
     */
    public void overrideForTest(Map<String, String> testConfig) {
        this.config = new HashMap<>(testConfig);
    }

    /** Restores config from the environment — call in @AfterEach. */
    public void resetToEnvironment() {
        this.config = new HashMap<>();
        loadFromEnvironment();
    }
}

// Test class — clean state before and after each test
class FeatureFlagTest {

    @BeforeEach
    void setUp() {
        ConfigurationRegistry.INSTANCE.overrideForTest(Map.of(
                "feature.newCheckout", "true",
                "db.url", "jdbc:h2:mem:testdb"
        ));
    }

    @AfterEach
    void tearDown() {
        ConfigurationRegistry.INSTANCE.resetToEnvironment();
    }

    @Test
    void newCheckoutFeatureShouldBeEnabled() {
        assertTrue(ConfigurationRegistry.INSTANCE.isEnabled("feature.newCheckout"));
    }
}

The reset approach is a pragmatic mitigation. If you find yourself writing many such overrides, it is a signal that the state would be better managed by a DI framework where each test can supply its own bean definition.

When Dependency Injection Makes Singleton Unnecessary

In any codebase using Spring, Guice, CDI, or Dagger, you should default to DI instead of hand-written Singletons for application-layer singletons:

java
// PREFERRED over GoF Singleton in Spring applications

// Spring manages exactly one instance of ConnectionService per ApplicationContext.
// The class has no static state, no getInstance(), and is trivially testable.
@Service  // @Scope("singleton") is the default
public class ConnectionService {

    private final DataSource dataSource;

    // Spring injects the DataSource — no Singleton.getInstance() anywhere
    public ConnectionService(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }
}

// Test — supply a mock DataSource, no global state to reset
class ConnectionServiceTest {

    @Test
    void shouldReturnConnectionFromDataSource() throws SQLException {
        DataSource mockDataSource = mock(DataSource.class);
        Connection mockConnection = mock(Connection.class);
        when(mockDataSource.getConnection()).thenReturn(mockConnection);

        ConnectionService service = new ConnectionService(mockDataSource);
        Connection conn = service.getConnection();

        assertSame(mockConnection, conn);
        // No static state, no reset hooks, no test ordering dependencies
    }
}

Reserve hand-written Singleton for components that must exist before the DI container starts (e.g., the logging framework used during container initialization) or for code outside of a DI-managed context entirely (e.g., a library distributed to other teams with no framework dependency).

When to Use

  • The resource is genuinely expensive to create (database connection pool, thread pool, cryptographic key material) and exactly one instance is correct by design.
  • The component is used across the entire application and a DI framework is not managing it.
  • The component is stateless after initialization (or has carefully synchronized mutable state).

When Not to Use

  • In application service code managed by a DI framework. Use the container's singleton scope instead.
  • To avoid passing a dependency through constructors. That coupling is a feature, not a problem — it makes dependencies visible and testable.
  • When the "single instance" constraint is not a real domain requirement, only a convenience.

Consequences

Benefits

BenefitExplanation
Controlled access to a shared resourceOne location creates and manages the instance; all callers share the same, correctly configured object.
Lazy initializationHolder idiom creates the instance only on first use, deferring startup cost until needed.
Reduced memory and connection overheadExpensive resources (connection pools, SDK clients) are created once and reused.

Trade-offs

Trade-offExplanation
Global stateAll callers share the same instance; bugs in one caller can corrupt state for all others.
Tight couplingCallers that reach Singleton.getInstance() directly cannot receive a different implementation in tests without reflection tricks.
TestabilityStatic access bypasses DI, making substitution hard. The resettable-instance mitigation is a workaround, not a solution.
Parallelism hazardStateful Singletons require careful synchronization; easy to get wrong under concurrent access.
  • Factory Method: A factory method that caches and reuses a previously constructed instance implements both Factory Method and Singleton. Logger.getLogger(name) in java.util.logging is an example.
  • Builder: AWS SDK client builders produce objects that are commonly held as Singletons for the lifetime of the application (e.g., S3Client built once at startup and never rebuilt).
  • Dependency Inversion Principle: Injecting dependencies through constructors eliminates the need for Singleton in most application code and makes the dependency explicit and substitutable.
  • Clean Architecture: In Clean Architecture, the composition root (outermost layer) is the appropriate place for Singleton infrastructure objects. Inner layers (use cases, entities) must never call Singleton.getInstance() directly.
  • Single Responsibility Principle: Singleton violates SRP when the class combines "enforce one instance" with application logic. Prefer keeping the pattern's enforcement in a minimal wrapper class and delegating all application logic to a plain collaborator.