Singleton Pattern
The singleton pattern ensures exactly one instance of a type exists and gives a shared point of access to it. Spring applies this pattern by default: every bean is a singleton within its ApplicationContext unless you say otherwise. But Spring’s singleton is fundamentally better than the classic Gang of Four version — it keeps the “one shared instance” benefit while shedding the static-state baggage that makes the GoF form hard to test.
The classic GoF singleton
The textbook implementation hides the constructor and exposes a static accessor:
public class LegacyConfig {
private static final LegacyConfig INSTANCE = new LegacyConfig();
private LegacyConfig() { } // no one else can construct it
public static LegacyConfig getInstance() { // global access point
return INSTANCE;
}
}
Callers reach it with LegacyConfig.getInstance(). This works, but it has real problems: the instance is global static state, it cannot be swapped for a mock in tests, it couples every caller to the concrete class, and it makes lazy/eager and thread-safety decisions awkward to express.
Spring’s container-managed singleton
A Spring singleton is “one instance per container,” not “one instance per JVM enforced by a private constructor.” You write an ordinary class with a public constructor:
import org.springframework.stereotype.Service;
@Service // singleton scope by default
public class PricingService {
public BigDecimal quote(String sku) { /* ... */ }
}
The container creates a single PricingService and injects that same instance everywhere it is required. Equivalently, with an explicit scope:
import org.springframework.context.annotation.Scope;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
@Service
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) // the default — shown for clarity
public class PricingService { /* ... */ }
See bean scopes for the full set (prototype, request, session, and more).
Why the container-managed singleton wins
| Aspect | GoF singleton | Spring singleton bean |
|---|---|---|
| Instance control | Private constructor, static field | Container lifecycle |
| Testability | Hard — global static state | Easy — inject a mock via constructor |
| Coupling | Callers depend on concrete class | Callers depend on an interface/type |
| Lifecycle hooks | Manual | @PostConstruct, @PreDestroy |
| Scope flexibility | Fixed | Change with one @Scope annotation |
Because the bean is injected rather than fetched from a static method, your code is decoupled from the singleton mechanism entirely — the dependency injection pattern does the sharing for you.
Thread safety — the critical rule
A singleton bean is shared across every thread handling requests concurrently. Therefore: singleton beans must be stateless. Never store per-request mutable state in a field.
@Service
public class CounterService {
private int count; // BUG: shared mutable state
public int next() { return ++count; } // race condition under load
}
Two requests calling next() at once can corrupt count. Fix it by keeping mutable state out of the bean — pass it as parameters, return it, or use a thread-safe type:
@Service
public class CounterService {
private final AtomicInteger count = new AtomicInteger();
public int next() { return count.incrementAndGet(); } // safe
}
Warning: The single most common Spring concurrency bug is mutable instance fields on a singleton bean. Injected dependencies are fine (they are themselves stateless singletons); per-call data is not. If a bean genuinely needs per-request state, use a
request-scoped bean instead.
Note: Spring guarantees the bean is a single instance, but it does not synchronise your methods. Thread-safety of the logic inside the bean is your responsibility.
When you do want a fresh instance
If each use needs its own object, declare the bean prototype scoped — the container hands out a new instance on every lookup. This is the opposite of the singleton pattern and is covered in bean scopes.