Dependency Injection (DI)
In the previous page, we changed OrderService so it no longer creates its own NotificationService. Instead, someone gives it one through the constructor. We briefly named this idea Dependency Injection. Now let's understand it properly, along with the different ways to do it.
What is Dependency Injection?
Dependency Injection (DI) means a class receives the objects it needs from outside, instead of creating them itself.
"Inject" simply means "give" or "pass in".
Here's the difference in one picture:
1Without DI: OrderService → creates its own → EmailService2With DI: Someone else → creates → EmailNotificationService3 → gives it to → OrderServiceIn code, the difference is just where new happens:
// Without DI: the class creates its dependencypublic class OrderService {private EmailService emailService = new EmailService();}
// With DI: the class receives its dependencypublic class OrderService {private final NotificationService notificationService;public OrderService(NotificationService notificationService) {this.notificationService = notificationService;}}
A simple way to remember it:
A class should ask for what it needs, not make what it needs.
Think of a chef in a restaurant. The chef doesn't grow vegetables or raise chickens. The ingredients are delivered to the kitchen, and the chef focuses on cooking. OrderService is the chef: it focuses on placing orders, and its tools are delivered to it.
But why is "asking" better than "making"? Let's look closer.
Why should a class not create its own dependencies?
We saw some problems in the previous page. Here they all are, plus one big new one.
1. It's locked to one exact class.
If OrderService writes new EmailService(), it can only ever use email. Switching to SMS means editing OrderService.
2. It can't be tested on its own.
Every test of placeOrder() would send a real email, because the class always creates the real thing.
3. It has to know how to build its dependencies.
This is the new problem, and it grows fast. In a real app, EmailNotificationService can't send emails alone. It needs a mail client, and the mail client needs settings:
public class OrderService {private EmailNotificationService notificationService =new EmailNotificationService(new SmtpClient(new MailConfig("smtp.example.com", 587, "user", "password")));}
Now OrderService, a class that should only place orders, knows the mail server's address, port, and password. If the mail settings change, we edit OrderService. That breaks the Single Responsibility Principle from the previous page.
4. Objects can't be shared.
If ten classes each create their own SmtpClient, the app opens ten separate mail connections, when one shared connection would do.
With DI, all of these go away. OrderService just says "I need a NotificationService", and whoever creates OrderService handles which one, how to build it, and whether to share it.
There are three ways to give a class its dependencies. Let's look at each.
Constructor injection
The dependency is passed in through the constructor. This is what we've been doing so far.
public class OrderService {private final NotificationService notificationService;public OrderService(NotificationService notificationService) {this.notificationService = notificationService;}public void placeOrder(String customer, String product) {System.out.println("Order placed: " + product);notificationService.send(customer, "Your order for " + product + " is confirmed");}}
OrderService orderService = new OrderService(new EmailNotificationService());orderService.placeOrder("asha@example.com", "Laptop");
Why it's the best choice in most cases:
You can't forget it. Java won't let you create an OrderService without passing a NotificationService. The object is complete and ready to use from the moment it's created.
The field can be final. Once set, it can never be changed or accidentally set to null later.
The needs are visible. Just by reading the constructor, you know exactly what this class depends on.
It works without any framework. It's plain Java, which makes testing easy, as we'll see later in this page.
If a class needs more than one dependency, the constructor simply takes more parameters:
public OrderService(NotificationService notificationService,PaymentService paymentService) {this.notificationService = notificationService;this.paymentService = paymentService;}
Tip: if a constructor needs many parameters (say, six or seven), it's often a sign the class is doing too many jobs, and should be split.
Setter injection
The dependency is passed in through a setter method, after the object has been created.
public class OrderService {private NotificationService notificationService;public void setNotificationService(NotificationService notificationService) {this.notificationService = notificationService;}public void placeOrder(String customer, String product) {System.out.println("Order placed: " + product);notificationService.send(customer, "Your order for " + product + " is confirmed");}}
OrderService orderService = new OrderService();orderService.setNotificationService(new EmailNotificationService());orderService.placeOrder("asha@example.com", "Laptop");
This works, but there's a risk. Nothing forces you to call the setter:
OrderService orderService = new OrderService();orderService.placeOrder("asha@example.com", "Laptop"); // NullPointerException!
The object was created in an incomplete state, and the mistake only shows up when the code runs.
Other downsides:
- The field can't be
final, because it's set after the constructor runs. - Anyone can call the setter again later and swap the dependency in the middle of the program.
When is setter injection useful? For optional dependencies: things the class can work without. For example, an optional DiscountService. If it's set, apply discounts; if not, skip them. The class must handle the null case itself.
Field injection
The dependency is put directly into the field, without a constructor or setter.
In plain Java, there's no clean way to do this for a private field. Field injection is something frameworks like Spring do. Here's how it looks in Spring (we'll learn these annotations properly in the next pages):
@Servicepublic class OrderService {@Autowiredprivate NotificationService notificationService;public void placeOrder(String customer, String product) {System.out.println("Order placed: " + product);notificationService.send(customer, "Your order for " + product + " is confirmed");}}
@Autowired tells Spring: "find a NotificationService and put it into this field". Spring does it behind the scenes, even though the field is private.
It looks clean and short, which is why many tutorials use it. But it has real problems:
It can't be tested without Spring. There's no constructor or setter to pass in a fake. In a plain unit test, the field stays null:
OrderService orderService = new OrderService();orderService.placeOrder("test@example.com", "Book"); // NullPointerException!
The needs are hidden. You can't see what the class depends on without reading every field.
The field can't be final.
It's too easy to add more. Adding a dependency is just one more line, so classes quietly grow until they have too many jobs, and nothing makes it obvious.
Here are all three side by side:
| Constructor | Setter | Field | |
|---|---|---|---|
| How it's given | Through the constructor | Through a setter method | Directly into the field (framework only) |
| Can you forget it? | No | Yes | Yes (without the framework) |
Field can be final | Yes | No | No |
| Dependencies visible | Yes, in the constructor | Partly, in setters | No |
| Easy unit testing | Yes | Yes | No |
| Best for | Required dependencies (most cases) | Optional dependencies | Avoid in new code |
Use constructor injection by default. The Spring team itself recommends it.
Constructor injection with Lombok
Writing constructors by hand gets repetitive when a class has several dependencies. Lombok can write the constructor for you.
@RequiredArgsConstructorpublic class OrderService {private final NotificationService notificationService;private final PaymentService paymentService;public void placeOrder(String customer, String product) {System.out.println("Order placed: " + product);notificationService.send(customer, "Your order for " + product + " is confirmed");}}
@RequiredArgsConstructor creates a constructor with one parameter for every final field. While compiling, Lombok turns the class above into this:
public OrderService(NotificationService notificationService,PaymentService paymentService) {this.notificationService = notificationService;this.paymentService = paymentService;}
Lombok doesn't inject anything. It only writes the constructor. This is still normal constructor injection, with all its benefits: final fields, dependencies you can't forget, and easy testing. You just type less.
This also explains why Lombok uses the provided scope from the Maven pages: it does its work while compiling, and isn't needed when the app runs.
Now let's see why all this effort pays off.
Benefits: flexibility, maintainability and unit testing
Dependency Injection, especially constructor injection, gives us three big benefits. We've already seen hints of each; here they are together.
Flexibility
You can change which implementation a class uses without touching the class.
new OrderService(new EmailNotificationService());new OrderService(new SmsNotificationService());new OrderService(new WhatsAppNotificationService());
Same OrderService, three different behaviors. This is the Open/Closed Principle from the previous page in action.
Maintainability
Each class does one job and knows only what it needs.
OrderServiceknows nothing about mail servers, ports, or passwords.- If the mail settings change, only the email classes change.
- If order rules change, only
OrderServicechanges.
Changes stay small and in one place, which makes the code easier to understand, fix, and grow. This is the Single Responsibility Principle from the previous page.
Unit testing
A unit test checks one class on its own, without real emails, real databases, or real networks.
Because OrderService receives its dependency, we can give it a fake one in tests:
public class FakeNotificationService implements NotificationService {String lastTo;String lastMessage;@Overridepublic void send(String to, String message) {lastTo = to;lastMessage = message; // no real email or SMS}}
import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.assertEquals;class OrderServiceTest {@Testvoid placeOrderSendsConfirmation() {FakeNotificationService fake = new FakeNotificationService();OrderService orderService = new OrderService(fake);orderService.placeOrder("test@example.com", "Book");assertEquals("test@example.com", fake.lastTo);assertEquals("Your order for Book is confirmed", fake.lastMessage);}}
This test:
- runs in milliseconds,
- sends no real messages,
- needs no internet or mail server,
- and checks exactly one thing: does
OrderServicesend the right confirmation?
None of this would be possible if OrderService created its own EmailService.
(In real projects, libraries like Mockito can create these fakes for you, so you don't have to write a fake class by hand.)
Putting it together
1Dependency Injection → a class receives what it needs instead of creating it2Constructor injection → required, visible, final, easy to test (use by default)3Setter injection → for optional dependencies only4Field injection → hidden and hard to test (avoid in new code)5Flexibility → swap implementations without editing the class6Maintainability → each class does one job and changes in one place7Unit testing → pass in fakes and test each class on its ownOne question remains. With DI, every class asks for its dependencies. But someone still has to create all these objects and connect them, in the right order:
MailConfig config = new MailConfig("smtp.example.com", 587, "user", "password");SmtpClient smtpClient = new SmtpClient(config);NotificationService notificationService = new EmailNotificationService(smtpClient);PaymentService paymentService = new PaymentService();OrderService orderService = new OrderService(notificationService, paymentService);
In a real application, this list could be hundreds of lines long. Spring can do all of this for you, which is exactly what we'll see next.