Bean Scopes and Lifecycle

Scopes

ScopeInstancesTypical use
singleton (default)one per container, shared by everyoneservices, repositories, controllers (stateless)
prototypea new one every time it is requestedstateful helpers, builders
requestone per HTTP request (web apps)request-specific data
sessionone 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.