Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 37

Singleton vs. Prototype Bean Scopes

Recommended resources

Hello! Welcome back to your course on the Spring Framework.

In our last lesson, we saw how to hook into the bean lifecycle using @PostConstruct and @PreDestroy. A critical takeaway was that while these hooks work reliably for default (singleton) beans, the @PreDestroy method is not called for prototype-scoped beans. This distinction is not a minor detail; it's fundamental to how you design and manage components in Spring.

Today, we will address this head-on. This lesson is dedicated to the learning outcome: Configure and compare the singleton and prototype bean scopes. We will explore what these scopes are, how they dictate the instantiation and lifecycle of your beans, and why choosing the correct scope is a crucial architectural decision. Understanding this is key to managing state, performance, and resources effectively in your applications.

The Two Fundamental Scopes: Singleton and Prototype

At its core, a bean's scope determines how many instances of that bean the Spring container will create and how they will be shared. The two most fundamental scopes are singleton and prototype.

  • Singleton (the default): The Spring IoC container creates exactly one instance of the bean. Every time your application asks for this bean, the container returns this same, single, shared instance.
  • Prototype: The container creates a new instance of the bean every single time it is requested.

This image provides a clear visual distinction between the two behaviors.

Spring Bean Scopes: Singleton vs. Prototype
This diagram illustrates the core difference: a singleton bean results in one shared instance for all requests, while a prototype bean generates a new, unique instance for each request.

Seeing Scopes in Action

Theory is one thing, but seeing the difference in practice makes it concrete. The following video provides an excellent hands-on demonstration. It creates two beans, one for each scope, and then requests them multiple times from the Spring container. By printing the object's hash code, it proves whether the same instance or a new instance is being returned.

Spring & Spring Boot Annotations Series - #8 - @Scope Annotation | Singleton and Prototype Scopes

The video '@Scope Annotation | Singleton and Prototype Scopes' from Java Guides provides a concise and clear code-along demonstration of how singleton and prototype scopes behave.

Please watch from 00:40 to 10:41. The video covers: The definition of singleton and prototype scopes. How to use the @Scope annotation (or not, for the default singleton). A practical test where beans of each scope are retrieved multiple times. An analysis of the console output, using hash codes to verify the number of instances created.

As you saw in the video, the results are unambiguous:

  • Singleton: The hash code was the same every time, proving only one object was ever created and shared.
  • Prototype: A different hash code was printed for each request, proving a new object was created each time.

The "Official" Word and Key Characteristics

Now that you've seen the behavior, let's turn to the official Spring documentation to solidify our understanding and create a clear comparison.

Bean Scopes :: Spring Framework

The official Spring Framework documentation provides the authoritative definitions for bean scopes. Reading these sections will give you the precise terminology and context.

Please read the introduction and the first two sections: 'Bean Scopes': The introductory text and the table of scopes. 'The Singleton Scope': Pay attention to how it's defined as 'per-container and per-bean'. 'The Prototype Scope': Note the critical paragraph about the lifecycle: 'Spring does not manage the complete lifecycle of a prototype bean.'

Based on the video and the documentation, we can summarize the key differences in a table. From a system design perspective, these differences have significant implications.

Feature Singleton Scope Prototype Scope
Instance Count One instance per Spring container. A new instance every time it's requested.
Default Scope? Yes. If you don't specify a scope, it's a singleton. No. Must be explicitly set with @Scope("prototype").
State Management Ideal for stateless objects (e.g., services, repositories, utility classes). Shared state is dangerous and can lead to bugs. Ideal for stateful objects where each client needs its own copy (e.g., a builder, a shopping cart object).
Initialization Eager by default. Beans are created when the application starts. Lazy. Beans are only created when they are requested.
Lifecycle Fully managed by Spring, including destruction callbacks (@PreDestroy). Creation is managed, but destruction is not. Spring will not call @PreDestroy methods. The client is responsible for cleanup.

Hands-On: Verifying Scopes Yourself

Let's reinforce this by writing the code yourself. This small application will replicate the experiment from the video.

  1. Create a Spring Boot Project: Use the Spring Initializr to generate a new project with no dependencies other than Spring Web (which ensures the context stays alive).

  2. Create a MessageService Bean: This will be our prototype bean. Note the use of @Scope("prototype").

    package com.example.scopes;
    
    import org.springframework.beans.factory.config.ConfigurableBeanFactory;
    import org.springframework.context.annotation.Scope;
    import org.springframework.stereotype.Component;
    
    @Component
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public class MessageService {
        private static int instanceCount = 0;
    
        public MessageService() {
            instanceCount++;
            System.out.println("MessageService (prototype) instance created. Total: " + instanceCount);
        }
    
        public String getMessage() {
            return "Hello from prototype bean instance: " + this.hashCode();
        }
    }
    

    Note: We use ConfigurableBeanFactory.SCOPE_PROTOTYPE instead of the string "prototype" to avoid magic strings, which is a good practice.

  3. Create a TimeService Bean: This will be our singleton bean. We will explicitly define it, though it's the default.

    package com.example.scopes;
    
    import org.springframework.context.annotation.Scope;
    import org.springframework.stereotype.Component;
    import java.time.LocalTime;
    
    @Component
    @Scope("singleton") // This is the default, but we're being explicit
    public class TimeService {
        public TimeService() {
            System.out.println("TimeService (singleton) instance created.");
        }
    
        public String getTime() {
            return "The time is " + LocalTime.now() + " from singleton bean: " + this.hashCode();
        }
    }
    
  4. Modify the Main Application Class: We'll use a CommandLineRunner to execute code after the application context is loaded. This is a common pattern for running logic in a Spring Boot application on startup.

    package com.example.scopes;
    
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.Bean;
    
    @SpringBootApplication
    public class ScopesApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ScopesApplication.class, args);
        }
    
        @Bean
        public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
            return args -> {
                System.out.println("--- Testing Singleton Scope ---");
                TimeService ts1 = ctx.getBean(TimeService.class);
                TimeService ts2 = ctx.getBean(TimeService.class);
                System.out.println(ts1.getTime());
                System.out.println(ts2.getTime());
                System.out.println("Are singleton instances the same? " + (ts1 == ts2));
    
                System.out.println("\n--- Testing Prototype Scope ---");
                MessageService ms1 = ctx.getBean(MessageService.class);
                MessageService ms2 = ctx.getBean(MessageService.class);
                System.out.println(ms1.getMessage());
                System.out.println(ms2.getMessage());
                System.out.println("Are prototype instances the same? " + (ms1 == ms2));
            };
        }
    }
    

When you run this application, your console output should look similar to this:

TimeService (singleton) instance created.
... (Spring Boot startup logs) ...
--- Testing Singleton Scope ---
The time is 14:30:00.12345 from singleton bean: 123456789
The time is 14:30:00.12346 from singleton bean: 123456789
Are singleton instances the same? true

--- Testing Prototype Scope ---
MessageService (prototype) instance created. Total: 1
MessageService (prototype) instance created. Total: 2
Hello from prototype bean instance: 987654321
Hello from prototype bean instance: 543219876
Are prototype instances the same? false

This output provides concrete proof: one singleton instance is created eagerly at startup, while two separate prototype instances are created lazily when getBean() is called.


A Critical Design Pitfall: Injecting a Prototype into a Singleton

There's a common "gotcha" that catches many developers. What happens if you inject a prototype-scoped bean into a singleton-scoped bean?

Given your experience with dependency injection in other languages, you might expect the singleton to get a fresh instance of the prototype every time it needs one. This is not what happens in Spring.

The dependencies of a singleton bean are resolved and injected only once, when the singleton itself is instantiated. This means the singleton receives a prototype instance and then holds on to that same instance for its entire lifetime, completely defeating the purpose of the prototype scope.

Bean Scopes :: Spring Framework

The official documentation has a specific section for this exact problem. It's a crucial concept to master.

Please read the section titled 'Singleton Beans with Prototype-bean Dependencies'. It explains this behavior and hints at solutions.

This is a frequent source of bugs, especially when the prototype bean is stateful. The singleton effectively "freezes" the state of the first prototype instance it receives. While there are advanced patterns to solve this (like "method injection" or using an ObjectFactory), the most important thing for now is to be aware of this behavior and its implications.


Conclusion

In this lesson, we dissected the two fundamental bean scopes in Spring and saw how your choice impacts instance management, state, and lifecycle.

Key Takeaways:

  • Singleton is the default scope, providing one shared, eagerly-created instance per container. It's best for stateless logic.
  • Prototype provides a new, lazily-created instance for every request. It's designed for stateful objects.
  • You configure the scope with @Scope("prototype").
  • Spring does not manage the destruction of prototype beans, meaning @PreDestroy hooks won't run.
  • Injecting a prototype bean into a singleton results in the singleton holding a single, cached instance of the prototype.

So far, we've focused on scopes relevant to any Spring application. However, when building web applications, Spring provides additional, powerful scopes that are tied to the lifecycle of web requests.

In our next lesson, "Configure and use web-specific bean scopes (request, session) in a web application," we will explore how you can create beans that live only for the duration of a single HTTP request or a user's session.