Spring Dependency Injection Crash Course – @Autowired, Bean Scopes, Constructor Injection
Java developers often encounter repetitive boilerplate code when writing larger applications. Enter Spring Dependency Injection (DI)—a key feature of the Spring Framework that simplifies code structure, improves maintainability, and encourages scalability. Whether you’re a newcomer or an experienced professional, understanding DI is crucial to mastering Spring development in 2025.
This crash course will take you through Dependency Injection fundamentals, types of DI in Spring, the role of the IOC container, and advanced topics like bean scopes, qualifiers, circular dependencies, and DI in testing. Additionally, a real-world example will demonstrate how to wire a service layer effectively.
Table of Contents
- The IOC Container – Bean Creation & Lifecycle
- Using @Component, @Service, and @Repository
- Working with @Autowired and Qualifiers
- Using Configuration Classes and @Bean Methods
- Understanding Bean Scopes – Singleton, Prototype, Request, Session
- Handling Circular Dependencies and Avoiding Them
- Dependency Injection in Testing with Mocked Dependencies
- Real Project Example – DI in the Service Layer
- FAQs About Spring Dependency Injection
What is Dependency Injection and Why Does It Matter?
Dependency Injection (DI) is a design pattern that allows the Spring Framework to inject required dependencies (objects) into other classes without the developer having to instantiate them manually.
Why Dependency Injection Is Important:
- Loose Coupling: Classes are not tightly bound to each other, making them easier to replace, mock, or modify.
- Improved Testability: Since dependencies are injected, they can be mocked during unit testing.
- Code Reusability: Components can be reused in multiple scenarios without modification.
- Simplified Scalability: Adding new functionality is easier, as dependent objects can be swapped without additional configuration.
Now, let’s explore how DI is implemented in Spring through several techniques.
Types of Dependency Injection in Spring
Constructor Injection
Constructor injection involves passing dependencies via the class constructor. It’s the most preferred method because it ensures that required dependencies are not left uninitialized.
Example:
@Component public class UserService { private final UserRepository userRepository; @Autowired public UserService(UserRepository userRepository) { this.userRepository = userRepository; } }
Here, Spring injects UserRepository
when creating an instance of UserService
.
Setter Injection
Setter injection uses a public setter method to pass dependencies. Although more flexible than constructor injection, it risks leaving dependencies unset.
Example:
@Component public class UserService { private UserRepository userRepository; @Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } }
Setter injection is often used for optional dependencies.
Field Injection
Field injection leverages the @Autowired
annotation directly over class fields. It’s concise but discouraged in current practices because the dependency remains hidden.
Example:
@Component public class UserService { @Autowired private UserRepository userRepository; }
Consider replacing field injection with constructor injection for better maintainability.
The IOC Container – Bean Creation & Lifecycle
Spring’s IOC (Inversion of Control) Container is the core mechanism responsible for managing the lifecycle of beans (objects managed by Spring). It creates, configures, and manages the dependencies of beans.
Bean Lifecycle Overview:
- Instantiation: The IOC container creates a bean instance.
- Dependency Injection: Dependencies are resolved and injected into the bean.
- Lifecycle Hooks: Developers can customize bean behavior using
@PostConstruct
and@PreDestroy
.
Example Bean Lifecycle Hooks:
@Component public class TaskManager { @PostConstruct public void initialize() { System.out.println("Task Manager initialized"); } @PreDestroy public void shutdown() { System.out.println("Task Manager shutting down..."); } }
Using @Component, @Service, and @Repository
Spring provides stereotype annotations to define different roles of components. All of these annotations register classes as beans in the container automatically.
1. @Component: Generic annotation for any Spring-managed bean.
2. @Service: Specifically used to indicate business logic components.
3. @Repository: Indicates DAO (Data Access Object) components, providing exception translation for persistence layers.
Working with @Autowired and Qualifiers
The @Autowired
annotation tells Spring how to resolve dependencies automatically by searching for a matching bean in the container.
Qualifiers for Multiple Beans
When multiple beans of the same type exist, use @Qualifier
to specify which one to inject.
Example:
@Component public class UserService { private final UserNotification notification; @Autowired public UserService(@Qualifier("emailNotification") UserNotification notification) { this.notification = notification; } }
Using Configuration Classes and @Bean Methods
Instead of relying on annotations only, you can define beans explicitly using configuration classes.
Example Configuration:
@Configuration public class AppConfig { @Bean public UserService userService() { return new UserService(userRepository()); } @Bean public UserRepository userRepository() { return new UserRepository(); } }
Understanding Bean Scopes – Singleton, Prototype, Request, Session
- Singleton (Default): A single shared instance of the bean across the application lifecycle.
- Prototype: A new instance is created every time the bean is requested.
- Request: A single instance is created per HTTP request.
- Session: One instance per user session.
Example of Scopes:
@Component @Scope("prototype") public class TaskHandler { // This bean is created fresh for every request }
Handling Circular Dependencies and Avoiding Them
Spring automatically resolves most circular dependencies, but certain use cases require manual intervention.
- Use Constructor Injection: Circular dependencies cannot be resolved with constructors. Use
@Lazy
or setter injection instead.
Solution Example:
@Component public class ClassA { @Autowired @Lazy private ClassB classB; } @Component public class ClassB { @Autowired @Lazy private ClassA classA; }
Dependency Injection in Testing with Mocked Dependencies
With Spring, you can use @MockBean (from Spring Boot Test) to inject mocks into your tests.
Example:
@WebMvcTest public class UserServiceTest { @MockBean private UserRepository userRepository; @Autowired private UserService userService; @Test public void testCreateUser() { User user = new User("Jane", "[email protected]"); when(userRepository.save(user)).thenReturn(user); Assertions.assertEquals("Jane", userService.createUser(user).getName()); } }
Real Project Example – DI in the Service Layer
Imagine an e-commerce application where a payment service depends on a user repository and notification service.
PaymentService.java:
@Service public class PaymentService { @Autowired private UserRepository userRepository; @Autowired private NotificationService notificationService; public void processPayment(Long userId, String paymentInfo) { User user = userRepository.findById(userId).orElseThrow(); notificationService.send(user.getEmail(), "Payment processed successfully!"); } }
This setup demonstrates seamless DI with minimal boilerplate code.
FAQs About Spring Dependency Injection
1. Can Spring manage non-Spring components?
No, only classes registered as Beans or @Components can be managed by the Spring container.
2. Which DI type is better—Constructor or Setter?
Constructor injection is preferred due to its immutability and guaranteed dependency resolution.
3. How can I debug Spring DI issues?
Use the Spring Boot Actuator to inspect bean definitions and dependencies at runtime.
By understanding and implementing the concepts discussed above, you can build scalable and maintainable Spring-based applications. Dependency Injection isn’t just a tool—it’s the backbone of creating efficient, testable code in today’s fast-evolving tech landscape.