Dependency Injection Spring
Problem
Managing object dependencies manually leads to tight coupling, difficult testing, and complex initialization logic. Creating dependencies directly in classes makes code hard to modify and test.
// Problematic approach - tight coupling
public class UserService {
private UserRepository repository = new UserRepositoryImpl(); // Hard to test
private EmailService emailService = new EmailServiceImpl(); // Hard to mock
public void createUser(User user) {
repository.save(user);
emailService.sendWelcomeEmail(user);
}
}This guide shows practical techniques for implementing dependency injection with Spring’s IoC container.
Solution
1. Constructor Injection (Recommended)
Constructor injection makes dependencies explicit and enables immutability.
Setup (Maven dependency):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.2.1</version>
</dependency>Service with constructor injection:
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
// Constructor injection - dependencies are final (immutable)
public UserService(UserRepository userRepository,
EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public void createUser(User user) {
userRepository.save(user);
emailService.sendWelcomeEmail(user);
}
}
@Repository
public class UserRepositoryImpl implements UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepositoryImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void save(User user) {
String sql = "INSERT INTO users (username, email) VALUES (?, ?)";
jdbcTemplate.update(sql, user.getUsername(), user.getEmail());
}
}
@Service
public class EmailServiceImpl implements EmailService {
private final JavaMailSender mailSender;
public EmailServiceImpl(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void sendWelcomeEmail(User user) {
// Send email implementation
System.out.println("Sending welcome email to: " + user.getEmail());
}
}2. Bean Configuration
Configure beans using Java-based configuration or annotations.
Java configuration approach:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
dataSource.setUsername("user");
dataSource.setPassword("password");
dataSource.setMaximumPoolSize(10);
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
@Profile("dev")
public EmailService devEmailService() {
return new ConsoleEmailService(); // Logs to console
}
@Bean
@Profile("prod")
@Primary
public EmailService prodEmailService(JavaMailSender mailSender) {
return new SmtpEmailService(mailSender); // Sends real emails
}
@Bean
public UserService userService(UserRepository userRepository,
EmailService emailService) {
return new UserService(userRepository, emailService);
}
}Component scanning approach:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = "com.example") // Scan for components
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// Beans discovered automatically with stereotype annotations
@Service // Business logic
@Repository // Data access
@Controller // Web controllers
@Component // Generic components3. Dependency Resolution Patterns
Handle multiple implementations and conditional beans.
Qualifier for multiple implementations:
public interface NotificationService {
void sendNotification(String message);
}
@Service
@Qualifier("email")
public class EmailNotificationService implements NotificationService {
@Override
public void sendNotification(String message) {
System.out.println("Email: " + message);
}
}
@Service
@Qualifier("sms")
public class SmsNotificationService implements NotificationService {
@Override
public void sendNotification(String message) {
System.out.println("SMS: " + message);
}
}
@Service
public class NotificationManager {
private final NotificationService emailService;
private final NotificationService smsService;
public NotificationManager(
@Qualifier("email") NotificationService emailService,
@Qualifier("sms") NotificationService smsService
) {
this.emailService = emailService;
this.smsService = smsService;
}
public void sendAll(String message) {
emailService.sendNotification(message);
smsService.sendNotification(message);
}
}Conditional beans:
import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.context.annotation.Conditional;
@Configuration
public class ConditionalConfig {
@Bean
@ConditionalOnProperty(name = "feature.cache.enabled", havingValue = "true")
public CacheService cacheService() {
return new RedisCacheService();
}
@Bean
@ConditionalOnMissingBean(CacheService.class)
public CacheService noCacheService() {
return new NoOpCacheService(); // Fallback when cache disabled
}
@Bean
@ConditionalOnClass(name = "com.example.AdvancedFeature")
public AdvancedService advancedService() {
return new AdvancedServiceImpl();
}
@Bean
@ConditionalOnMissingClass("com.example.AdvancedFeature")
public AdvancedService basicService() {
return new BasicServiceImpl();
}
}4. Bean Lifecycle Management
Control bean initialization and destruction.
Lifecycle callbacks:
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
@Service
public class DatabaseService {
private Connection connection;
// Method 1: @PostConstruct annotation (recommended)
@PostConstruct
public void initialize() {
System.out.println("Initializing database connection...");
connection = createConnection();
}
@PreDestroy
public void cleanup() {
System.out.println("Closing database connection...");
if (connection != null) {
connection.close();
}
}
// Method 2: InitializingBean interface
@Override
public void afterPropertiesSet() throws Exception {
// Called after all properties set
System.out.println("All properties configured");
}
// Method 3: DisposableBean interface
@Override
public void destroy() throws Exception {
// Called on bean destruction
System.out.println("Bean destroyed");
}
private Connection createConnection() {
// Connection creation logic
return new Connection();
}
}
// Method 4: Custom init/destroy methods in @Bean
@Configuration
public class Config {
@Bean(initMethod = "init", destroyMethod = "close")
public CustomService customService() {
return new CustomService();
}
}How It Works
Spring IoC Container Lifecycle
graph TD
A[Application Start] --> B[Load Configuration]
B --> C[Component Scan]
C --> D[Bean Definition Registry]
D --> E[Dependency Resolution]
E --> F{All Dependencies Available?}
F -->|Yes| G[Bean Instantiation]
F -->|No| H[Throw NoSuchBeanDefinitionException]
G --> I[Dependency Injection]
I --> J[@PostConstruct Callbacks]
J --> K[Bean Ready]
K --> L[Application Running]
L --> M[Application Shutdown]
M --> N[@PreDestroy Callbacks]
N --> O[Beans Destroyed]
style A fill:#0173B2,stroke:#000000,color:#FFFFFF
style D fill:#DE8F05,stroke:#000000,color:#FFFFFF
style F fill:#029E73,stroke:#000000,color:#FFFFFF
style G fill:#CC78BC,stroke:#000000,color:#FFFFFF
style K fill:#029E73,stroke:#000000,color:#FFFFFF
%% Color palette: Blue (#0173B2), Orange (#DE8F05), Teal (#029E73), Purple (#CC78BC)
%% Blue = Start, Orange = Registry, Teal = Decision/Success, Purple = Instantiation
Key concepts:
- IoC Container: Spring ApplicationContext manages bean lifecycle and dependencies
- Dependency Injection: Container injects dependencies automatically (constructor, setter, or field)
- Bean Scopes: Control bean lifecycle (singleton, prototype, request, session, application)
- Stereotype Annotations: @Service, @Repository, @Controller, @Component mark beans for scanning
Bean Scopes
Spring supports multiple bean scopes:
- Singleton (default): One instance per Spring container
- Prototype: New instance each time bean is requested
- Request: One instance per HTTP request (web apps)
- Session: One instance per HTTP session (web apps)
- Application: One instance per ServletContext (web apps)
import org.springframework.context.annotation.Scope;
import org.springframework.web.context.WebApplicationContext;
@Service
@Scope("singleton") // Default - single instance
public class SingletonService { }
@Service
@Scope("prototype") // New instance per request
public class PrototypeService { }
@Service
@Scope(WebApplicationContext.SCOPE_REQUEST)
public class RequestScopedService { }Variations
Field Injection (Not Recommended)
Field injection is concise but has drawbacks:
import org.springframework.beans.factory.annotation.Autowired;
@Service
public class UserService {
@Autowired // Field injection - avoid in new code
private UserRepository userRepository;
// Issues:
// 1. Cannot make fields final (no immutability)
// 2. Hard to test (requires reflection to inject mocks)
// 3. Hides dependencies (not visible in constructor)
// 4. Circular dependencies not caught at compile time
}Setter Injection
Setter injection allows optional dependencies:
@Service
public class UserService {
private UserRepository userRepository;
private CacheService cacheService; // Optional dependency
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository; // Required
}
@Autowired(required = false)
public void setCacheService(CacheService cacheService) {
this.cacheService = cacheService; // Optional
}
}Lookup Method Injection
Inject prototype beans into singleton beans:
import org.springframework.beans.factory.annotation.Lookup;
@Service
public abstract class CommandManager {
@Lookup
protected abstract Command createCommand(); // Returns prototype bean
public void process() {
Command command = createCommand(); // New instance each time
command.execute();
}
}
@Component
@Scope("prototype")
public class Command {
public void execute() {
System.out.println("Executing command");
}
}Common Pitfalls
Pitfall 1: Circular Dependencies
Avoid circular dependencies between beans:
// Bad: Circular dependency
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; }
}
@Service
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) { this.serviceA = serviceA; }
}
// Throws BeanCurrentlyInCreationException
// Good: Refactor to remove circular dependency
@Service
public class ServiceA {
private final SharedService sharedService;
public ServiceA(SharedService sharedService) {
this.sharedService = sharedService;
}
}
@Service
public class ServiceB {
private final SharedService sharedService;
public ServiceB(SharedService sharedService) {
this.sharedService = sharedService;
}
}Pitfall 2: Missing @Component Annotations
Beans must be annotated or configured:
// Bad: No annotation - bean not registered
public class MyService {
// Not managed by Spring
}
// Good: Annotated with stereotype
@Service
public class MyService {
// Managed by Spring
}
// Or configure in @Configuration class
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyService();
}
}Pitfall 3: Prototype Beans in Singleton
Prototype beans injected into singletons remain same instance:
// Bad: Prototype bean injected once into singleton
@Service // Singleton scope
public class SingletonService {
private final PrototypeBean prototypeBean;
public SingletonService(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean; // Same instance always
}
}
// Good: Use Provider or Lookup method
@Service
public class SingletonService {
private final Provider<PrototypeBean> prototypeBeanProvider;
public SingletonService(Provider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public void doSomething() {
PrototypeBean bean = prototypeBeanProvider.get(); // New instance
}
}Pitfall 4: Field Injection in Tests
Field injection makes unit testing difficult:
// Bad: Field injection - hard to test
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // Cannot mock easily
}
// Good: Constructor injection - easy to test
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
// Test with mock
@Test
public void testCreateUser() {
UserRepository mockRepo = Mockito.mock(UserRepository.class);
UserService service = new UserService(mockRepo); // Easy to inject mock
// Test service with mock repository
}Related Patterns
Related Tutorial: See Beginner Tutorial - Dependency Injection Basics for DI fundamentals and Intermediate Tutorial - Spring Framework for Spring IoC container details.
Related How-To: See Write Effective Tests for testing with dependency injection and Build REST APIs with Spring for DI in web controllers.
Related Cookbook: See Cookbook recipes “Spring Bean Configuration”, “Conditional Bean Creation”, and “Bean Lifecycle Management” for copy-paste ready DI patterns.
Related Explanation: See Best Practices - Dependency Injection for DI design principles.
Further Reading
- Spring Framework Reference - IoC Container - Official Spring IoC documentation
- Dependency Injection Principles - Martin Fowler on DI patterns
- Spring Boot Auto-Configuration - Spring Boot DI features
- Effective Java (Item 5) - Prefer dependency injection to hardwiring resources