Dependency Injection : SpringBoot

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:

  • When you need complex initialization logic
  • For third-party libraries that aren't Spring-managed
  • When you need conditional bean creation
  • For legacy code integration

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.

⭕ 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

  1. Prefer Constructor Injection - It's the most robust and testable approach
  2. Use @RequiredArgsConstructor with Lombok for cleaner code
  3. Avoid Field Injection - It makes testing harder and hides dependencies
  4. Keep constructors simple - Don't put business logic in constructors
  5. Use @Qualifier when you have multiple beans of the same type
  6. Avoid circular dependencies through better design

🔄 When to Use Which?

  • Manual Injection → Tiny apps, quick demos, or when not using Spring.
  • Automatic Injection → When you want Spring to manage dependencies easily.
  • Constructor Injection → Preferred in professional Spring Boot projects.

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

Like
Reply

Thanks for sharing, Brother ❤️

Like
Reply

Thanks for sharing, Yashith

Like
Reply

To view or add a comment, sign in

More articles by Yashith Prabhashwara

Others also viewed

Explore content categories