Dependency Injection : SpringBoot
Dependency Injection is one of the core concepts that makes Spring Boot so powerful and popular among Java developers. If you've ever wondered how Spring magically "wires" your components together, this article is for you. Let's break down the three main types of dependency injection in Spring Boot with simple, practical examples.
What is Dependency Injection?
Think of dependency injection like ordering food at a restaurant. Instead of going to the kitchen yourself to cook (creating dependencies manually), you simply tell the waiter what you want, and the kitchen prepares and delivers it to your table. Similarly, Spring Boot manages creating and delivering the objects your classes need.
⭕ Before Dependency Injection:
public class OrderService {
private EmailService emailService;
public OrderService() {
// Tightly coupled - we create the dependency ourselves
this.emailService = new EmailService();
}
}
⭕ With Dependency Injection:
@Service
public class OrderService {
private EmailService emailService;
// Spring will inject EmailService for us
public OrderService(EmailService emailService) {
this.emailService = emailService;
}
}
1. Manual Injection
Manual injection is when you explicitly configure how dependencies should be wired, usually through Java configuration classes.
Example: Manual Injection with @Configuration
@Configuration
public class AppConfig {
@Bean
public EmailService emailService() {
return new EmailService("smtp.gmail.com", 587);
}
@Bean
public OrderService orderService() {
// Manually injecting EmailService into OrderService
return new OrderService(emailService());
}
@Bean
public PaymentService paymentService() {
// Manually wiring multiple dependencies
return new PaymentService(emailService(), orderService());
}
}
When to use Manual Injection:
Real-world Example: Database Configuration
@Configuration
public class DatabaseConfig {
@Bean
@Primary
public DataSource primaryDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/primary_db");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(20);
return new HikariDataSource(config);
}
@Bean
public JdbcTemplate jdbcTemplate() {
// Manual injection of DataSource into JdbcTemplate
return new JdbcTemplate(primaryDataSource());
}
}
2. Automatic Injection (Field and Setter Injection)
Automatic injection lets Spring automatically wire dependencies using annotations. There are two main types:
Field Injection with @Autowired
@Service
public class OrderService {
@Autowired
private EmailService emailService;
@Autowired
private PaymentService paymentService;
@Autowired
private InventoryService inventoryService;
public void processOrder(Order order) {
inventoryService.checkStock(order);
paymentService.processPayment(order);
emailService.sendConfirmation(order);
}
}
⭕ Setter Injection with @Autowired
@Service
public class UserService {
private EmailService emailService;
private DatabaseService databaseService;
@Autowired
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
@Autowired
public void setDatabaseService(DatabaseService databaseService) {
this.databaseService = databaseService;
}
public void createUser(User user) {
databaseService.save(user);
emailService.sendWelcomeEmail(user.getEmail());
}
}
⭕ Advanced Automatic Injection Features
Handling Optional Dependencies:
@Service
public class NotificationService {
@Autowired(required = false)
private SMSService smsService; // May be null if not available
@Autowired
private EmailService emailService; // Required dependency
public void notifyUser(String message, User user) {
// Always send email
emailService.send(user.getEmail(), message);
// Send SMS only if service is available
if (smsService != null) {
smsService.send(user.getPhone(), message);
}
}
}
⭕ Qualifying Beans:
@Service
public class PaymentService {
@Autowired
@Qualifier("creditCardProcessor")
private PaymentProcessor creditCardProcessor;
@Autowired
@Qualifier("paypalProcessor")
private PaymentProcessor paypalProcessor;
public void processPayment(Payment payment) {
if (payment.getType() == PaymentType.CREDIT_CARD) {
creditCardProcessor.process(payment);
} else if (payment.getType() == PaymentType.PAYPAL) {
paypalProcessor.process(payment);
}
}
}
3. Constructor Injection (Recommended Approach)
Constructor injection is considered the best practice in modern Spring applications. It makes dependencies explicit and enables immutable objects.
Recommended by LinkedIn
⭕ Basic Constructor Injection
@Service
public class OrderService {
private final EmailService emailService;
private final PaymentService paymentService;
private final InventoryService inventoryService;
// Spring automatically injects dependencies through constructor
public OrderService(EmailService emailService,
PaymentService paymentService,
InventoryService inventoryService) {
this.emailService = emailService;
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
public void processOrder(Order order) {
inventoryService.reserveItems(order);
paymentService.charge(order.getTotal());
emailService.sendOrderConfirmation(order);
}
}
⭕ Lombok Constructor Injection (Even Cleaner)
@Service
@RequiredArgsConstructor // Lombok generates constructor for final fields
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
private final PasswordEncoder passwordEncoder;
public User createUser(UserRequest request) {
User user = User.builder()
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.name(request.getName())
.build();
User savedUser = userRepository.save(user);
emailService.sendWelcomeEmail(savedUser.getEmail());
return savedUser;
}
}
Real-world Constructor Injection Example
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final OrderValidator orderValidator;
private final OrderMapper orderMapper;
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
// Validate the order
orderValidator.validate(request);
// Convert request to domain object
Order order = orderMapper.toOrder(request);
// Process the order
Order processedOrder = orderService.processOrder(order);
// Convert to response
OrderResponse response = orderMapper.toResponse(processedOrder);
return ResponseEntity.ok(response);
}
}
Why Constructor Injection is Preferred
1. Immutability
@Service
public class BankingService {
private final SecurityService securityService; // final = immutable
private final AuditService auditService; // Cannot be changed after creation
public BankingService(SecurityService securityService, AuditService auditService) {
this.securityService = securityService;
this.auditService = auditService;
}
}
2. Fail-Fast Behavior
If dependencies are missing, the application fails to start rather than failing at runtime.
3. Easier Testing
@Test
public void testOrderProcessing() {
// Easy to mock dependencies in constructor
EmailService mockEmailService = mock(EmailService.class);
PaymentService mockPaymentService = mock(PaymentService.class);
OrderService orderService = new OrderService(mockEmailService, mockPaymentService);
// Test your logic
Order result = orderService.processOrder(testOrder);
verify(mockEmailService).sendConfirmation(testOrder);
verify(mockPaymentService).processPayment(testOrder);
}
Circular Dependencies and Solutions
Sometimes you might encounter circular dependencies. Here's how to handle them:
Problem Example:
@Service
public class OrderService {
private final InventoryService inventoryService;
public OrderService(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
}
@Service
public class InventoryService {
private final OrderService orderService; // Circular dependency!
public InventoryService(OrderService orderService) {
this.orderService = orderService;
}
}
Solution 1: Redesign (Recommended)
// Extract common logic to a new service
@Service
public class BusinessLogicService {
// Common operations
}
@Service
public class OrderService {
private final BusinessLogicService businessLogicService;
// No direct dependency on InventoryService
}
@Service
public class InventoryService {
private final BusinessLogicService businessLogicService;
// No direct dependency on OrderService
}
Solution 2: @Lazy Annotation
@Service
public class OrderService {
private final InventoryService inventoryService;
public OrderService(@Lazy InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
}
✅ Best Practices Summary
🔄 When to Use Which?
Conclusion
Dependency injection is what makes Spring Boot applications modular, testable, and maintainable. While Spring offers multiple injection methods, constructor injection should be your go-to choice for most scenarios. It provides the clearest contract, enables immutable objects, and makes your code easier to test.
#SpringBoot #Java #DependencyInjection #SoftwareDevelopment #Programming
Thanks for sharing, Yashith
Thanks for sharing, Brother ❤️
Thanks for sharing, Yashith