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
- If the "shared instance" requirement is really just "convenient global access," prefer passing the dependency explicitly (constructor injection) — singletons hide dependencies and make tests harder to isolate.
- If you're already inside a DI container (Spring, in this repo's own example), let the
container manage the singleton scope; hand-rolled
getInstance()code next to a container is redundant and confusing. - If the "single instance" needs to vary per-request, per-tenant, or per-thread, this is the
wrong scope entirely — reach for a scoped bean or a
ThreadLocalinstead.
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
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. — Chapter 3 formalizes Singleton itself; every module in this repo traces back to this book.
- Pugh, W., Bacon, D., Bloch, J., et al. "Double-Checked Locking is Broken" Declaration.
University of Maryland. — the exact reasoning behind why
getInstance()needs more than a null check under the pre-Java-5 memory model, and why a plain (non-volatile) field is not enough. - Manson, J., Pugh, W., & Adve, S. V. (2005). "The Java Memory Model." In Proceedings of the
32nd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL '05),
378–391. — the JSR-133 formalization that makes
volatilesufficient to fix double-checked locking; this is the paper the fix inLazyThreadSafeSingletonultimately rests on.
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);
}
}