DEV Community

Cover image for Deep Dive into the Spring IoC Container: Unmasking BeanFactory and ApplicationContext
mohanathas holins
mohanathas holins

Posted on

Deep Dive into the Spring IoC Container: Unmasking BeanFactory and ApplicationContext

Why Every Spring Boot Developer Must Understand the IoC Container

In the modern era of software development, abstracting away complexity has never been easier.

With AI assistants ready to generate code blocks at a moment's notice, anyone can quickly build a working CRUD application using Spring Boot. You annotate a class with @RestController, add a constructor, connect a service, and the API works.

That first successful response from Postman gives an instant dopamine hit.

But there is a hidden trap in this "code-first, fundamentals-later" approach.

When your application grows, or when an unexpected production bug appears, blindly patching code with trial and error is not enough. To truly master Spring Boot, you need to step away from just writing code and understand the engine behind it:

The Spring IoC Container.

In this article, we will break down the core architecture that powers every Spring Boot application.


1. The Problem with Manual Object Management

Before understanding Spring, let’s start with plain Java.

In standard Object-Oriented Programming, you are responsible for creating and managing objects manually.

public class Student {

    private int studentId;
    private String name;
    private int age;

    public Student(int studentId, String name, int age) {
        this.studentId = studentId;
        this.name = name;
        this.age = age;
    }

    public int getStudentId() {
        return this.studentId;
    }

    public void setStudentId(int studentId) {
        this.studentId = studentId;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let’s create a Student object manually:

public class Main {

    public static void main(String[] args) {

        Student student = new Student(123, "John Doe", 21);
    }
}
Enter fullscreen mode Exit fullscreen mode

This is normal Java object creation.

But here is the issue:

Student student = new Student(123, "John Doe", 21);
Enter fullscreen mode Exit fullscreen mode

The Main class is now directly responsible for creating the Student object.

That means:

  • Your code controls object creation.
  • Your code controls object configuration.
  • Your code controls dependency creation.
  • Your code controls the object lifecycle.

This may look simple in a small application. But in a real enterprise application, objects depend on many other objects.

For example:

UserController -> UserService -> UserRepository -> Database Connection
Enter fullscreen mode Exit fullscreen mode

If you manually create and connect everything using new, your code becomes tightly coupled and difficult to maintain.


2. What Is Inversion of Control?

Spring solves this problem using Inversion of Control, commonly known as IoC.

In simple words:

Instead of you creating objects manually, Spring creates and manages them for you.

This means the control is inverted.

In plain Java:

UserService userService = new UserService();
Enter fullscreen mode Exit fullscreen mode

You are creating the object.

In Spring Boot:

@Service
public class UserService {
}
Enter fullscreen mode Exit fullscreen mode

Spring creates the object and manages it inside its container.

That is the basic idea of IoC.

The core principle is:

Spring Boot takes responsibility for creating, configuring, connecting, and managing objects throughout the application.

This makes your application:

  • More loosely coupled
  • Easier to test
  • Easier to maintain
  • Cleaner in structure
  • Better for large-scale development

3. What Is a Spring Bean?

In Java, we usually say object.

In Spring, we say bean.

But what is the difference?

Java Object

A Java object is created manually by you using the new keyword.

Student student = new Student();
Enter fullscreen mode Exit fullscreen mode

Spring does not know about this object.

It is just a normal Java object.

Spring Bean

A Spring Bean is an object created, configured, and managed by the Spring IoC Container.

Example:

@Service
public class UserService {
}
Enter fullscreen mode Exit fullscreen mode

Here, UserService becomes a Spring Bean because Spring detects it during component scanning and registers it inside the application context.

So the simple difference is:

Type Created By Managed By Spring?
Java Object Developer No
Spring Bean Spring Container Yes

A bean is not just an object. It is an object whose lifecycle is fully managed by Spring.


4. What Is Dependency Injection?

If Inversion of Control is the concept, then Dependency Injection is the technique used to implement it.

Dependency Injection means:

Instead of a class creating its dependencies, Spring provides those dependencies from outside.

Let’s say UserController needs UserService.

Without Spring:

public class UserController {

    private UserService userService = new UserService();
}
Enter fullscreen mode Exit fullscreen mode

Here, UserController is tightly coupled with UserService.

With Spring:

@RestController
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now UserController does not create UserService.

Spring injects it automatically.

This is Dependency Injection.


5. Types of Dependency Injection in Spring Boot

Spring provides three main ways to inject dependencies.


5.1 Constructor Injection

This is the recommended and industry-standard approach.

@RestController
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }
}
Enter fullscreen mode Exit fullscreen mode

Why constructor injection is preferred:

  • It supports final fields.
  • It makes dependencies clear.
  • It prevents incomplete object creation.
  • It improves testability.
  • It avoids hidden dependencies.

This is the best option for production-level Spring Boot applications.


5.2 Setter Injection

In setter injection, dependencies are injected through setter methods.

@RestController
public class UserController {

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}
Enter fullscreen mode Exit fullscreen mode

Setter injection is useful when the dependency is optional or changeable.

However, it makes the object mutable because the dependency can be changed after object creation.


5.3 Field Injection

Field injection directly injects the dependency into the field.

@RestController
public class UserController {

    @Autowired
    private UserService userService;
}
Enter fullscreen mode Exit fullscreen mode

This looks simple, but it is not recommended for production code.

Problems with field injection:

  • Dependencies are hidden.
  • Unit testing becomes harder.
  • It tightly couples your class to the Spring framework.
  • You cannot use final fields.
  • The object can be created in an incomplete state.

So, as a best practice, use constructor injection.


6. BeanFactory vs ApplicationContext

The Spring IoC container is mainly represented by two interfaces:

BeanFactory
    ↑
ApplicationContext
Enter fullscreen mode Exit fullscreen mode

ApplicationContext extends BeanFactory.

Both are containers, but they are not the same.


BeanFactory

BeanFactory is the basic IoC container.

It provides the core functionality for creating and managing beans.

Important characteristics:

  • Lightweight
  • Lazy initialization
  • Creates beans only when requested
  • Basic dependency injection support

ApplicationContext

ApplicationContext is the advanced container used in modern Spring Boot applications.

It provides everything BeanFactory provides, plus additional enterprise features.

Important characteristics:

  • Eagerly creates singleton beans during startup
  • Supports internationalization
  • Supports event publishing
  • Supports AOP integration
  • Supports application environment and profiles
  • Used by Spring Boot applications by default

BeanFactory vs ApplicationContext

Feature BeanFactory ApplicationContext
Bean creation Lazy Mostly eager for singleton beans
Memory usage Lightweight Heavier
Dependency injection Supported Supported
AOP support Manual setup needed Built-in support
Event publishing Not supported Supported
Internationalization Not supported Supported
Common Spring Boot usage Rare Default

In modern Spring Boot applications, we usually work with ApplicationContext.


7. What Happens When a Spring Boot Application Starts?

Every Spring Boot application starts from the main method.

@SpringBootApplication
public class MentifyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MentifyApplication.class, args);
    }
}
Enter fullscreen mode Exit fullscreen mode

This line is very important:

SpringApplication.run(MentifyApplication.class, args);
Enter fullscreen mode Exit fullscreen mode

This is where the Spring Boot application starts.

Behind this single line, many things happen.


8. Spring Boot Startup Flow

Here is the high-level startup flow:

JVM Starts
    ↓
main() method runs
    ↓
SpringApplication.run()
    ↓
Environment is prepared
    ↓
ApplicationContext is created
    ↓
Component scanning happens
    ↓
Bean definitions are registered
    ↓
Beans are created
    ↓
Dependencies are injected
    ↓
Auto-configuration is applied
    ↓
Embedded server starts
    ↓
Application is ready
Enter fullscreen mode Exit fullscreen mode

Let’s understand this step by step.


Step 1: JVM Starts

The JVM starts and calls the main() method.

public static void main(String[] args) {
    SpringApplication.run(MentifyApplication.class, args);
}
Enter fullscreen mode Exit fullscreen mode

Step 2: SpringApplication.run() Executes

Spring Boot begins the application bootstrap process.

This is where a normal Java application starts becoming a Spring-powered application.


Step 3: Environment Is Prepared

Spring loads configuration details such as:

  • application.properties
  • application.yml
  • Active profiles
  • Environment variables
  • System properties

Example:

server.port=8080
spring.profiles.active=dev
Enter fullscreen mode Exit fullscreen mode

Step 4: ApplicationContext Is Created

Spring creates the correct type of ApplicationContext.

For a web application, Spring Boot commonly uses:

AnnotationConfigServletWebServerApplicationContext
Enter fullscreen mode Exit fullscreen mode

This context manages your beans and also starts the embedded web server.


Step 5: Component Scanning Happens

Spring scans your project to find classes annotated with stereotypes like:

@Component
@Service
@Repository
@Controller
@RestController
@Configuration
Enter fullscreen mode Exit fullscreen mode

Example:

@Service
public class UserService {
}
Enter fullscreen mode Exit fullscreen mode

Spring detects this class and registers it as a bean.


Step 6: Bean Creation and Dependency Injection

Spring creates bean objects and injects dependencies.

Example:

@RestController
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }
}
Enter fullscreen mode Exit fullscreen mode

Spring creates UserService first, then injects it into UserController.


Step 7: Auto-Configuration Is Applied

Spring Boot checks your classpath and automatically configures required components.

For example:

If Spring Boot finds web dependencies, it configures:

  • Embedded Tomcat
  • DispatcherServlet
  • JSON conversion
  • Web MVC setup

If Spring Boot finds database dependencies, it configures:

  • DataSource
  • EntityManagerFactory
  • TransactionManager
  • JPA repositories

This is why Spring Boot feels magical.

But it is not magic.

It is conditional auto-configuration.


Step 8: Embedded Server Starts

Spring Boot starts the embedded server, such as Tomcat.

Example:

Tomcat started on port 8080
Enter fullscreen mode Exit fullscreen mode

Now your application can receive HTTP requests.


Step 9: Application Is Ready

Finally, Spring publishes an application ready event.

At this point, your application is fully started and ready to handle requests.


9. Spring Bean Lifecycle

A Spring Bean is not simply created and forgotten.

It goes through a lifecycle.

The main stages are:

Bean Definition
    ↓
Instantiation
    ↓
Dependency Injection
    ↓
Initialization
    ↓
Ready to Use
    ↓
Destruction
Enter fullscreen mode Exit fullscreen mode

9.1 Bean Definition

Spring first identifies metadata about the bean.

For example:

@Service
public class UserService {
}
Enter fullscreen mode Exit fullscreen mode

Spring understands that UserService should be managed as a bean.


9.2 Instantiation

Spring creates the object in memory.

This is similar to calling:

new UserService();
Enter fullscreen mode Exit fullscreen mode

But instead of you doing it manually, Spring does it.


9.3 Dependency Injection

Spring injects required dependencies into the bean.

Example:

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}
Enter fullscreen mode Exit fullscreen mode

9.4 Initialization

After dependencies are injected, Spring allows custom initialization logic.

Example:

@PostConstruct
public void init() {
    System.out.println("UserService initialized");
}
Enter fullscreen mode Exit fullscreen mode

This method runs after the bean is created and dependencies are injected.


9.5 Ready to Use

Now the bean is fully ready and can be used inside the application.


9.6 Destruction

When the application shuts down, Spring destroys the bean.

Example:

@PreDestroy
public void destroy() {
    System.out.println("UserService destroyed");
}
Enter fullscreen mode Exit fullscreen mode

This is useful for cleanup operations such as closing resources.


10. Bean Scopes in Spring

Bean scope defines how long a bean lives and how many instances Spring creates.

Spring supports several scopes.


10.1 Singleton Scope

Singleton is the default scope.

@Service
public class UserService {
}
Enter fullscreen mode Exit fullscreen mode

Spring creates only one object of this bean for the entire application.

Every class that needs this bean gets the same instance.

UserController ---> same UserService object
AdminController ---> same UserService object
OrderController ---> same UserService object
Enter fullscreen mode Exit fullscreen mode

This is the most common scope in Spring Boot.


10.2 Prototype Scope

Prototype scope creates a new object every time the bean is requested.

@Component
@Scope("prototype")
public class ReportGenerator {
}
Enter fullscreen mode Exit fullscreen mode

Each request to the container creates a new instance.


10.3 Request Scope

Request scope is used in web applications.

A new bean instance is created for each HTTP request.

@Component
@Scope("request")
public class RequestData {
}
Enter fullscreen mode Exit fullscreen mode

10.4 Session Scope

Session scope creates one bean per user session.

@Component
@Scope("session")
public class UserSession {
}
Enter fullscreen mode Exit fullscreen mode

10.5 Application Scope

Application scope creates one bean for the entire servlet context.

@Component
@Scope("application")
public class AppConfig {
}
Enter fullscreen mode Exit fullscreen mode

11. Component Scanning in Spring Boot

The annotation @SpringBootApplication is a combination of three important annotations:

@SpringBootApplication
Enter fullscreen mode Exit fullscreen mode

Internally, it includes:

@Configuration
@EnableAutoConfiguration
@ComponentScan
Enter fullscreen mode Exit fullscreen mode

Let’s understand them.


@Configuration

This tells Spring that the class can define bean configurations.

@Configuration
public class AppConfig {
}
Enter fullscreen mode Exit fullscreen mode

@EnableAutoConfiguration

This enables Spring Boot’s auto-configuration mechanism.

Spring Boot checks the dependencies in your project and automatically configures matching features.


@ComponentScan

This tells Spring to scan the current package and its sub-packages for components.

Example project structure:

com.mentify
 ├── MentifyApplication.java
 ├── user
 │   ├── controller
 │   │   └── UserController.java
 │   └── service
 │       └── UserService.java
Enter fullscreen mode Exit fullscreen mode

If MentifyApplication.java is inside the com.mentify package, Spring scans:

com.mentify
com.mentify.user
com.mentify.user.controller
com.mentify.user.service
Enter fullscreen mode Exit fullscreen mode

So it will detect:

@RestController
public class UserController {
}
Enter fullscreen mode Exit fullscreen mode

And also:

@Service
public class UserService {
}
Enter fullscreen mode Exit fullscreen mode

12. Bean Name and Bean Type

When Spring detects a component, it registers it with two important details:

Bean Name
Bean Type
Enter fullscreen mode Exit fullscreen mode

Example:

@RestController
public class UserController {
}
Enter fullscreen mode Exit fullscreen mode

Spring registers it like this:

Bean Name: userController
Bean Type: com.mentify.user.controller.UserController
Enter fullscreen mode Exit fullscreen mode

By default, Spring uses the class name with the first letter converted to lowercase.

So:

UserController
Enter fullscreen mode Exit fullscreen mode

becomes:

userController
Enter fullscreen mode Exit fullscreen mode

This is how Spring identifies and manages beans internally.


13. Why This Matters for Real Projects

When you are building a small CRUD application, you can survive without understanding these concepts deeply.

But when you build production-level systems, you will face problems like:

  • Circular dependencies
  • Bean creation errors
  • Missing bean exceptions
  • Incorrect package scanning
  • Lazy vs eager initialization issues
  • Transaction proxy problems
  • Testing difficulties
  • Configuration conflicts

If you understand the IoC container, these problems become easier to debug.

Instead of guessing, you can reason about what Spring is doing internally.


Conclusion

Spring Boot is not magic.

It is a powerful framework built on clear principles:

  • Inversion of Control
  • Dependency Injection
  • Bean management
  • ApplicationContext
  • Component scanning
  • Auto-configuration
  • Bean lifecycle

At the beginner level, it is normal to focus only on writing APIs and getting output quickly.

But to grow as a strong backend developer, you need to understand what happens behind the scenes.

Once you understand the Spring IoC Container, you stop writing code blindly.

You start writing code with confidence.

You understand:

  • Who creates the object
  • Where the object lives
  • How dependencies are injected
  • When beans are initialized
  • How Spring Boot starts
  • Why certain errors happen

That is the point where you move from just using Spring Boot to truly understanding Spring Boot.


Final Thought

When I first started learning Spring Boot, I focused only on writing code and getting APIs to work.

But later, I realized that understanding the fundamentals is what makes debugging, scaling, and writing production-level applications much easier.

So before going deeper into advanced Spring Boot topics, take time to understand the IoC Container.

It is the heart of Spring.

What was your biggest learning moment when you moved from plain Java objects to Spring-managed beans?

Top comments (0)