Spring Boot Deep Dive: Beans, Lifecycle & Dependency Injection

A comprehensive guide to understanding how Spring manages objects, when they come alive, and how they talk to each other.
1. What is a Bean?
In simple terms, a Bean is just a Java object managed by the Spring container - also known as the IOC (Inversion of Control) container.
Think of the IOC container as a smart factory. You tell it what objects (beans) to create, and it takes full responsibility for:
Creating their dependencies
Injecting their dependencies
Managing their entire lifecycle
Destroying them when no longer needed
IOC Container -> Creates, Manages, and Destroys Beans
You don't call new MyClass() manually everywhere. Spring does it for you.
2. How to Create a Bean
There are two primary ways to define a bean in Spring Boot:
Way 1: Using @Component Annotation
@Component follows the "convention over configuration" approach. Spring Boot will auto-configure your beans based on conventions, reducing the need for explicit setup.
Annotations like @Controller, @Service, and @Repository are all specializations of @Component - they internally tell Spring to create and manage a bean.
@Component
public class User {
String username;
String email;
public String getUsername(){
return username;
}
public void setUsername(String username){
this.username = username;
}
public String getEmail(){
return email;
}
public void setEmail(String email){
this.email = email;
}
}
Here, Spring Boot will internally call new User() to create an instance of this class.
The problem with Parameterized Constructors
What if your class has a constructor that requires arguments?
@Component
public class User{
String username;
String email;
public User(String username, String email){
this.username = username;
this.email = email;
}
}
This will cause:
****************************
APPLICATION FAILED TO START
****************************
Why? Because Spring does not know what values to pass to the constructor parameters.
Way 2: Using @Bean Annotation
@Bean lets you provide the configuration details explicitly, telling Spring exactly how to create a bean. This is placed inside a @Configuration class.
//User.java -- no Spring annotation needed here
public class User {
String username;
String email;
public User(String username, String email){
this.username = username;
this.email = email;
}
}
// AppConfig.java
@Configuration
public class AppConfig {
@Bean
public User createUserBean(){
return new User("defaultUsername", "defaultEmail");
}
}
You can even define multiple beans of the same type - Spring will create a separate bean for each:
@Configuration
public class AppConfig {
@Bean
public User createUserBean(){
return new User("defaultUsername", "defaultEmail");
}
@Bean
public User createAnotherUserBean(){
return new User("anotherUsername", "anotherEmail");
}
}
3. How Spring Finds Your Beans
Once you define beans, Spring needs to discover them. There are two approaches:
1. @ComponentScan
Spring uses @ComponentScan to scan specified packages (and sub-packages) for classes annotated with @Component, @Service, @Controller, etc.
@SpringBootApplication
@ComponentScan(basePackages = "com.springbootbynandini.springblogs")
public class SpringbootApplication{
public static void main(String[] args){
SpringApplication.run(SpringbootApplication.class, args);
}
}
Note: @SpringBootApplication itself includes @ComponentScan by default, scanning the package it resides in and all sub-packages.
2. Explicit @Bean in @Configuration
As shown above - when you manually declare beans using @Bean inside a @Configuration class, Spring discovers them through that configuration.
4. When are Beans created Eager vs Lazy
Beans aren't all created at the same time. Spring supports two initialization strategies:
- Eager -- Created at application startup
Example: Singleton-scoped beans (default)
- Lazy -- Created when first needed/requested
Example: Prototype-scoped beans, @Lazy beans
Eager Initialization (Default)
Singleton beans are initialized when the application starts. This ensures all dependencies are wired up before any request is processed.
Lazy Initialization
Use @Lazy to delay bean creation until it's actually needed:
@Lazy
@Component
public class Order{
public Order(){
System.out.println("Initializing Order");
}
}
The "Initializing Order" message won't print at startup - only when Order is first accessed.
5. The Bean Lifecycle - Step by Step
Understanding the bean lifecycle is crucial for writing clean Spring applications. Here's the complete flow:
Application Start
|
IOC Container Started <-- Configuration Loaded
|
Construct Bean
|
Inject Dependency Into Constructed Bean
|
@PostConstruct (run custom init logic)
|
Use the Bean
|
@PreDestroy (run cleanup logic)
|
Bean Destroyed
Let's walk through each step:
Step 1: IOC Container Starts
During application startup, Spring Boot invokes the IOC container (ApplicationContext). It uses @Configuration and @ComponentScan to discover all beans that need to be created.
Step 2: Construct the Bean
Spring calls the constructor to instantiate the bean.
@Component
public class User{
public User(){
System.out.println("Initializing User");
}
}
You'll see "Initializing User" printed in the logs at startup.
Step 3: Inject the Dependencies
After construction, Spring injects all required dependencies (via @Autowired). We'll cover this in depth in the Dependency Injection section.
There are three injection styles:
Constructor Injection
Setter Injection
Field Injection
Step 4: @PostConstruct -- Post-Initialization Hook
After dependencies are injected, Spring calls the method annotated with @PostConstruct. Use this to run any initialization logic that requires dependencies to already be available.
@Component
public class User{
@Autowired
Order order;
@PostConstruct
public void initialize(){
System.out.println("Bean constructed and dependencies injected!");
}
public User(){
System.out.println("Initializing User");
}
}
Output Order:
Initializing User
Lazy: Initializing Order
Bean constructed and dependencies injected!
Step 5: Use the Bean
The bean is fully initialized and ready to be used across your application to execute business logic.
Step 6: @PreDestroy - Pre-Destruction Hook
Before the bean is destroyed (i.e., when the application context is closed), Spring calls the method annotated with @PreDestroy. Use this for cleanup - like closing connections, releasing resources, etc.
@Component
public class User{
@PostConstruct
public void initialize(){
System.out.println("Post Construct initiated");
}
@PreDestroy
public void preDestroy(){
System.out.println("Bean is about to be destroyed, in PreDestroyMethod");
}
public User(){
System.out.println("Initializing User");
}
}
To trigger @PreDestroy, you need to explicitly close the context:
@SpringBootApplication
public class SpringbootApplication{
public static void main(String[] args){
ConfigurableApplicationContext context = SpringApplication.run(SpringbootApplication.class, args);
context.close();
}
}
6. What is Dependency Injection?
Dependency Injection (DI) is a design pattern that makes your class independent of its concrete dependencies - instead, dependencies are injected from outside.
The Problem Without DI
public class User{
Order order = new Order(); // Tightly coupled!
public User(){
System.out.println("Initializing User");
}
}
This creates two serious problems:
Problem 1: Tight Coupling
If Order changes to an interface with multiple implementations(OnlineOrder, OfflineOrder), the User class must also change. Classes become tightly coupled.
Problem 2: Violates Dependency Inversion Principle (SOLID)
The D in SOLID says: depend on abstractions, not concrete implementations.
// Breaks DIP
public class User {
Order order = new OnlineOrder(); // depends on concrete class
// Follows DIP
public class User {
Order order;
public User(Order orderObj){
this.order = orderObj; // injected from outside
}
}
The Solution: Dependency Injection via Spring
@Component
public class User{
@Autowired
Order order;
}
@Component
public class Order{}
@Autowired tells Spring: "Find the appropriate bean for this type and inject it here".
7. Types of Dependency Injection
Spring supports three types of dependency injection:
1. Field Injection
Dependency is set directly on the field using reflection.
@Component
public class User{
@Autowired
Order order;
public User(){
System.out.println("User Initialized");
}
}
Advantage:
- Very simple and easy to use
Disadvantage:
Cannot use with final (immutable) fields
Prone to NullPointerException if you instantiate the class manually (e.g., in unit tests)
Setting mock dependencies during unit testing requires reflection - cumbersome
//NPE risk: if you do this outside Spring context
User userObj = new User();
userObj.process(); // order is null here --- NullPointerException!
2. Setter Injection
Dependency is set via a setter method annotated with @Autowired.
@Component
public class User{
public Order order;
public User(){
System.out.println("User initialized");
}
@Autowired
public void setOrderDependency(Order order){
this.order = order;
}
}
Advantages:
Dependency can be changed after object creation
Easy to pass mock objects in tests
Disadvantages:
Field cannot be marked final (can't make it immutable)
Object initialization and dependency injection are separated - hurts readability
3. Constructor Injection (Recommended)
Dependency is resolved at the time of object creation - passed through the constructor.
@Component
public class User{
Order order;
@Autowired
public User(Order order){
this.order = order;
System.out.println("User initialized");
}
}
Spring 4.3+ tip: When only one constructor exists, @Autowired is optional.
@Component
public class User{
Order order;
public User(Order order){ // @Autowired not needed(single constructor)
this.order = order;
System.out.println("User initialized");
}
}
When multiple constructors exist, you must annotate the one Spring should use:
@Component
public class User{
Order order;
Invoice invoice;
public User(Order order){
this.order = order;
System.out.println("User initialized with only Order");
}
@Autowired // Spring uses this one
public User(Invoice invoice){
this.invoice = invoice;
System.out.println("User initialized with only Invoice");
}
}
8. Why Constructor Injection is Recommended
Constructor injection is the Spring team's recommended approach, and for very good reasons:
- Reason 1: Guaranteed Full Initialization
All mandatory dependencies are created at the time of object initialization - your object is 100% ready to use. No runtime NPEs from missing dependencies.
- Reason 2: Immutability - Use final Fields
Constructor injection is the only way to use final (immutable) fields:
// Works with constructor injection
@Component
public class User{
public final Order order;
@Autowired
public User(Order order){
this.order = order;
System.out.println("User initialized");
}
}
// Does NOT work with field injection
@Component
public class User{
@Autowired
public final Order order; // compilation error!
}
- Reason 3: Fail Fast
If a required dependency is missing, the application fails at startup (compile time detection), not silently at runtime when it's too late:
@Component
public class User{
Order order;
public User(Order order){
this.order = order;
}
@PostConstruct
public void init(){
System.out.println(order == null); // Always false with constructor injection
}
}
If Order bean is missing -> app fails to start with a clear error, rather than an NPE somewhere deep in runtime execution.
- Reason 4: Unit Testing is Easy
No reflection tricks needed. Just pass a mock object directly through the constructor:
class UserTest{
private Order orderMockObj;
private User user;
@BeforeEach
public void setup(){
this.orderMockObj = Mockito.mock(Order.class);
this.user = new User(orderMockObj); // clean, simple!
}
}
Compare to field injection where you need @InjectMocks and Mockito's reflection-based injection - much more complex.
9. Common Issues & How to Fix Them
- Issue 1: Circular Dependency
This happens when two beans depend on each other, forming a cycle:
@Component
public class Order{
@Autowired
Invoice invoice; // Order needs Invoice
}
@Component
public class Invoice{
@Autowired
Order order; // Invoice needs order --- Cycle!
}
This results in:
APPLICATION FAILED TO START
The dependencies of some of the beans in the application context form a cycle:
----------
| |
| Invoice
| |
| Order
|---------|
Solutions:
Solution 1: Refactor - Remove the Cycle
Extract shared logic into a third class. Both Order and Invoice depend on it, but not on each other.
Solution 2: Use @Lazy on @Autowired
Spring creates a proxy bean instead of the real bean immediately, breaking the cycle:
@Component
public class Order{
@Lazy
@Autowired
Invoice invoice;
public Order(){
System.out.println("Order initialized");
}
}
Solution 3: Use @PostConstruct
Set the dependency after construction using a post-construct method:
@Component
public class Order{
@Autowired
Invoice invoice;
@PostConstruct
public void initialize(){
invoice.setOrder(this); // Set back-reference after construction
}
}
Issue 2: Unsatisfied Dependency
Occurs when Spring can't figure out which implementation to inject for an interface:
public interface Order{}
@Component
public class OnlineOrder implements Order{}
@Component
public class OfflineOrder implements Order{}
@Component
public class User{
@Autowired
Order order; // which one? Spring is confused -> ApplicationFailed!
}
Error: UnsatisfiedDependencyException: Error creating bean with name 'User'
Solutions:
Solution 1: @Primary Annotation
Mark one implementation as the default:
@Primary
@Component
public class OnlineOrder implements Order{}
Solution 2: @Qualifier Annotation
Be explicit about which implementation you want:
@Component
public class User{
@Qualifier("offlineOrderName")
@Autowired
Order order;
}
@Component
@Qualifier("onlineOrderName")
public class OfflineOrder implements Order{}
10. Summary
Here's a quick recap of everything covered:
Bean - A Java object managed by the Spring IOC container
@Component - Auto-creates bean using convention; needs no-arg or injectable constructor
@Bean - Explicit bean definition with full control over construction
@ComponentScan - Tells Spring where to look for Component classes
Eager - Singleton beans created at startup (default behavior)
Lazy - Beans created when first requested; use Lazy
Bean Lifecycle - Start -> Construct -> Inject -> @PostConstruct -> Use -> @PreDestroy -> Destroy
DI - Pattern to decouple classes by injecting dependencies externally
Field Injection - Simple but not testable; no immutability; not recommended
Setter Injection - Flexible but no immutability; dependencies can be missed
Constructor Injection - Recommended - immutable, fail-fast, testable
Circular Dependency - Refactor, or use @Lazy / @PostConstruct to break cycles
Unsatisfied Dependency - Use @Primary or @Qualifier to resolve ambiguity
Conclusion
Spring's IOC container and dependency injection are the beating heart of any Spring Boot application. Once you deeply understand:
How beans are created and discovered
When they come alive (eager vs lazy)
The full lifecycle from construction to destruction
The pros and cons of each injection style
And how to handle common pitfalls
You'll write Spring Boot code that is clean, testable, and production-ready.
Happy coding! If you found this helpful, drop a reaction and share it with fellow Spring learners.




