Understanding IoC Container in Spring Boot with a Real-Time Project Example

A cup of JAVA coffee with NeeSri
13 min readAug 17, 2024

--

In this blog, we’ll explore the concept of the Inversion of Control (IoC) Container in Spring Boot, how it works, and illustrate its usage with a real-time project example.

What is an IoC Container?

Inversion of Control (IoC) is a design principle in which the control of object creation and management is transferred from the developer to the framework. The IoC Container in Spring Boot is the core component that handles the lifecycle of Spring beans (objects), manages their dependencies, and injects them where needed.

Inversion of Control (IoC) is a fundamental concept in Spring Boot (and the broader Spring Framework) where the control of object creation and lifecycle management is inverted from the programmer to the framework.

Traditionally, when writing code, the programmer is responsible for creating and managing objects (dependencies). In IoC, this responsibility is handed over to the framework (in this case, Spring). This design pattern helps in building loosely coupled applications, where objects do not need to know about how their dependencies are created.

Spring implements IoC through Dependency Injection (DI), which allows the framework to inject the dependencies required by an object at runtime.

In Spring Boot, the IoC container is primarily represented by the ApplicationContext. It is responsible for:

  • Bean Creation: Creating and initializing beans (objects) defined in the application context.
  • Dependency Injection: Injecting dependencies (beans) into other beans.
  • Bean Lifecycle Management: Managing the lifecycle of beans such as initialization and destruction.

Types of IoC Containers in Spring:

  • BeanFactory: The most basic IoC container that provides basic dependency injection.
  • ApplicationContext: A more advanced container that provides additional features such as event propagation, declarative mechanisms to create a bean, etc.

Types of Dependency Injection

  1. Constructor Injection: Dependencies are provided through the constructor of the class.
  2. Setter Injection: Dependencies are provided through setter methods.
  3. Field Injection: Dependencies are injected directly into the fields using annotations.

Real-Time Project Example: Building a Notification Service

Scenario:

Imagine you’re working on a notification service that sends notifications via multiple channels, such as email and SMS. Each notification service (email or SMS) needs specific configuration and dependencies. The goal is to manage these services using the IoC container in Spring Boot.

Step-by-Step Guide:

1. Create Interfaces for the Notification Service

Start by defining interfaces for your notification services. These interfaces define the contract for sending notifications.

public interface NotificationService {
void sendNotification(String message);
}

2. Implement Email and SMS Notification Services

Now, create two implementations for the NotificationService interface: one for email notifications and one for SMS notifications.

Email Notification Service:

import org.springframework.stereotype.Service;

@Service
public class EmailNotificationService implements NotificationService {

@Override
public void sendNotification(String message) {
System.out.println("Sending email notification: " + message);
}
}

SMS Notification Service:

import org.springframework.stereotype.Service;

@Service
public class SmsNotificationService implements NotificationService {

@Override
public void sendNotification(String message) {
System.out.println("Sending SMS notification: " + message);
}
}

Here, @Service annotations mark these classes as Spring-managed beans. When the application starts, the IoC container will create and manage the lifecycle of these beans.

3. Define a NotificationController to Use the Services

Create a controller that uses the notification services. Here, we’ll demonstrate constructor-based dependency injection, a preferred method of injecting dependencies.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class NotificationController {

private final EmailNotificationService emailNotificationService;
private final SmsNotificationService smsNotificationService;

@Autowired
public NotificationController(EmailNotificationService emailNotificationService,
SmsNotificationService smsNotificationService) {
this.emailNotificationService = emailNotificationService;
this.smsNotificationService = smsNotificationService;
}

@GetMapping("/sendEmail")
public String sendEmail(@RequestParam String message) {
emailNotificationService.sendNotification(message);
return "Email notification sent!";
}

@GetMapping("/sendSms")
public String sendSms(@RequestParam String message) {
smsNotificationService.sendNotification(message);
return "SMS notification sent!";
}
}

Explanation:

  • NotificationController depends on EmailNotificationService and SmsNotificationService.
  • Spring’s IoC container injects these services into the controller through the constructor, ensuring that the correct implementations are used.
  • The @GetMapping annotations map HTTP requests to the respective notification service.

4. Application Setup

The Spring Boot application will bootstrap using the ApplicationContext, which is the IoC container.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class NotificationServiceApplication {
public static void main(String[] args) {
SpringApplication.run(NotificationServiceApplication.class, args);
}
}

When the application starts, Spring Boot will automatically scan for components (@Component, @Service, @Controller), register them as beans, and handle their lifecycle.

5. Running the Application

Once the application is up and running, you can test the notification services by making HTTP GET requests:

How the IoC Container Works in This Example:

  • Bean Creation: The IoC container creates beans for EmailNotificationService, SmsNotificationService, and NotificationController.
  • Dependency Injection: The container injects the EmailNotificationService and SmsNotificationService beans into the NotificationController through constructor injection.
  • Bean Management: The lifecycle of each bean is managed by the IoC container. For example, when the application starts, the container initializes the beans, and when the application stops, the container can handle the destruction of beans if needed.

Benefits of Using IoC Container in Spring Boot

  1. Loose Coupling: The controller and services are loosely coupled because they rely on interfaces and are not responsible for creating instances of the services.
  2. Maintainability: If you need to add new notification services (e.g., push notifications), you can do so without modifying the core logic of your application.
  3. Testability: IoC makes it easier to mock dependencies and write unit tests since you can easily swap implementations.
  4. Centralized Configuration: The IoC container manages the creation and configuration of beans, reducing the amount of boilerplate code.

Interview Questions on IoC (Inversion of Control) and DI (Dependency Injection) in Spring Boot:

1. What is Inversion of Control (IoC) in Spring Boot?

Answer: Inversion of Control (IoC) is a design principle where the control of object creation, configuration, and management is transferred from the developer to the framework. In Spring Boot, the IoC container (represented by the ApplicationContext) is responsible for creating, managing, and injecting beans (objects) where needed. This leads to better separation of concerns, loose coupling, and easier testability.

2. What is Dependency Injection (DI), and how is it related to IoC?

Answer: Dependency Injection (DI) is a specific type of IoC where the framework automatically provides the dependencies that a class needs, instead of the class creating its own dependencies. DI can be implemented in three ways: Constructor Injection, Setter Injection, and Field Injection. Spring Boot uses DI to wire up beans at runtime, thus promoting loose coupling.

3. What are the different types of Dependency Injection in Spring Boot?

Answer:

  • Constructor Injection: Dependencies are injected through the constructor.
  • Setter Injection: Dependencies are injected via setter methods.
  • Field Injection: Dependencies are injected directly into fields using annotations like @Autowired.

4. What are the advantages of using IoC and DI in Spring Boot?

Answer:

  • Loose Coupling: Classes are less dependent on each other because the IoC container manages the dependencies.
  • Better Testability: Since dependencies are injected, it’s easier to replace them with mocks during testing.
  • Code Reusability and Flexibility: You can switch between different implementations of the same interface without changing the core business logic.

5. How does the IoC container work in Spring Boot?

Answer: The IoC container in Spring Boot, typically ApplicationContext, scans the classpath for components annotated with @Component, @Service, @Repository, or @Controller. It then creates instances of these beans, manages their lifecycle, and injects dependencies as required, usually through DI.

6. What is the difference between BeanFactory and ApplicationContext in Spring?

Answer:

  • BeanFactory: It is the simplest IoC container in Spring and is responsible for basic DI. It lazily initializes beans only when they are requested.
  • ApplicationContext: It extends BeanFactory with more advanced features like event propagation, declarative bean creation, AOP, and more. It eagerly loads beans at startup.

7. How do you inject a bean in Spring Boot?

  • Answer: Beans can be injected using the @Autowired annotation. The most recommended way is Constructor Injection.
@Autowired
public MyService(MyDependency myDependency) {
this.myDependency = myDependency;
}

8. What is the role of @Autowired in Spring Boot?

Answer: @Autowired is an annotation used by Spring to automatically inject beans into other beans. It tells the IoC container to resolve the dependency and inject it at runtime.

9. What happens if there are multiple beans of the same type? How do you handle that in Spring Boot?

Answer: If there are multiple beans of the same type, you can specify which bean should be injected using the @Qualifier annotation along with @Autowired. This helps to avoid ambiguity when Spring needs to inject a specific bean.

@Autowired
@Qualifier("specificBean")
private MyService myService;

10. Can you explain the lifecycle of a Spring Bean?

Answer:

The lifecycle of a Spring Bean involves several stages:

  • Instantiation: The IoC container creates an instance of the bean.
  • Dependency Injection: Dependencies are injected into the bean.
  • Initialization: The bean’s @PostConstruct method or any custom initialization method is called.
  • Usage: The bean is ready to be used by the application.
  • Destruction: Before the container shuts down, the bean’s @PreDestroy method or any custom destroy method is called.

11. Can you inject a prototype bean into a singleton bean? What challenges does that create?

Answer:

  • Yes, you can inject a prototype bean into a singleton bean, but it creates a challenge because Spring only injects the prototype bean once at the time of injection. The singleton bean will keep reusing the same instance of the prototype bean, defeating the purpose of the prototype scope. To solve this, you can use ObjectFactory or @Scope("prototype") in combination with @Lookup.

12. What is the difference between @Component, @Service, @Repository, and @Controller in Spring?

Answer: All these annotations are used to define Spring beans, but they have specific roles:

  • @Component: A general-purpose stereotype annotation for any Spring-managed component.
  • @Service: Specifically for service layer classes.
  • @Repository: Specifically for data access layer classes, with additional benefits like exception translation.
  • @Controller: Specifically for web layer classes that handle HTTP requests.

13. How do you configure custom beans in Spring Boot?

Answer: You can configure custom beans by using the @Bean annotation in a @Configuration class. This allows you to define beans that are not automatically detected by component scanning.

@Configuration
public class AppConfig {

@Bean
public MyService myService() {
return new MyServiceImpl();
}
}

14. What is the use of the @Primary annotation in Spring Boot?

Answer: @Primary is used to designate a bean as the default bean to be injected when multiple beans of the same type are available. If no @Qualifier is specified, Spring will use the @Primary bean by default.

15. What is the role of @Primary in Spring IoC? Can you explain a scenario where you need to use it?

Answer:

  • The @Primary annotation is used to mark a bean as the default choice when there are multiple beans of the same type, and Spring needs to decide which one to inject.
  • Tricky Scenario: Suppose you have multiple implementations of an interface, and Spring needs to inject one of them without a @Qualifier annotation. Using @Primary helps Spring decide which bean to use.
@Primary
@Service
public class DefaultServiceImpl implements MyService {
// implementation
}

@Service
public class OtherServiceImpl implements MyService {
// implementation
}

In this case, Spring will inject DefaultServiceImpl whenever MyService is autowired unless another bean is specifically qualified.

16. How does the IoC container handle bean scopes in Spring Boot?

Answer:

The Spring IoC container manages beans based on different scopes, and these scopes define the lifecycle of a bean.

Tricky Follow-Up:

  • Singleton Scope: The bean is created once per container and reused across the application. This is the default scope.
  • Prototype Scope: A new instance of the bean is created every time it is requested.
  • Session and Request Scopes: These scopes are typically used in web applications to create beans for the duration of an HTTP session or request.

Tricky Question: Can a prototype bean be injected into a singleton bean?

  • Yes, but the prototype bean will only be injected once. To resolve this, you can use ObjectFactory or @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) to ensure a new instance is injected each time it is requested.

17. What is the @Lookup annotation in Spring, and how does it relate to IoC?

Answer: The @Lookup annotation is used to inject a prototype-scoped bean into a singleton bean. It tells Spring to override the method at runtime and return a new bean instance every time the method is called.

@Component
public class SingletonBean {

@Lookup
public PrototypeBean getPrototypeBean() {
// Spring will override this method at runtime
return null;
}
}

Tricky Follow-Up: What are the benefits of using @Lookup over ObjectFactory?

  • @Lookup provides a cleaner approach and better readability when dealing with prototype beans, while ObjectFactory gives more control but is less intuitive.

18. What is the difference between @ComponentScan and @Import in Spring Boot?

Answer:

  • @ComponentScan: This annotation is used to scan for Spring-managed beans within specified packages or classes.
  • @Import: This annotation is used to import additional configuration classes into the current context.

Tricky Scenario:

  • If you are working with modular applications and want to include a specific configuration class from another module, @Import will be useful, while @ComponentScan is more general for scanning components in the current or related packages.
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
}

@Import(AnotherConfig.class)
public class AppConfig {
}

The key difference is that @Import is more explicit, importing specific configurations, whereas @ComponentScan is about discovering components dynamically in the specified packages.

19. How does the Spring IoC container handle circular dependencies?

Answer: Spring IoC container can handle circular dependencies using setter-based injection or field injection. However, it cannot handle circular dependencies with constructor injection.

Tricky Scenario: Suppose you have two beans, A and B, where A depends on B and B depends on A. If both beans are injected using constructors, the IoC container will throw a BeanCurrentlyInCreationException because it can’t resolve the circular dependency.

Solution: Use setter-based or field-based injection:

public class A {
private B b;

@Autowired
public void setB(B b) {
this.b = b;
}
}

public class B {
private A a;

@Autowired
public void setA(A a) {
this.a = a;
}
}

Tricky Follow-Up: How does @Lazy help in resolving circular dependencies?

  • The @Lazy annotation can defer bean initialization, which can help break circular dependencies by delaying the injection until it is actually needed.

20. Can you explain the difference between @Autowired and @Inject in the Spring IoC context?

Answer:

  • @Autowired: This is the Spring-specific annotation used for dependency injection. It can be applied to constructors, setters, or fields.
  • @Inject: This is the JSR-330 (Java standard) annotation, which is part of the CDI (Contexts and Dependency Injection) specification. Spring supports @Inject as well, making it interchangeable with @Autowired.

Tricky Follow-Up: Which one should you use, @Autowired or @Inject?

  • In Spring applications, you can use either @Autowired or @Inject. However, if you want to write more portable code that may work in other CDI-compliant frameworks, you should use @Inject.

21. How does the @Bean annotation work under the IoC container? Can you have multiple @Bean methods for the same type?

Answer:

  • The @Bean annotation is used to define a method that returns a bean to be managed by the Spring IoC container. The method name is used as the bean's ID by default unless explicitly provided.

Tricky Scenario: Can you define multiple @Bean methods that return beans of the same type?

  • Yes, you can define multiple @Bean methods that return beans of the same type. If Spring needs to inject one of them, you can either use @Primary or @Qualifier to specify which one should be injected.
@Bean
public MyService myService1() {
return new MyServiceImpl1();
}

@Bean
public MyService myService2() {
return new MyServiceImpl2();
}

Follow-Up: How does the IoC container decide which bean to inject if you don’t use @Primary or @Qualifier?

  • If neither @Primary nor @Qualifier is used, the IoC container will throw a NoUniqueBeanDefinitionException because it won't know which bean to inject.

22. How does Spring IoC handle exception translation, especially in the data access layer?

Answer:

Spring provides exception translation through the @Repository annotation, which marks a class as a Data Access Object (DAO). Spring wraps any data access exceptions (like SQLException) into Spring's own unchecked DataAccessException.

Tricky Follow-Up: What are the benefits of exception translation in IoC?

  • Consistent Exception Handling: It abstracts underlying exceptions (e.g., JDBC exceptions) into a common hierarchy.
  • Decoupling: The service layer doesn’t need to worry about handling technology-specific exceptions and can handle generic exceptions instead.

23. What is the role of BeanPostProcessor in the Spring IoC container?

Answer:

A BeanPostProcessor is an interface provided by Spring that allows you to perform some operations before and after the initialization of a bean.

public class CustomBeanPostProcessor implements BeanPostProcessor {

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// Code before bean initialization
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// Code after bean initialization
return bean;
}
}

Tricky Follow-Up: Can BeanPostProcessor prevent the creation of a bean?

  • Yes, by returning null from the postProcessBeforeInitialization() or postProcessAfterInitialization() methods, you can prevent the bean from being created and initialized in the Spring IoC container.

24. How does the IoC container handle @Configuration classes in Spring Boot?

Answer:

The IoC container treats @Configuration classes as special beans. When Spring processes a class annotated with @Configuration, it not only registers all @Bean methods but also ensures that the beans are properly managed, including handling dependencies and lifecycle callbacks.

Tricky Scenario: What happens if a @Bean method calls another @Bean method in the same @Configuration class?

  • Spring ensures that beans are managed properly. It does not invoke the method directly but intercepts the call and returns the existing singleton bean, preventing multiple instances from being created.

25. What is @depedensOn Annotation?

(Call second bean before the first bean)

The @DependsOn annotation can force Spring IoC container to initialize one or more beans before the bean which is annotated by @DependsOn annotation.

In Spring Boot, the @DependsOn annotation is used to specify that a bean should be initialized only after another bean has been initialized. This is especially useful when there is an explicit dependency between beans that is not managed by constructor or setter injection.

Use Case for @DependsOn

Imagine a scenario where one bean requires another to be initialized first because it depends on some side effect, such as initializing a database connection, loading configuration, or setting up a cache.

Example:

Let’s say we have two beans, CacheService and DatabaseService. The CacheService bean needs to be initialized only after the DatabaseService bean is fully set up because the cache depends on data loaded from the database.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service;

@Configuration
public class AppConfig {

@Bean
public DatabaseService databaseService() {
return new DatabaseService();
}

@Bean
@DependsOn("databaseService")
public CacheService cacheService() {
return new CacheService();
}
}

@Service
public class DatabaseService {
public DatabaseService() {
System.out.println("Initializing DatabaseService...");
// Simulate database initialization
connectToDatabase();
}

private void connectToDatabase() {
System.out.println("Connected to Database.");
}
}

@Service
public class CacheService {
public CacheService() {
System.out.println("Initializing CacheService...");
// Simulate cache setup
setupCache();
}

private void setupCache() {
System.out.println("Cache is set up.");
}
}

Explanation:

  1. DatabaseService:
  • This service is initialized first. It simulates connecting to a database, which might be a prerequisite for setting up the cache.
  1. CacheService:
  • The CacheService is dependent on DatabaseService. By using the @DependsOn("databaseService") annotation, Spring ensures that the DatabaseService is initialized before the CacheService.

Output:

When the Spring Boot application starts, you will see the following output:

Initializing DatabaseService...
Connected to Database.
Initializing CacheService...
Cache is set up.

This ensures that the CacheService is initialized only after the DatabaseService has been fully initialized and connected to the database.

Use Cases for @DependsOn

  • Initialization Order: You have services that must be initialized in a particular order.
  • External Resources: One service depends on an external resource (e.g., database, messaging queue) initialized by another bean.
  • Caching: The cache must be populated only after certain data is loaded or resources are available.

Important Notes:

  • Be Careful with Circular Dependencies: When using @DependsOn, make sure that there is no circular dependency, as this can cause startup failures.
  • Order Not Guaranteed by Default: By default, Spring Boot does not guarantee the order of bean initialization unless explicitly stated using @Dependson.

--

--

Responses (1)