Skip to content
Spring Boot sb design-patterns 3 min read

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

AspectGoF singletonSpring singleton bean
Instance controlPrivate constructor, static fieldContainer lifecycle
TestabilityHard — global static stateEasy — inject a mock via constructor
CouplingCallers depend on concrete classCallers depend on an interface/type
Lifecycle hooksManual@PostConstruct, @PreDestroy
Scope flexibilityFixedChange 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.

Last updated June 13, 2026
Was this helpful?