What is a Spring Singleton?
When working with Spring, you often hear that beans are Singletons by default. But what does that actually mean, and how is it different from the classic Gang of Four (GoF) Singleton pattern?
Spring Singleton vs. Java Singleton
- Java Singleton (GoF): Ensures that one and only one instance of a class exists per ClassLoader.
- Spring Singleton: Ensures that one and only one instance of a bean exists per Spring IoC Container.
If you have multiple containers running in the same JVM, you will have multiple instances of your "Singleton" bean.
That sounds like a technicality until you hit it. Integration tests are the usual place: Spring caches an application context per unique test configuration, so a suite that uses three different @SpringBootTest setups is running three containers, each with its own instance of every singleton. Anything you cached in a static field is shared across all three. Anything you cached on the bean is not.
The other distinction worth making: a GoF singleton enforces its own uniqueness with a private constructor, so nothing can create a second one. A Spring singleton is an ordinary class with an ordinary public constructor. Nothing stops you writing new UserService(...) yourself, and in a unit test that is exactly what you should do. Uniqueness is a property of the container, not of the class.
Default Scope
By default, every @Bean or @Component you define is a singleton. Spring creates it once during startup and caches it.
@Component
public class UserService {
// This constructor is called only once by Spring
public UserService() {
System.out.println("UserService initialized!");
}
}
If you have not met bean registration yet, start here.
Why Spring Uses Singletons
- Performance: Creating objects is expensive. Reuse saves memory and CPU.
- Statelessness: Service and Repository layers are typically stateless, making them perfect candidates for singletons.
- Fail fast: Because singletons are created eagerly at startup, a missing dependency or bad configuration breaks the deploy rather than the first request that touches it.
Providing a Singleton Bean
You don't need to do anything special! It's the default.
@Configuration
public class AppConfig {
// Explicitly a singleton (default)
@Bean
@Scope("singleton")
public MyService myService() {
return new MyServiceImpl();
}
}
Thread Safety Warning
Since a single instance is shared across multiple threads (e.g., multiple HTTP requests), Spring Singleton beans must be stateless.
Bad Practice (Stateful Singleton):
@Service
public class TicketService {
private int ticketCount = 0; // DANGEROUS! Shared state
public int bookTicket() {
return ++ticketCount; // Race condition heaven
}
}
Good Practice (Stateless):
@Service
public class TicketService {
// State is kept in the database, not in the bean
private final TicketRepository repo;
public TicketService(TicketRepository repo) {
this.repo = repo;
}
public int bookTicket() {
// ... DB operations ...
}
}
"Stateless" here means no mutable state. Injected collaborators held in final fields are fine, because they are assigned once during startup and never written again. What is not fine is anything that changes per request: a counter, a cached user, a StringBuilder reused across calls, a field you set at the top of a method and read at the bottom.
The reason this bug is so nasty is that it never shows up locally. One developer clicking through the UI generates one request at a time, and the code looks correct. It only breaks under concurrency, in production, intermittently.
The prototype-in-singleton trap
Here is the classic follow-up question, and it catches almost everyone.
You define a prototype bean, expecting a fresh instance each time:
@Component
@Scope("prototype")
public class ReportBuilder { }
Then you inject it into a singleton:
@Service
public class ReportService {
private final ReportBuilder builder; // injected ONCE
public ReportService(ReportBuilder builder) {
this.builder = builder;
}
}
ReportService is a singleton, so its constructor runs exactly once, so builder is resolved exactly once. Every call for the rest of the application's life uses that same ReportBuilder. The prototype scope has been silently defeated.
The fix is to ask the container for an instance at the point of use, rather than holding one:
@Service
public class ReportService {
private final ObjectProvider<ReportBuilder> builders;
public ReportService(ObjectProvider<ReportBuilder> builders) {
this.builders = builders;
}
public Report build() {
ReportBuilder builder = builders.getObject(); // fresh every call
return builder.build();
}
}
@Lookup methods and @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) solve the same problem in different ways, but ObjectProvider is the one that reads most clearly.
Eager creation, and when to defer it
Singletons are instantiated at startup, in dependency order. That is usually what you want. If one bean is genuinely expensive and rarely used, @Lazy defers its creation to first use:
@Service
@Lazy
public class ExpensiveReportGenerator { }
Use it sparingly. Deferring creation also defers the failure, which trades a broken startup for a broken request at 3am.