Careeroza
Bean Scopes and Lifecycle
Scopes
| Scope | Instances | Typical use |
|---|---|---|
singleton (default) | one per container, shared by everyone | services, repositories, controllers (stateless) |
prototype | a new one every time it is requested | stateful helpers, builders |
request | one per HTTP request (web apps) | request-specific data |
session | one per HTTP session (web apps) | rarely used in REST APIs |
@Component
@Scope("prototype") // or @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class InvoiceBuilder { ... }Note: Because singletons are shared across all request threads, never keep per-user data in a field of a singleton (for example "current cart"). Keep services stateless and store data in the database or pass it as parameters.
Singleton vs Prototype in Plain Java
Example
A tiny container that supports both scopes, showing why the difference matters:
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public class Main {
static int created = 0;
public static void main(String[] args) {
Container ctx = new Container();
ctx.register("productService", "singleton", () -> new Bean("ProductService#" + (++created)));
ctx.register("invoiceBuilder", "prototype", () -> new Bean("InvoiceBuilder#" + (++created)));
Object a = ctx.get("productService"), b = ctx.get("productService");
Object c = ctx.get("invoiceBuilder"), d = ctx.get("invoiceBuilder");
System.out.println("singleton: " + a + ", " + b + " same=" + (a == b));
System.out.println("prototype: " + c + ", " + d + " same=" + (c == d));
System.out.println("objects created: " + created);
}
}
record Bean(String name) {
public String toString() { return name; }
}
class Container {
private final Map<String, Supplier<Object>> factories = new HashMap<>();
private final Map<String, String> scopes = new HashMap<>();
private final Map<String, Object> singletons = new HashMap<>();
void register(String name, String scope, Supplier<Object> factory) {
factories.put(name, factory);
scopes.put(name, scope);
}
Object get(String name) {
if (scopes.get(name).equals("singleton")) {
return singletons.computeIfAbsent(name, n -> factories.get(n).get());
}
return factories.get(name).get(); // prototype: new every time
}
}Output
singleton: ProductService#1, ProductService#1 same=true prototype: InvoiceBuilder#2, InvoiceBuilder#3 same=false objects created: 3
Lifecycle Callbacks
Spring creates a bean, injects its dependencies, then calls your @PostConstruct method (good for warming a cache or checking config). On shutdown it calls @PreDestroy (close connections, flush data). Both come from jakarta.annotation.
constructor -> dependencies injected -> @PostConstruct -> bean in use ... -> @PreDestroy -> gone
(app shutdown, singletons only)import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class CategoryCache {
private static final Logger log = LoggerFactory.getLogger(CategoryCache.class);
private final Map<Long, String> categories = new ConcurrentHashMap<>();
@PostConstruct
void load() {
categories.put(1L, "Electronics");
categories.put(2L, "Books");
log.info("Category cache loaded: {} entries", categories.size());
}
@PreDestroy
void clear() {
log.info("Clearing category cache");
categories.clear();
}
}Example
./mvnw spring-boot:run (then press Ctrl+C)Example output
... INFO c.s.config.CategoryCache : Category cache loaded: 2 entries ... INFO c.s.ShopeaseApplication : Started ShopeaseApplication in 1.874 seconds ^C ... INFO c.s.config.CategoryCache : Clearing category cache
Note: Spring does not call @PreDestroy on prototype beans; whoever asked for them owns them.