Dependency Injection in Java: In Practice
Dependency Injection (DI) is one of the key principles of modern Java development, underlying frameworks like Spring and Jakarta EE. In this article, we will break down what dependency injection is, why it is needed, and, most importantly, how to apply it in practice with code examples.
What is Dependency Injection and Why is it Important?
Dependency Injection is a design pattern where an object receives its dependencies from the outside, rather than creating them itself. This implements the principle of Inversion of Control (IoC). Instead of a class managing its own dependencies, control is transferred to an external container or factory.
Imagine you are writing an application for sending notifications. Without DI, your code might look like this:
public class NotificationService { private EmailSender emailSender = new EmailSender();
public void send(String message) { emailSender.send(message); }}The problem is obvious: NotificationService is tightly coupled to EmailSender. If you want to add SMS or Push notifications, you would have to rewrite the entire class. With Dependency Injection, this problem is solved:
public class NotificationService { private final MessageSender messageSender;
// Dependency is passed through the constructor public NotificationService(MessageSender messageSender) { this.messageSender = messageSender; }
public void send(String message) { messageSender.send(message); }}Now the class depends on the abstraction MessageSender, not on a specific implementation. This makes the code flexible, testable, and scalable.
Ways to Inject Dependencies in Java
There are three main ways to implement DI in Java. Let's look at each with examples.
1. Constructor Injection
This is the most preferred method. Dependencies are passed through constructor parameters and are usually marked as final.
public class UserService { private final UserRepository userRepository; private final EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) { this.userRepository = userRepository; this.emailService = emailService; }
public void registerUser(User user) { userRepository.save(user); emailService.sendWelcomeEmail(user.getEmail()); }}Advantages:
- Mandatory dependencies are clearly visible
- The object is always in a valid state
- Excellent for immutable objects
- Easy to test — just pass mock objects
2. Setter Injection
Dependencies are set via setter methods after the object is created.
public class OrderService { private PaymentGateway paymentGateway;
public void setPaymentGateway(PaymentGateway paymentGateway) { this.paymentGateway = paymentGateway; }
public void processOrder(Order order) { paymentGateway.charge(order.getTotal()); }}When to use: For optional dependencies or when dependencies may change during the object's lifetime. However, this method makes the object mutable and less predictable.
3. Field Injection
Most often used with frameworks via annotations, for example @Autowired in Spring.
public class ReportService { @Autowired private DataSource dataSource;
public Report generateReport() { return dataSource