Spring Beans Explained:

Configuration, IoC Container, Scopes, and Lifecycle

Spring Beans are the core building blocks of any Spring Framework or Spring Boot application. A Spring Bean is an object whose creation, dependencies, and lifecycle are managed by the Spring IoC Container. For developers, understanding how beans are instantiated, initialized, and destroyed as well as how to configure their scope is essential for building modular, maintainable, and testable applications.

This article provides practical examples showing how Spring Beans are created and managed and how their lifecycle and scope can be customized. With clear explanations and hands-on demonstrations, it helps you build clean, modular, and testable Spring Boot applications.

Overview:

  • Core Concepts: What a Spring Bean is and its role in dependency injection.
  • Configuration: Defining beans with annotations or XML.
  • Annotations: Using @Component, @Controller, @Service, @Repository, and other Spring stereotypes.
  • Scopes: Singleton, prototype, and other bean scopes.
  • Lifecycle: Creation, initialization, and destruction of beans.
  • Practical Example: A simple layered architecture example demonstrating Spring Beans in action.

I hope this helps you understand Spring Beans and use them effectively πŸ™‚

Quickstart

You can quickly get started with the Spring Beans Explained by following these steps:

1. Ensure a JDK is available to build and run the code. Temurin, based on OpenJDK and available from adoptium.net, can be used.

2. Get the source code by cloning the repository using Git, or downloading it as a ZIP archive from Spring Beans Explained repository.

git clone https://github.com/kamilmazurek/spring-beans-explained

If you downloaded the ZIP file, extract it and navigate to the spring-beans-explained directory.

3. Start the application:

mvnw spring-boot:run

4. Verify that the application is running by sending a GET request to the following URL (you can simply open it in a browser):

http://localhost:8080/api/items/1

The following item should be returned in the response:

{
    "id": 1,
    "name":"Item A"
}

5. Keep on reading to see how this works πŸš€.

What Is a Spring Bean?

In Spring, a bean is simply a Java object managed by the Spring IoC (Inversion of Control) container. Instead of manually creating and wiring objects, you can let Spring handle the instantiation, configuration, dependency injection, and lifecycle of those objects.

In simpler terms, a Spring Bean can be thought of as an object that Spring creates and manages for you. This allows developers to focus on business logic rather than on object creation and dependency management.

Spring creates and manages beans based on how developers define them. One of the most common ways is to use the @Bean annotation inside a class annotated with @Configuration.

Below is an example that you can find in the pl.kamilmazurek.example.beans.mybean package.

@Configuration
public class MyConfiguration {

    @Bean
    public MyBean myBean() {
        return new MyBean();
    }

}

In this example, MyConfiguration defines a Spring Bean by returning a new instance of MyBean. To see what MyBean actually looks like, here's its class definition:

@Log4j2
public class MyBean {

    public MyBean() {
        log.info("MyBean instance created");
    }

}

Here, MyBean is a Spring Bean. When the application starts, Spring detects the @Configuration class, calls the myBean() method, and registers its return value as a managed bean in the IoC container. Once registered, the bean becomes available for dependency injection throughout the application, so it can be injected into other beans or components, e.g. by using @Autowired or constructor injection.

If you want to verify that it works and the bean is actually created, you can start the application with:

mvnw spring-boot:run

Then, in the logs, you should be able to find an entry like this:

INFO  pl.kamilmazurek.example.beans.mybean.MyBean: MyBean instance created

This approach enables loose coupling, easier testing, and more maintainable applications.

A Spring Bean typically:

  • Is a POJO (Plain Old Java Object) registered with the Spring container.
  • Has its dependencies injected automatically by Spring.
  • Lives within a defined Spring Bean scope (e.g., singleton, prototype, request, session).
  • Is configured using annotations like @Component, @Service, or @Bean, or through XML configuration.

Why Use Spring Beans?

Spring Beans are useful because they allow the Spring Framework to handle object creation, configuration, and lifecycle management, so developers can focus on writing business logic instead of manually wiring objects.

Here's why I find Spring Beans so helpful:

  • Automatic Dependency Management: Spring can inject dependencies automatically, removing manual wiring.
  • Loose Coupling: Letting Spring manage dependencies makes code more modular and easier to maintain.
  • Lifecycle Management: Spring handles initialization and destruction, so setup or cleanup logic doesn't clutter business code.
  • Testability: Beans can be swapped with mocks or stubs for easier unit testing.
  • Scope Flexibility: Beans can be singleton, prototype, request, or session scoped, giving fine-grained control.
  • Consistent Configuration: Spring provides a unified way to manage objects via annotations or XML.

Spring Beans are powerful, but their benefits might not be obvious at first. Here is a practical example that starts with a simple System.out, then moves to using Spring Beans, and finally exposes functionality through a REST API with just a few lines of code.

Below is an example that you can find in the pl.kamilmazurek.example.beans.greeting package.

Let's say we want our application to display a greeting message.

Step 1: Using System.out

In a plain Java application, you can print a greeting like this:

System.out.println("Hello from the application!");
Step 2: Using a POJO for Flexibility

In real-world applications, it's common to keep logic flexible and reusable. Let's move the greeting logic into a Greeter class:

public class Greeter {

    public String createHelloMessage() {
        return "Hello!";
    }

}

In a plain Java application, this class can be used like this:

var greeter = new Greeter();
System.out.println(greeter.createHelloMessage());
Step 3: Using Spring Bean

With Spring, the framework can manage the Greeter instance for you:

@Configuration
public class GreeterConfiguration {

    @Bean
    public Greeter greeter() {
        return new Greeter();
    }

}

Spring will create the Greeter instance, manage its lifecycle, and make it available for dependency injection wherever it is needed.

Now that Greeter is Spring-managed, we can use it in other components. Next, we'll see how to expose its functionality through a REST API.

Step 4: Exposing the Greeting via REST API

With Spring Boot, we can now easily expose the greeting through a REST endpoint:

@RestController
public class GreeterRestController {

    private final Greeter greeter;

    public GreeterRestController(Greeter greeter) {
        this.greeter = greeter;
    }

    @GetMapping("/hello")
    public String sayHello() {
        return greeter.createHelloMessage();
    }

}

To make this work, Spring Boot Web Starter can be used, for example, by including the dependency in pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

With just a few lines of code, this simple POJO is now managed by Spring, with automatic dependency injection and lifecycle management. This lets developers focus on writing business logic and reusable components while Spring handles wiring and managing beans behind the scenes.

This approach makes the code easier to maintain, test, and extend, and allows you to use many Spring mechanisms along with their related libraries and solutions, for example, to expose functionality like the greeting message through a REST API without much effort.

To see this in action, run the application with:

mvnw spring-boot:run

Then open the following URL in your browser:

http://localhost:8080/api/greetings/hello

You should see the Hello! message displayed πŸ™‚

Understanding the Spring IoC Container

The Spring IoC Container is a core component of the Spring Framework that creates, manages, and wires beans. Instead of manually instantiating and linking objects, the container handles it automatically.

It follows the Inversion of Control (IoC) principle, where the framework, rather than the application, is responsible for object creation and dependency injection. This approach keeps code focused on business logic and makes it more modular, testable, and maintainable.

Why the Spring IoC Container Is Useful
  • Automatic Object Instantiation: Creates and configures beans based on annotations, XML, or Java classes.
  • Dependency Injection (DI): Injects required dependencies into beans so classes don't have to construct or locate them manually.
  • Lifecycle Handling: Manages bean initialization and, where applicable, destruction.
  • Scope Flexibility: Supports singleton, prototype, request, or session scopes depending on application needs.

I think of the IoC container as a combination of a "bean factory" and a "dependency manager". Developers define which beans exist and how they depend on each other, and the container takes care of their creation, management, and wiring automatically.

Dependency Injection and Spring Beans

One of the main benefits of the Spring IoC Container is dependency injection (DI), which allows an object's dependencies to be provided from the outside, rather than having the object create them itself.

Dependencies between beans can be configured in multiple ways, including:

  • Constructor Injection: Providing dependencies via the bean's constructor.
  • Injection with @Autowired: Spring injects the required bean via a setter, field, or constructor.
  • Java Configuration: Defining beans in a @Configuration class so Spring can manage their dependencies.
  • XML Configuration: Declaring beans and their dependencies in an XML file using <bean> elements with property or constructor-arg.

To better understand the concept, let's see how it can be done using a @Configuration class.

Below is an example that you can find in the pl.kamilmazurek.example.beans.time package.

Suppose we have a simple TimeProvider class that returns the current time:

public class TimeProvider {

    public LocalDateTime now() {
        return LocalDateTime.now();
    }

}

Next, we want aTimeLogger class that uses TimeProvider to log the current time:

@Slf4j
public class TimeLogger {

    private final TimeProvider timeProvider;

    public TimeLogger(TimeProvider timeProvider) {
        this.timeProvider = timeProvider;
    }

    public void logCurrentTime() {
        log.info("Current time: " + timeProvider.now());
    }

}

We can write a configuration class to tell Spring to inject the TimeProvider bean into the TimeLogger bean, like this:

@Configuration
public class TimeConfiguration {

    @Bean
    public TimeProvider timeProvider() {
        return new TimeProvider();
    }

    @Bean
    public TimeLogger timeLogger(TimeProvider timeProvider) {
        return new TimeLogger(timeProvider);
    }

}

Here, the Spring IoC Container:

  • Creates a TimeProvider bean.
  • Creates a TimeLogger bean and automatically injects the TimeProvider into it.

The classes don't construct or pass dependencies themselves. They simply declare what they need, and the IoC container provides it.

This approach reduces boilerplate, increases modularity, and simplifies testing by allowing dependencies to be replaced with mocks or stubs. The IoC container manages beans automatically, letting developers focus on business logic

There is also a Runner class that calls the logCurrentTime() method, so the current time will be logged during application startup:

@Component
@AllArgsConstructor
public class Runner implements ApplicationRunner {

    private final TimeLogger timeLogger;

    private final UserService userService;

    @Override
    public void run(ApplicationArguments args) {
        timeLogger.logCurrentTime();
        userService.logExistingUsers();
    }

}

To see it in action, start the application:

mvnw spring-boot:run

You should then see a log entry similar to the following:

INFO  pl.kamilmazurek.example.beans.time.TimeLogger: Current time: 2025-12-07T19:37:52.921718300

Configuring Beans with Annotations or XML

Spring provides several ways to define and configure beans, allowing developers to choose the approach that best fits their application's complexity and design preferences.

The three most common methods are:

Although XML configuration remains supported for legacy applications, modern Spring Boot projects typically favor annotation-driven and Java-based configurations for better readability, maintainability, and seamless integration with auto-configuration.

Which Configuration Type Should Be Used?

Spring provides flexibility in how beans are defined and managed, with each configuration style offering its own advantages and ideal use cases.

Annotation-based configuration is the preferred approach for most modern Spring applications. It allows concise definitions, integrates seamlessly with Spring Boot's auto-configuration, and promotes a clean, component-oriented structure that is easy to read and maintain.

Java-based configuration complements annotation-driven approaches by providing explicit, type-safe bean definitions in code. It is particularly useful when integrating third-party libraries, adding custom initialization logic, or when fine-grained control over bean creation is needed.

XML-based configuration provides explicit control and works well for legacy systems or when integrating with older Spring applications. However, it can become verbose and harder to maintain as a project grows.

In modern Spring Boot development, annotation-driven and Java-based configurations are considered the standard. For now, XML is still supported, but I think it is mainly used in legacy systems or specific integration scenarios.

Annotation-Based Configuration

Annotation-based configuration allows developers to mark classes with specific annotations, enabling Spring to automatically detect and register them as beans during component scanning. I think this is the most common and convenient way to define beans in modern Spring applications

Developers typically use Spring Stereotypes, such as:

  • @Component: Generic stereotype for any Spring-managed component.
  • @Service: Specialization of @Component used to mark classes that hold business logic.
  • @Repository: Indicates a component that handles data access and enables automatic exception translation.
  • @Controller: Identifies a class that handles web requests in Spring MVC applications.
  • @RestController: Combination of @Controller and @ResponseBody, typically used for RESTful web services.

Below is an example from the pl.kamilmazurek.example.beans.user package that demonstrates how to define a UserServiceand inject a UserRepository into it using annotations and constructor-based injection.

In this example, the UserService receives a UserRepository through its constructor. It then fetches all existing users and logs their logins:

@Slf4j
@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void logExistingUsers() {
        var users = userRepository.findAll().stream().map(UserEntity::getLogin).toList();
        log.info("Existing users: " + String.join(", ", users));
    }

}

Below is the correspondingUserRepository, a simple Spring Data JPA interface used to accessUserEntity records:

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
}

Below is the UserEntity class, a simple JPA entity representing a user record in the users table:

@Data
@Entity
@Table(name = "users")
public class UserEntity {

    @Id
    private Long id;

    private String login;

}

This example uses Spring Data JPA, added by the following Maven dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

For simplicity, the example runs on an in-memory H2 database, with Spring Boot configured to use H2 and load initial data from users.sql:

spring:
    datasource:
        url: jdbc:h2:mem:spring-beans-explained
    jpa:
        defer-datasource-initialization: true
    sql:
        init:
        data-locations:
            - classpath*:users.sql

Similarly to the previous example, the Runner class calls the logExistingUsers() method, so the existing users are logged during application startup:

@Component
@AllArgsConstructor
public class Runner implements ApplicationRunner {

    private final TimeLogger timeLogger;

    private final UserService userService;

    @Override
    public void run(ApplicationArguments args) {
        timeLogger.logCurrentTime();
        userService.logExistingUsers();
    }

}

As a result, after running the application with mvnw spring-boot:run, you should see a log entry like:

INFO  pl.kamilmazurek.example.beans.user.UserService: Existing users: test-user-a, test-user-b, test-user-c

Note: Spring Boot automatically performs component scanning for the package containing the main application class (and its subpackages). However, if you need to manually enable automatic detection of annotated classes, you can use the@ComponentScan annotation:

@Configuration
@ComponentScan(basePackages = "package.goes.here")
public class AppConfig {

    //some configuration

}

Annotation-based configuration offers a lightweight, easy-to-start approach that fits naturally within microservice architectures. That said, in very large applications, extensive use of automatic scanning can make it harder to trace dependencies and manage configurations explicitly.

Java-Based Configuration

Java-based configuration defines beans using configuration classes annotated with @Configuration and methods annotated with @Bean. This approach provides a clear and type-safe way to configure beans without relying on XML or component scanning.

As shown in the Dependency Injection and Spring Beans section, the TimeConfiguration class in the pl.kamilmazurek.example.beans.time package demonstrates how to define beans and wire their dependencies using @Bean methods.

@Configuration
public class TimeConfiguration {

    @Bean
    public TimeProvider timeProvider() {
        return new TimeProvider();
    }

    @Bean
    public TimeLogger timeLogger(TimeProvider timeProvider) {
        return new TimeLogger(timeProvider);
    }

}

Java-based configuration gives developers explicit control over bean creation and wiring. It is particularly useful when integrating third-party libraries, applying custom initialization logic, or when precise control over the bean lifecycle is needed.

XML-Based Configuration

In the early versions of Spring, beans were typically defined in XML configuration files using the <bean> element. This approach allowed developers to explicitly declare each bean and its dependencies.

The XML configuration below reflects the same setup as the previously shownTimeConfiguration example:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="timeProvider" class="pl.kamilmazurek.example.beans.time.TimeProvider"/>

    <bean id="timeLogger" class="pl.kamilmazurek.example.beans.time.TimeLogger">
        <constructor-arg ref="timeProvider"/>
    </bean>

</beans>

With this configuration, the Spring IoC container:

  • Creates aTimeProvider bean.
  • Creates aTimeLogger bean and injects the TimeProvider into it via the constructor.

Depending on the application setup, an XML configuration file can be loaded in several ways. For example, something like:

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
TimeLogger timeLogger = context.getBean("timeLogger", TimeLogger.class);
timeLogger.logCurrentTime();

XML-based configuration offers clarity and flexibility, as all beans and dependencies are explicitly defined. However, it can become verbose and harder to maintain as the application grows, which is why I think modern Spring Boot projects prefer annotation-driven or Java-based configuration.

Spring Stereotypes

Spring stereotype annotations are special markers for classes. They indicate that the class is managed by the Spring container. These annotations allow Spring to automatically detect and register classes as beans during component scanning, reducing the need for explicit configuration.

A basic building block of this mechanism is the @Component annotation, which serves as a generic stereotype for any Spring-managed component. Spring also provides specialized annotations built on top of @Component. These help organize classes by their responsibilities, making the application structure clearer and easier to maintain.

Common stereotypes include:

  • @Component: General-purpose annotation for any Spring-managed bean.
  • @Repository: Marks a class as a data access or persistence layer component.
  • @Service: Indicates that the class holds business logic or service operations.
  • @Controller: Marks a class as a web controller, typically used in Spring MVC to handle HTTP requests.
  • @RestController: A combination of @Controller and @ResponseBody, used to create RESTful web services.

The following example from the pl.kamilmazurek.example.beans.order package demonstrates how beans of different types, defined with multiple stereotypes, work together in practice.

@Component

Marks a class as a general-purpose Spring bean. It is the base stereotype for all other specialized annotations.

@Component
public class OrderValidator {

    public boolean isValid(OrderEntity orderEntity) {
        if (orderEntity == null) {
            return false;
        }

        return hasProducts(orderEntity);
    }

    private boolean hasProducts(OrderEntity orderEntity) {
        return orderEntity.getProducts() != null && !orderEntity.getProducts().isEmpty();
    }

}

@Repository

Marks a persistence (data access) component, including Spring Data repository interfaces. Spring can automatically translate persistence-related exceptions into its unified DataAccessException hierarchy.

@Repository
public interface OrderRepository extends JpaRepository<OrderEntity, Long> {
}

For a bigger picture, sample OrderEntity may look as follows:

@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "orders")
public class OrderEntity {

    @Id
    private Long id;

    private LocalDate orderDate;

    private double totalAmount;

    @ElementCollection
    @Column(name = "product")
    @CollectionTable(name = "order_products", joinColumns = @JoinColumn(name = "order_id"))
    private List<String> products;

}

@Service

A specialization of @Component used for classes containing business logic or service-related operations. This helps organize large applications and makes their structure easier to understand.

@Service
public class OrderService {

    private final OrderValidator orderValidator;

    private final OrderRepository orderRepository;

    public OrderService(OrderValidator orderValidator, OrderRepository orderRepository) {
        this.orderValidator = orderValidator;
        this.orderRepository = orderRepository;
    }

    public List<OrderEntity> getOrders() {
        return orderRepository.findAll();
    }

    public List<OrderEntity> getValidOrders() {
        return orderRepository.findAll().stream().filter(orderValidator::isValid).toList();
    }

}

@Controller

Marks a class as a controller, commonly used in Spring MVC, where its methods handle HTTP requests and return views or data.

@Controller
@RequestMapping("/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping("")
    public ModelAndView showOrders() {
        var modelAndView = new ModelAndView("orders");

        modelAndView.addObject("name", "Orders");
        modelAndView.addObject("orders", orderService.getOrders());

        return modelAndView;
    }

    @GetMapping("/valid")
    public ModelAndView showValidOrders() {
        var modelAndView = new ModelAndView("orders");

        modelAndView.addObject("name", "Valid Orders");
        modelAndView.addObject("orders", orderService.getValidOrders());

        return modelAndView;
    }

}

In this example, the controller provides data to the template, which Spring renders on the server before sending the final HTML to the browser.

This rendering is handled by a template engine such as Thymeleaf. In a Spring Boot-based application, this can be configured by adding the following dependency to pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

In this example, the template is placed in src/main/resources/templates/orders.html and uses a CSS stylesheet from src/main/resources/static/css/styles.css. The same template is used for both all orders and valid orders, with the displayed data changing based on the controller method that handles the request.

When the application is running, the results can be viewed at the following addresses:

  • All orders: http://localhost:8080/orders
  • Valid orders: http://localhost:8080/orders/valid

The template will display either all orders or only valid orders, depending on which URL is accessed. The controller provides the data by retrieving it from the service, which in turn obtains it from the repository that reads it from the database.

@RestController

A convenient specialization of @Controller that combines @Controller and @ResponseBody. It is commonly used in RESTful web services to return data directly as JSON or XML.

@RestController
@RequestMapping("/api/orders")
public class OrderRestController {

    private final OrderService orderService;

    public OrderRestController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping
    public List<OrderEntity> getAllOrders() {
        return orderService.getOrders();
    }

    @GetMapping("/valid")
    public List<OrderEntity> getValidOrders() {
        return orderService.getValidOrders();
    }

}

When the application is running, responses from the REST endpoints can be viewed by accessing them in a browser or any HTTP client:

  • All orders: http://localhost:8080/api/orders
  • Valid orders: http://localhost:8080/api/orders/valid

The RestController retrieves the data from the service layer, which obtains it from the repository layer that reads it from the database. The server then returns the result in JSON format.


These stereotype annotations help organize an application's structure by clearly defining the responsibilities of different components such as controllers, services, and repositories. They make the application easier to understand and maintain by providing meaningful context about each component's role within the overall architecture.

Spring Bean Scopes

In Spring, bean scope determines how many instances of a bean are created and how long they live within the application context. Understanding scopes is essential for managing state, memory usage, and lifecycle behavior of your beans.

Spring provides several built-in bean scopes, giving developers fine-grained control over how and when bean instances are created:

  • Singleton Scope: The default Spring scope, with one shared instance per container. Useful for stateless services and shared resources.
  • Prototype Scope: A new instance is created every time the bean is requested. Useful for stateful or temporary objects.
  • Request Scope: A new instance is created for each HTTP request. Useful for request-specific data.
  • Session Scope: One instance per HTTP session, shared across multiple requests from the same user. Useful for user-specific state.
  • Application Scope: One instance per web application context, shared across all requests and sessions. Useful for global resources or caches.
  • WebSocket Scope: One instance per WebSocket session. Useful for maintaining session-specific state in real-time applications.

For my thoughts on when to use each scope, please see When to Use Each Scope.

Singleton Scope

Singleton is the default scope in Spring. Only one shared instance of the bean is created per Spring IoC container. Every time the bean is requested from the same container, the same instance is returned.

For example, consider a SingletonService bean:

@Service
public class SingletonService {

    public void doSomething(){
        // place for some logic
    }

}

SingletonService can be injected into multiple other beans, such as OtherServiceA and OtherServiceB:

@Service
public class OtherServiceA {

    private final SingletonService singletonService;

    public OtherServiceA(SingletonService singletonService) {
        this.singletonService = singletonService;
    }

}
@Service
public class OtherServiceB {

    private final SingletonService singletonService;

    public OtherServiceB(SingletonService singletonService) {
        this.singletonService = singletonService;
    }

}

In this case both OtherServiceA and OtherServiceB will share the same instance of SingletonService.

Notes about singleton beans:

  • Useful for stateless beans and services.
  • Created eagerly at container startup by default.
  • May be shared among beans, which can help save memory and simplify dependency management.

Prototype Scope

Prototype beans are created anew each time they are requested from the Spring container. Unlike singleton beans, every injection or retrieval results in a fresh instance. For example, consider a PrototypeService bean:

@Service
public class OtherServiceA {

    private final PrototypeService prototypeService;

    public OtherServiceA(PrototypeService prototypeService) {
        this.prototypeService = prototypeService;
    }
}
@Service
public class OtherServiceB {

    private final PrototypeService prototypeService;

    public OtherServiceB(PrototypeService prototypeService) {
        this.prototypeService = prototypeService;
    }

}

In this case, OtherServiceA and OtherServiceB will each receive a separate instance of PrototypeService.

Notes about prototype beans:

  • Useful for stateful or temporary objects.
  • Created on demand, not eagerly at container startup.
  • Each injection or request receives a new instance.

Request Scope

The request scope is specific to web applications. A new instance of the bean is created for each HTTP request. This ensures that each request gets its own independent instance, making it suitable for request-specific data.

For example, consider a RequestService bean:

@Service
@RequestScope
public class RequestService {

    public void processRequest() {
        // logic specific to a single HTTP request
    }

}

If RequestService is injected into multiple components handling the same request, they share the same instance for that request. However, a new HTTP request will receive a new instance.

@RestController
@RequestMapping("/api")
public class RequestController {

    private final RequestService requestService;

    public RequestController(RequestService requestService) {
        this.requestService = requestService;
    }

    @GetMapping("/process")
    public String handle() {
        requestService.processRequest();
        return "Request processed";
    }

}

Notes about request-scoped beans:

  • Created once per HTTP request.
  • Useful for beans that hold request-specific state.
  • Shared only within the same request.
  • Requires a web-aware Spring context.

Session Scope

The session scope is used in web applications where a bean needs to be shared across multiple HTTP requests within the same user session. A new instance of the bean is created once per HTTP session and is reused for all requests from that session. This is useful for user-specific state, such as shopping carts, user preferences, or temporary session data.

For example, consider a SessionService bean:

@Service
@SessionScope
public class SessionService {

    private int counter = 0;

    public int incrementCounter() {
        counter++;
        return counter;
    }

}

When SessionService is injected into different components handling requests from the same user session, they share the same instance. Another user with a different session will get a separate instance.

@RestController
@RequestMapping("/api")
public class SessionController {

    private final SessionService sessionService;

    public SessionController(SessionService sessionService) {
        this.sessionService = sessionService;
    }

    @GetMapping("/count")
    public String count() {
        int value = sessionService.incrementCounter();
        return "Counter for this session: " + value;
    }

}

Notes about session-scoped beans:

  • Created once per HTTP session.
  • Shared across multiple requests in the same session.
  • Suitable for user-specific data that persists during the session.
  • Requires a web-aware Spring context.

Application Scope

Application scope is useful for beans that need to be shared across a web application. A single instance is created per ServletContext and shared across all requests and sessions within that ServletContext. This makes it useful for global resources such as caches, configuration settings, or shared services.

For example, consider an ApplicationService bean:

@Service
@ApplicationScope
public class ApplicationService {

    private int globalCounter = 0;

    public int incrementCounter() {
        globalCounter++;
        return globalCounter;
    }

}

If ApplicationService is injected into multiple components, all requests and sessions in the same web application share the same instance:

@RestController
@RequestMapping("/api")
public class ApplicationController {

    private final ApplicationService applicationService;

    public ApplicationController(ApplicationService applicationService) {
        this.applicationService = applicationService;
    }

    @GetMapping("/global-count")
    public String globalCount() {
        int value = applicationService.incrementCounter();
        return "Global counter: " + value;
    }

}

Notes about application-scoped beans:

  • Created once per web application context.
  • Shared across all requests and sessions.
  • Suitable for global resources such as caches, configuration objects, or shared services.
  • Requires a web-aware Spring context.

WebSocket Scope

The WebSocket scope is used in Spring applications with WebSocket support. A new bean instance is created for each WebSocket session and is shared across all interactions within that session. This is useful for maintaining session-specific state in real-time applications, such as chat sessions, live notifications, or user-specific streaming data.

For example, consider a WebSocketService bean:

@Service
@WebSocketScope
public class WebSocketService {

    private final List<String> messages = new ArrayList<>();

    public void addMessage(String message) {
        messages.add(message);
    }

    public List<String> getMessages() {
        return messages;
    }

}

Within the same WebSocket session, all injections of WebSocketService share the same instance. Each new WebSocket session gets a separate instance, isolating state between sessions

@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {

    private final WebSocketService webSocketService;

    public ChatWebSocketHandler(WebSocketService webSocketService) {
        this.webSocketService = webSocketService;
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
        webSocketService.addMessage(message.getPayload());
    }

}

Notes about WebSocket-scoped beans:

  • Created once per WebSocket session.
  • Shared across all handlers within the same session.
  • Useful for session-specific state in real-time applications.
  • Requires a WebSocket-aware Spring context.

When to Use Each Scope

Personally, I like to choose the bean scope based on how it will be used: singleton for shared, stateless services, and request, session, or prototype scopes only when per-instance or per-user state is needed. Here are my thoughts:

Singleton
  • Useful for stateless services, shared utilities, or beans that do not maintain per-request state.
  • Example: A NotificationService that sends emails or push notifications, where a single shared instance is sufficient for the entire application.
Prototype
  • Useful when a new instance is needed each time the bean is requested.
  • Example: A ReportGenerator bean that holds temporary data while creating a report. Each request (for example, via ObjectProvider) receives a fresh instance.
Request
  • Useful for web applications where a bean exists only for a single HTTP request.
  • Example: A UserRequestLogger that collects request-specific information for auditing or metrics.
Session
  • Useful for maintaining user-specific state across multiple HTTP requests.
  • Example: A ShoppingCart bean that tracks items a user adds during their session.
Application
  • Useful for global resources shared across the entire web application.
  • Example: A CacheManager or ApplicationConfig bean that stores configuration or cached data accessible to all users.
WebSocket
  • Useful for WebSocket sessions where state persists during real-time communication for a single connection.
  • Example: A ChatSessionService that maintains message history for each user during a live chat session.

This approach helps me avoid memory issues, keep things thread-safe, and make the code easier to manage.

Spring Bean Lifecycle

Every Spring bean managed by the IoC container follows a Spring bean lifecycle that defines how it is created, initialized, and eventually destroyed, helping developers manage resources, add custom logic, and troubleshoot initialization issues effectively.

When the Spring application context starts, it scans, instantiates, and wires all defined beans. Each bean then passes through several stages before it's ready for use, and the reverse happens when the context shuts down.

The Spring bean lifecycle for singleton beans includes the following steps (some of them are optional):

  1. Instantiation

    The container creates a new instance of the bean, typically by calling its constructor or a factory method.

  2. Dependency Injection

    The container injects the bean's dependencies, setting properties or constructor arguments declared in the configuration.

  3. Aware Interfaces

    If the bean implements certain aware interfaces, the container supplies the corresponding context information. These include BeanNameAware, BeanFactoryAware, and ApplicationContextAware.

  4. BeanPostProcessor (Before Initialization)

    Registered BeanPostProcessors are applied before the bean's initialization callbacks. For example, postProcessBeforeInitialization().

  5. Initialization

    The Spring container calls the bean's configured initialization logic, e.g. @PostConstruct, afterPropertiesSet() from InitializingBean, or a custom init-method.

  6. BeanPostProcessor (After Initialization)

    Registered BeanPostProcessors are applied after the bean's initialization callbacks. For example, postProcessAfterInitialization().

  7. Ready for Use

    The bean is fully initialized and available for injection or retrieval from the container.

  8. Destruction

    When the application context is closed, Spring invokes destruction callbacks. This can include @PreDestroy, destroy() from DisposableBean, or a custom destroy-method.Prototype beans are not automatically destroyed by the container.

Note: The step names above are descriptive labels used to explain the observable lifecycle behavior of a bean managed by the Spring container. I did not find any official Spring documentation that defines a formal, named set of lifecycle phases.

Note: This and the next sections focus on singleton beans, the most common type of Spring bean. Bean lifecycle behavior can vary across scopes, particularly in the timing and handling of initialization and destruction callbacks.

For details on bean lifecycle, initialization, and destruction, including non-singleton scopes, see the Spring Framework Documentation.

Conceptual Flow

Before diving into the implementation details, it might be helpful to see the entire Spring bean lifecycle at a glance. The simplified flow below provides a high-level view of how a Spring bean transitions through its key lifecycle phases (for singleton beans):

Instantiation β†’ Dependency Injection β†’ Aware Interfaces β†’ BeanPostProcessor (Before Initialization) β†’
Initialization β†’ BeanPostProcessor (After Initialization) β†’ Ready for Use β†’ Destruction

During these phases, Spring allows developers to hook into the bean lifecycle using

  • annotations such as @PostConstruct, @PreDestroy,
  • interfaces such ase InitializingBean, DisposableBean,
  • configuration options such as custom init-method and destroy-method

to execute custom logic.

These mechanisms are explained in the next sections:

Bean Creation and Initialization

After a singleton bean is instantiated and its dependencies are injected, the Spring container calls any configured initialization logic, allowing the bean to be fully ready for use within the application.

Spring provides several mechanisms to handle bean initialization, giving developers flexibility in applying custom logic:

For my thoughts on choosing an initialization approach, please see Choosing the Right Way to Initialize a Bean.

Initialization with Annotations

Using the @PostConstruct annotation is a common choice for bean initialization in many modern Spring applications. This annotation marks a method to run after the bean is created and dependencies are injected.

To use it simply annotate method as follows:

@Slf4j
@Component
public class SomePostConstructAnnotatedBean {

    @PostConstruct
    public void init() {
        log.info("SomePostConstructAnnotatedBean has been initialized");
    }

}

I often choose this approach because it keeps the bean decoupled from Spring interfaces, making the code cleaner and easier to maintain.

Initialization with Interfaces

Spring also offers the InitializingBean interface, which allows a bean to execute custom initialization logic after its properties are set by the container.

To use it, implement InitializingBean interface and the afterPropertiesSet() method:

@Slf4j
@Component
public class SomeInitializingBean implements InitializingBean {

    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("SomeInitializingBean has been initialized");
    }

}

This approach is useful for initialization logic that directly integrates with the Spring framework.

Initialization with Init Method

In Java configuration classes, it is possible to specify an initMethod that Spring will call after dependency injection is complete.

One way to do this is to use the @Bean annotation and define an initMethod:

@Configuration
public class MyCustomInitConfiguration {

    @Bean(initMethod = "customInit")
    public MyCustomInitBean myCustomInitBean() {
        return new MyCustomInitBean();
    }

}

And implement the method. Here is a sample MyCustomInitBean class:

@Slf4j
public class MyCustomInitBean {

    public void customInit() {
        log.info("Custom initialization method invoked");
    }

}

I find this especially useful when integrating third-party classes that cannot be annotated with @PostConstruct. You can either create your own initialization method or tell Spring to call an existing method after the bean is created. This approach works well for classes you cannot or prefer not to modify.

Please note that the same can also be done using XML configuration, for example:

<bean id="myCustomInitBean" class="pl.kamilmazurek.example.beans.mybean.MyCustomInitBean" init-method="customInit"/>

XML configuration can be especially useful for legacy systems.

Choosing the Right Way to Initialize a Bean

Custom initialization logic may be needed in different situations:

  • Annotating with @PostConstruct: I like this approach because it keeps the bean decoupled from Spring and the code cleaner to maintain.
  • Implementing InitializingBean: May be a good choice when initialization needs tight Spring integration and implementing an interface is fine.
  • Specifying initMethod: This can be useful when working with third-party classes or beans you don't want to modify.

Spring calls initialization callbacks in a defined order:

  1. Methods annotated with @PostConstruct, if any
  2. The afterPropertiesSet() of InitializingBean, if implemented
  3. The configured initMethod, if provided

Choosing the right approach depends on your project style and whether you prefer annotations, interfaces, or configuration-based control. For me, @PostConstruct provides a simple and maintainable way to handle bean initialization in most modern Spring Boot applications.

Bean Destruction and Cleanup

Just as Spring beans can perform setup tasks after being created, they can also perform cleanup when they are about to be destroyed. This cleanup allows beans to release resources, close connections, or perform any necessary shutdown operations before being removed from the container.

Spring provides various ways to define bean destruction logic, depending on how beans are configured:

For more details, see:

Destruction with Annotations

The common and modern approach is to use the @PreDestroy annotation. This annotation marks a method that the Spring container will call before the bean is destroyed, e.g. when the application context is being closed for singleton beans.

To use it simply annotate method as follows:

@Slf4j
@Component
public class SomePreDestroyAnnotatedBean {

    @PreDestroy
    public void cleanup() {
    log.info("SomePreDestroyAnnotatedBean is being destroyed");
    }

}

This method runs automatically when the application context is closed, for example, when a Spring Boot application shuts down. It's a clean and non-invasive way to handle bean cleanup without tying your code to Spring-specific interfaces.

Destruction with Interfaces

Spring also offers the DisposableBean interface for beans that want to execute destruction logic in a more framework-integrated way.

During the destruction phase of singleton beans, typically when the application context shuts down, the Spring container calls the destroy() method of any bean that implements DisposableBean.

Such a destruction hook can be defined by implementing the DisposableBean interface and defining the destroy() method:

@Slf4j
@Component
public class SomeDisposableBean implements DisposableBean {

    @Override
    public void destroy() throws Exception {
        log.info("SomeDisposableBean is being destroyed");
    }

}

I find this useful when I need a direct hook into Spring's lifecycle, such as when managing resources tightly coupled to the framework.

Destruction with Destroy Method

In Java configuration classes, it is possible to specify a destroyMethod. This tells Spring which method to invoke before destroying the bean.

This can be done by defining a destroyMethod, for example by using the @Bean annotation:

@Configuration
public class MyCustomDestroyConfiguration {

    @Bean(destroyMethod = "customDestroy")
    public MyCustomDestroyBean myCustomDestroyBean() {
        return new MyCustomDestroyBean();
    }

}

The method is implemented in the bean class. Here is a sample MyCustomDestroyBean:

@Slf4j
public class MyCustomDestroyBean {

    public void customDestroy() {
        log.info("Custom destroy method invoked");
    }

}

This approach works particularly well for third-party classes or libraries that need a custom shutdown method but cannot be annotated directly.

As with initialization, XML configuration supports the same concept:

<bean id="myCustomInitBean" class="pl.kamilmazurek.example.beans.mybean.MyCustomInitBean" init-method="customInit"/>

This can be handy for systems where XML configuration is still in use, such as legacy applications.

When Bean Destruction Happens

Spring triggers destruction callbacks automatically in several situations, like:

  • When the application context shuts down: e.g. during Spring Boot shutdown via context.close() or JVM termination.
  • When the lifecycle of a scoped bean ends: e.g. a request-scoped bean at the end of an HTTP request.
  • When a bean is explicitly removed from the container: e.g. manually via ConfigurableApplicationContext, though this is uncommon.

When a singleton bean is destroyed, Spring invokes destruction callbacks in a specific order:

  • Methods annotated with @PreDestroy
  • DisposableBean destroy(), if implemented
  • The configured destroyMethod, if provided

This order ensures annotation-based cleanup runs first, Spring-specific lifecycle hooks run second, and configuration-based destruction runs last.

Keep in mind that destruction callbacks do not apply to prototype beans.Prototype beans are not tracked after creation, so Spring does not call any destruction callbacks for them. Their cleanup must be handled manually.

Note: Spring can automatically infer a destroy method by looking for a public close() or shutdown() method.@Bean methods in Java configuration use this behavior by default and work with beans implementing java.lang.AutoCloseable or java.io.Closeable, allowing cleanup without tying your code to Spring.

For more details please see: Customizing the Nature of a Bean.

Choosing the Right Way to Clean Up a Bean

When deciding how to implement cleanup logic, consider your project's style and dependencies. Some of my thoughts:

  • Using @PreDestroy: I prefer this in modern Spring Boot apps. Simple, annotation-based, and keeps code decoupled from Spring interfaces.
  • Implementing DisposableBean: This can be a good choice for lifecycle control integrated with Spring or for explicit destruction hooks.
  • Using destroyMethod: This can be useful for third-party beans or for configuration-based control without modifying the class source.

No matter which approach you choose, proper cleanup is essential to ensure that resources such as file handles, database connections, or threads are released safely. This helps prevent memory leaks and improves overall application stability.

Practical Example: Simple Layered Architecture

Let's look at a concrete example. We'll walk through how Spring Beans work together across the controller, service, and repository layers to handle a simple "get item by ID" request, while applying the concepts of bean initialization, dependency injection, and lifecycle management.

The following example, from the pl.kamilmazurek.example.beans.item package, shows how beans of different types work together to handle a typical use case.

Controller Layer: Handling HTTP Requests

In this example, the @RestController bean serves as the entry point for handling HTTP requests.

@RestController is a specialized type of @Controller for REST APIs. It marks the class as a Spring-managed bean, which the IoC container automatically detects and instantiates.

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/items")
public class ItemRestController {

    private final ItemService itemService;

    @GetMapping("/{id}")
    public ResponseEntity<ItemDTO> getItem(@PathVariable Long id) {
        return itemService.getItem(id).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
    }

}

Relevant bean concepts:

  • ItemRestController is a singleton bean, created automatically at application startup.
  • ItemService is injected via constructor injection.
  • The controller bean is initialized once and reused for all incoming HTTP requests.

Service Layer: Business Logic

The service layer contains the application's business logic. Here's the corresponding ItemService bean:

@Slf4j
@Service
@RequiredArgsConstructor
public class ItemService {

    private final ItemRepository itemRepository;

    @PostConstruct
    public void init() {
        // some initialization logic can be added here
        log.info("ItemService has been initialized");
    }

    public Optional<ItemDTO> getItem(Long id) {
        return itemRepository.findById(id).map(ItemDTO::fromEntity);
    }

    @PreDestroy
    public void cleanup() {
        log.info("ItemService is shutting down");
        // some cleanup logic can be added here
    }

}

Relevant bean concepts:

  • @Service registers the class as a Spring-managed bean.
  • Spring instantiates the bean and injects ItemRepository when the application context starts.
  • The @PostConstruct method runs after dependency injection for one-time initialization.
  • The bean stays active until the application context shuts down, at which point @PreDestroy executes.

Repository Layer: Data Access

The repository layer handles persistence logic. Spring Data JPA manages repository beans, creating proxy implementations at runtime.

@Repository
public interface ItemRepository extends JpaRepository<ItemEntity, Long> {
}

Relevant bean concepts:

  • ItemRepository is a singleton bean.
  • Spring Boot automatically detects it, generates an implementation at runtime, and injects it into the ItemService bean.

Supporting classes: Entity and DTO

To complete the flow, we define the entity and its corresponding DTO.

@Data
@Entity
@Table(name = "items")
public class ItemEntity {

    @Id
    private Long id;

    private String name;

}
public record ItemDTO(Long id, String name) {

    public static ItemDTO fromEntity(ItemEntity entity) {
        return new ItemDTO(entity.getId(), entity.getName());
    }

}

Relevant concepts:

  • ItemEntity represents the database table and is managed by JPA.
  • ItemDTO is a simple data transfer object used to send data from the service layer to the controller layer.
  • The fromEntity method converts the entity to its DTO representation, keeping layers decoupled.

End-to-End Lifecycle Flow

The example in pl.kamilmazurek.example.beans.item demonstrates how Spring Beans, dependency injection, scopes, and lifecycle management work together across a layered architecture:

  • The controller handles requests using injected service beans.
  • The service applies business logic and may run one-time initialization via @PostConstruct.
  • The repository abstracts persistence and is created automatically by Spring Data JPA.
  • The Spring container manages the entire lifecycle, allowing developers to focus on business logic instead of manual wiring.

When the application starts, the Spring container instantiates beans, injects dependencies, and executes initialization methods. Beans handle incoming requests as needed, and the container calls destruction methods when the application shuts down.

Application startup:

  • Spring scans for annotated classes such as @RestController, @Service and @Repository.
  • Singleton beans are created, e.g. ItemRestController, ItemService, ItemRepository.
  • Dependencies are injected automatically.
  • Spring invokes the @PostConstruct method on ItemService after dependency injection.

Handling a client request:

  • The controller bean handles the HTTP request (GET /api/items/{id}).
  • It delegates to the service bean, which applies business logic.
  • The repository bean performs database access.
  • Entities and DTOs flow through the layers, keeping them decoupled.

Application shutdown:

  • The Spring container gracefully destroys beans.
  • Cleanup logic defined in the @PreDestroy hook is executed.

Try It: Sample Request and Response

Once the application is running, you can test the API endpoint using curl or another HTTP client. For example:

curl -X GET http://localhost:8080/api/items/1

Expected JSON response:

{
    "id": 1,
    "name":"Item A"
}

Request processing steps:

  • ItemRestController receives the request.
  • The controller delegates the request to ItemService.
  • ItemService calls ItemRepository to fetch the ItemEntity from the database.
  • The entity is converted to ItemDTO and returned to the controller.
  • Spring serializes the ItemDTO into JSON for the HTTP response.

Repositories

The source code for this project is available on GitHub and mirrored on GitLab:

Author


This article and the accompanying example code was written by Kamil Mazurek, a Software Engineer based in Warsaw, Poland. You can also find me on my LinkedIn profile.

My public repositories can be found on my GitHub and GitLab profiles:

Thanks for visiting πŸ™‚


Disclaimer

THIS ARTICLE AND ANY SOFTWARE INCLUDED WITH THIS ARTICLE AND CREATED BY THE ARTICLE'S AUTHOR ARE PROVIDED FOR EDUCATIONAL PURPOSES ONLY.

THE ARTICLE AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE ARTICLE, THE SOFTWARE, OR THE USE OR OTHER DEALINGS IN THE ARTICLE OR SOFTWARE.

THIRD-PARTY LIBRARIES REFERENCED OR INCLUDED IN THIS SOFTWARE ARE SUBJECT TO THEIR OWN LICENSES. THIRD-PARTY DOCUMENTATION OR EXTERNAL RESOURCES REFERENCED IN THIS ARTICLE ARE SUBJECT TO THEIR OWN LICENSES AND TERMS.