← All patterns

Singleton

Creational · view source on GitHub

Read this in: English · Português · Español

Category: Creational

The problem

Some resources genuinely need exactly one shared instance per process: a configuration registry, a connection pool, a regulatory limit table. If every caller constructs its own copy, you either waste the cost of building it repeatedly or — worse — different parts of the system end up looking at different, possibly stale, copies of what should be one source of truth. Getting this "one instance" guarantee right under concurrent access is harder than it looks: a naive if (instance == null) instance = new Thing() check has a race where two threads can both pass the null check before either has assigned the field.

The solution

Hide the constructor, expose a single access point, and make that access point safe under concurrent first use.

classDiagram
    class LazyThreadSafeSingleton {
        -static volatile instance
        -LazyThreadSafeSingleton()
        +static getInstance() LazyThreadSafeSingleton
        +getSetting(key) String
    }
    class Caller
    Caller --> LazyThreadSafeSingleton : getInstance()

Classic example

classic/LazyThreadSafeSingleton is the textbook double-checked-locking singleton: a volatile field, a null check outside the lock (fast path once initialized), and a second null check inside a synchronized block (so only the first thread through actually constructs the instance). The field must be volatile — without it, a thread could observe a non-null reference to an object whose constructor hasn't finished writing its fields yet, because the JVM is allowed to reorder the write to instance ahead of the writes happening inside the constructor.

LazyThreadSafeSingletonTest fires 50 threads at getInstance() simultaneously (synchronized with a CountDownLatch so they actually contend on the first call) and asserts every thread observed the exact same instance.

Applied example: PIX regulatory limit registry

applied/HandRolledLimitRegistry models a central table of BACEN-defined PIX limits (daily cap, reduced nighttime cap) that every concurrent transaction validator reads. Reloading these limits per validation call would be wasteful, and validators running concurrently must all see the same values — exactly the scenario the pattern exists for, applied with the same double-checked-locking mechanics as the classic example.

applied/SpringManagedLimitRegistry implements the same LimitRegistry contract with no singleton machinery at all — it's a plain class. The single-instance guarantee comes entirely from Spring's default bean scope (singleton), wired in SingletonRegistryConfig. SpringManagedLimitRegistryTest proves it: two context.getBean(...) calls return the same reference, with zero hand-written locking code. Same guarantee, two ways to get it — one you build yourself, one a container gives you for free once you accept the dependency.

When not to use it

Test coverage

97% instruction coverage, 87% branch coverage (JaCoCo). Reproduce it yourself:

./gradlew :creational:singleton:jacocoTestReport

Report at creational/singleton/build/reports/jacoco/test/html/index.html.

Further reading

Unit tests

src/test/java/com/designpatterns/creational/singleton/classic/LazyThreadSafeSingletonTest.java
package com.designpatterns.creational.singleton.classic;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Ordered on purpose: the concurrency test below must be the one that constructs the
 * singleton (all 50 threads racing through the synchronized block), not a coincidence of
 * JUnit's default method order. If {@code exposesTheSettingsLoadedAtConstruction} ran first,
 * it would construct the instance single-threaded, and the "concurrent" test would never
 * actually exercise contention on the lock - it would just find the instance already there.
 */
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class LazyThreadSafeSingletonTest {

    @Test
    @Order(1)
    void returnsTheSameInstanceUnderConcurrentFirstAccess() throws InterruptedException {
        int threadCount = 50;
        ExecutorService pool = Executors.newFixedThreadPool(threadCount);
        CountDownLatch ready = new CountDownLatch(threadCount);
        CountDownLatch start = new CountDownLatch(1);
        Set<LazyThreadSafeSingleton> seenInstances = Collections.newSetFromMap(new ConcurrentHashMap<>());

        try {
            for (int i = 0; i < threadCount; i++) {
                pool.submit(() -> {
                    ready.countDown();
                    awaitUninterruptibly(start);
                    seenInstances.add(LazyThreadSafeSingleton.getInstance());
                });
            }

            assertThat(ready.await(5, TimeUnit.SECONDS)).isTrue();
            start.countDown();
            pool.shutdown();
            assertThat(pool.awaitTermination(5, TimeUnit.SECONDS)).isTrue();
        } finally {
            pool.shutdownNow();
        }

        assertThat(seenInstances).hasSize(1);
    }

    @Test
    @Order(2)
    void exposesTheSettingsLoadedAtConstruction() {
        assertThat(LazyThreadSafeSingleton.getInstance().getSetting("environment")).isEqualTo("production");
    }

    private static void awaitUninterruptibly(CountDownLatch latch) {
        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
src/test/java/com/designpatterns/creational/singleton/applied/HandRolledLimitRegistryTest.java
package com.designpatterns.creational.singleton.applied;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Ordered on purpose - see {@code LazyThreadSafeSingletonTest} for why: the concurrency test
 * must be the one that actually constructs the singleton under contention, not whichever test
 * happens to call getInstance() first.
 */
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class HandRolledLimitRegistryTest {

    @Test
    @Order(1)
    void everyConcurrentValidatorSeesTheSameRegistryInstance() throws InterruptedException {
        int threadCount = 50;
        ExecutorService pool = Executors.newFixedThreadPool(threadCount);
        CountDownLatch ready = new CountDownLatch(threadCount);
        CountDownLatch start = new CountDownLatch(1);
        Set<HandRolledLimitRegistry> seenInstances = Collections.newSetFromMap(new ConcurrentHashMap<>());

        try {
            for (int i = 0; i < threadCount; i++) {
                pool.submit(() -> {
                    ready.countDown();
                    awaitUninterruptibly(start);
                    seenInstances.add(HandRolledLimitRegistry.getInstance());
                });
            }

            assertThat(ready.await(5, TimeUnit.SECONDS)).isTrue();
            start.countDown();
            pool.shutdown();
            assertThat(pool.awaitTermination(5, TimeUnit.SECONDS)).isTrue();
        } finally {
            pool.shutdownNow();
        }

        assertThat(seenInstances).hasSize(1);
    }

    @Test
    @Order(2)
    void exposesTheRegulatoryLimits() {
        LimitRegistry registry = HandRolledLimitRegistry.getInstance();

        assertThat(registry.dailyLimitCents()).isEqualTo(100_000_00L);
        assertThat(registry.nightlyLimitCents()).isEqualTo(1_000_00L);
    }

    private static void awaitUninterruptibly(CountDownLatch latch) {
        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
src/test/java/com/designpatterns/creational/singleton/applied/SpringManagedLimitRegistryTest.java
package com.designpatterns.creational.singleton.applied;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;

class SpringManagedLimitRegistryTest {

    private AnnotationConfigApplicationContext context;

    @AfterEach
    void closeContext() {
        if (context != null) {
            context.close();
        }
    }

    @Test
    void springReturnsTheSameBeanInstanceOnEveryLookup() {
        context = new AnnotationConfigApplicationContext(SingletonRegistryConfig.class);

        SpringManagedLimitRegistry first = context.getBean(SpringManagedLimitRegistry.class);
        SpringManagedLimitRegistry second = context.getBean(SpringManagedLimitRegistry.class);

        assertThat(first).isSameAs(second);
        assertThat(first.dailyLimitCents()).isEqualTo(100_000_00L);
    }
}

View full JaCoCo coverage report →