Dependency Injection
Imagine a car that needs an engine to operate. If the car creates and installs its own engine internally every time it is constructed, changing the engine becomes difficult. But if the engine is supplied from outside, the same car can work with different engine implementations.
This simple idea is at the heart of Dependency Injection. Instead of a class creating the objects it depends on, those dependencies are provided to it from outside.
What Is Dependency Injection?
Dependency Injection (DI) is a design technique in which an object's required dependencies are supplied to it from an external source instead of being created internally by the object itself.
A dependency is simply another object that a class needs to perform its work.
For example, consider:
class OrderService {
private PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
Here, OrderService depends on PaymentService. The dependency is supplied through the constructor rather than created inside OrderService.
The central idea is simple: a class should receive what it needs instead of constructing everything it needs by itself.
What Is a Dependency?
Suppose we have an order-processing class:
class OrderService {
private final PaymentService paymentService;
private final EmailService emailService;
}
The OrderService cannot perform all of its responsibilities without these services.
Therefore:
- OrderService is the dependent object.
- PaymentService is a dependency.
- EmailService is another dependency.
Dependency Injection is the technique used to provide those dependencies from outside.
The Problem Without Dependency Injection
Consider this design:
class OrderService {
private PaymentService paymentService =
new PaymentService();
public void placeOrder(double amount) {
paymentService.pay(amount);
}
}
At first glance, this looks straightforward. But OrderService is now responsible for creating its own dependency.
Suppose we later want to use a different payment implementation:
- Credit card payment.
- UPI payment.
- Mock payment for testing.
- A third-party payment gateway.
The class becomes tightly coupled to the concrete implementation.
Dependency Injection Solves the Coupling Problem
Instead of creating the dependency internally, define an abstraction:
interface PaymentService {
void pay(double amount);
}
Create implementations:
class UpiPaymentService implements PaymentService {
@Override
public void pay(double amount) {
System.out.println(
"Payment made using UPI: ₹" + amount);
}
}
class CardPaymentService implements PaymentService {
@Override
public void pay(double amount) {
System.out.println(
"Payment made using Card: ₹" + amount);
}
}
Now inject the dependency:
class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void placeOrder(double amount) {
paymentService.pay(amount);
}
}
The client decides which implementation should be provided:
PaymentService payment =
new UpiPaymentService();
OrderService orderService =
new OrderService(payment);
orderService.placeOrder(2500);
The important change is architectural: OrderService no longer decides which payment implementation to construct.
A Real-World Analogy
Think of a restaurant kitchen.
A chef needs ingredients to prepare a dish. The chef should not necessarily manufacture the vegetables, grow the spices, build the oven, and produce the cooking oil.
Those resources are supplied to the kitchen.
Similarly, a Java class should focus on its responsibility while required services are supplied to it.
Dependency Injection separates using a dependency from creating a dependency.
Three Common Types of Dependency Injection
Dependency Injection is commonly discussed in three forms:
| Type | How Dependency Is Provided |
|---|---|
| Constructor Injection | Dependency is supplied through the constructor. |
| Setter Injection | Dependency is supplied through a setter method. |
| Field Injection | Dependency is assigned directly to a field, commonly by a framework. |
Constructor Injection
Constructor Injection provides dependencies through the class constructor.
class NotificationService {
private final EmailSender emailSender;
public NotificationService(
EmailSender emailSender) {
this.emailSender = emailSender;
}
public void send(String message) {
emailSender.send(message);
}
}
The dependency is available immediately after object construction.
Usage:
EmailSender sender = new EmailSender();
NotificationService service =
new NotificationService(sender);
service.send("Order confirmed");
Constructor Injection is often preferred because it makes required dependencies explicit.
Why Constructor Injection Is Powerful
Consider a class that cannot function without a particular dependency.
With constructor injection:
class ReportService {
private final ReportRepository repository;
public ReportService(
ReportRepository repository) {
this.repository = repository;
}
}
The class cannot be meaningfully constructed without a repository.
This communicates the class's requirements directly through its API.
It also allows dependencies to be declared final, which helps establish a stable object configuration after construction.
Setter Injection
Setter Injection supplies a dependency through a setter method.
class NotificationService {
private EmailSender emailSender;
public void setEmailSender(
EmailSender emailSender) {
this.emailSender = emailSender;
}
public void send(String message) {
emailSender.send(message);
}
}
Usage:
NotificationService service =
new NotificationService();
service.setEmailSender(
new EmailSender());
service.send("Hello");
Setter injection can be useful when a dependency is optional or when the design intentionally permits replacing a dependency after construction.
Field Injection
Field Injection assigns the dependency directly to a field, typically through reflection or framework support.
A framework-based example may look like:
class OrderController {
@Inject
private OrderService orderService;
}
The framework creates and injects the dependency.
Although field injection can be convenient, it hides required dependencies from the constructor and can make plain unit testing less direct. For required dependencies, constructor injection is generally easier to reason about.
Dependency Injection Without a Framework
A common misconception is that Dependency Injection requires a framework.
It does not.
This is Dependency Injection:
class Engine {
}
class Car {
private final Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
}
public class Main {
public static void main(String[] args) {
Engine engine = new Engine();
Car car = new Car(engine);
}
}
The Engine is created outside Car and passed into it.
No framework is involved.
Dependency Injection is a design technique. A framework can automate it, but the technique itself does not depend on a framework.
Dependency Injection vs Dependency Inversion Principle
These concepts are closely related but they are not identical.
Dependency Inversion Principle (DIP) is a SOLID design principle. It encourages high-level modules to depend on abstractions rather than concrete low-level implementations.
Dependency Injection is a technique for supplying dependencies from outside.
| Dependency Inversion Principle | Dependency Injection |
|---|---|
| Describes a design principle. | Describes a technique for providing dependencies. |
| Encourages dependency on abstractions. | Supplies an implementation from outside. |
| Part of SOLID. | Can be used to implement loosely coupled designs. |
A class can use Dependency Injection while depending on a concrete class, although injecting abstractions often provides greater flexibility.
Dependency Injection and Loose Coupling
Consider this tightly coupled design:
class OrderService {
private final UpiPaymentService payment =
new UpiPaymentService();
}
The class knows the exact implementation it wants.
Now compare it with:
class OrderService {
private final PaymentService payment;
public OrderService(PaymentService payment) {
this.payment = payment;
}
}
The class knows what capability it needs, but not necessarily which implementation provides it.
This reduces coupling and makes the design easier to extend.
Dependency Injection and Testing
One of the biggest practical benefits of Dependency Injection is easier testing.
Suppose a service directly creates a real payment gateway:
class OrderService {
private final PaymentService payment =
new RealPaymentService();
}
A unit test may now accidentally depend on real payment behavior.
With Dependency Injection, a test implementation can be supplied:
class FakePaymentService
implements PaymentService {
@Override
public void pay(double amount) {
System.out.println(
"Fake payment: " + amount);
}
}
The test can then inject it:
PaymentService fake =
new FakePaymentService();
OrderService service =
new OrderService(fake);
service.placeOrder(1000);
This isolates the class under test from external systems.
Dependency Injection and Interfaces
Interfaces are often used with Dependency Injection because they allow different implementations to be supplied.
interface MessageSender {
void send(String message);
}
class EmailSender implements MessageSender {
@Override
public void send(String message) {
System.out.println(
"Sending email: " + message);
}
}
class SmsSender implements MessageSender {
@Override
public void send(String message) {
System.out.println(
"Sending SMS: " + message);
}
}
The consumer depends on the abstraction:
class NotificationService {
private final MessageSender sender;
public NotificationService(
MessageSender sender) {
this.sender = sender;
}
public void notifyUser(String message) {
sender.send(message);
}
}
The same service can work with email or SMS:
NotificationService emailService =
new NotificationService(
new EmailSender());
NotificationService smsService =
new NotificationService(
new SmsSender());
emailService.notifyUser("Welcome");
smsService.notifyUser("OTP sent");
Composition Root
A useful architectural idea related to Dependency Injection is the Composition Root.
The Composition Root is the place where application components are assembled and their dependencies are connected.
For example:
MessageSender sender =
new EmailSender();
NotificationService notificationService =
new NotificationService(sender);
The application startup layer can decide which implementation to use, while the business class remains focused on its actual responsibility.
In larger applications, a Dependency Injection container often automates much of this assembly process.
Dependency Injection in Large Applications
In enterprise applications, a class may depend on several services:
class OrderService {
private final OrderRepository repository;
private final PaymentService paymentService;
private final NotificationService notificationService;
public OrderService(
OrderRepository repository,
PaymentService paymentService,
NotificationService notificationService) {
this.repository = repository;
this.paymentService = paymentService;
this.notificationService = notificationService;
}
}
Manually constructing an entire object graph can eventually become repetitive.
A Dependency Injection container can create objects and resolve their dependencies automatically.
The important distinction is that the framework automates the wiring; the underlying design idea remains Dependency Injection.
Dependency Injection and the Dependency Graph
Applications can be viewed as dependency graphs.
For example:
OrderController
|
v
OrderService
|
+------> OrderRepository
|
+------> PaymentService
|
+------> NotificationService
Dependency Injection provides a systematic way to construct and connect this graph.
Instead of each class constructing its children, an external composition mechanism can assemble the complete object graph.
Common Mistakes Beginners Make
- Confusing DI with a framework: Dependency Injection is a design technique and does not require a framework.
- Injecting everything: Not every object needs to be injected. Simple value objects and straightforward internal objects may be created normally.
- Using field injection everywhere: Hidden dependencies can make classes harder to understand and test.
- Creating interfaces without a real abstraction: An interface should represent a meaningful variation or contract rather than exist merely for the sake of abstraction.
- Using a service locator as a substitute for DI: Pulling dependencies from a global registry can hide dependencies instead of making them explicit.
- Creating huge constructors: A constructor with many dependencies can indicate that the class has too many responsibilities.
The Constructor Smell
Dependency Injection can reveal design problems rather than create them.
Suppose a class requires twelve dependencies:
public OrderService(
A a,
B b,
C c,
D d,
E e,
F f,
G g,
H h,
I i,
J j,
K k,
L l) {
}
The problem may not be the constructor itself. The class may simply be doing too much.
A large number of required dependencies can be a useful design signal that the class should be divided into smaller, more focused components.
Dependency Injection and Immutability
Constructor Injection works naturally with immutable object design.
class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
}
Once constructed, the dependency reference does not need to change.
This can make the object's lifecycle easier to reason about and reduces accidental reassignment.
Dependency Injection and Strategy Pattern
Dependency Injection and Strategy Pattern work very well together.
The Strategy Pattern defines interchangeable behavior:
interface DiscountStrategy {
double calculate(double amount);
}
Dependency Injection can provide the required strategy:
class OrderService {
private final DiscountStrategy strategy;
public OrderService(
DiscountStrategy strategy) {
this.strategy = strategy;
}
}
This separates two concerns:
- Strategy Pattern: defines interchangeable behavior.
- Dependency Injection: supplies the selected behavior.
Dependency Injection vs Factory Pattern
Factory and Dependency Injection can both participate in object creation, but their responsibilities differ.
| Dependency Injection | Factory |
|---|---|
| Supplies an existing or constructed dependency to a consumer. | Encapsulates object creation. |
| Focuses on separating dependency usage from dependency construction. | Focuses on deciding how and which object is created. |
| Often works well with constructor injection. | Can be used to choose among concrete implementations. |
A Factory can even be used inside a Composition Root to create the dependency that is subsequently injected.
Advantages of Dependency Injection
- Loose coupling: Classes can depend on abstractions instead of concrete implementations.
- Better testability: Test doubles can be supplied easily.
- Explicit dependencies: Constructor parameters clearly communicate required collaborators.
- Improved flexibility: Implementations can be replaced without modifying the dependent class.
- Better separation of concerns: Business classes can focus on business logic instead of object construction.
- Supports extensibility: New implementations can often be introduced with minimal changes to existing consumers.
Disadvantages and Trade-Offs
- The number of classes and configuration points may increase.
- Object construction can become harder to follow in very large applications.
- A DI framework introduces additional concepts and configuration.
- Poorly designed dependency graphs can become difficult to understand.
- Overusing abstractions can make simple code unnecessarily complicated.
Good Dependency Injection is not about injecting everything. It is about creating clear boundaries between components.
Best Practices
- Prefer constructor injection for required dependencies.
- Use final fields for dependencies that should not change after construction.
- Depend on abstractions when meaningful variation exists.
- Keep the Composition Root close to the application's startup or assembly boundary.
- Keep business classes independent of DI framework details when practical.
- Use setter injection primarily for genuinely optional or replaceable dependencies.
- Avoid service locators that hide dependencies.
- Watch for constructors with excessive dependencies.
- Do not introduce interfaces without a meaningful reason.
- Use Dependency Injection to improve architecture, not merely to follow a framework convention.
A Complete Practical Example
Let us build a small order-processing example using constructor injection.
interface PaymentGateway {
void pay(double amount);
}
class UpiPaymentGateway
implements PaymentGateway {
@Override
public void pay(double amount) {
System.out.println(
"Processing UPI payment: ₹" + amount);
}
}
interface OrderRepository {
void save(String orderId);
}
class DatabaseOrderRepository
implements OrderRepository {
@Override
public void save(String orderId) {
System.out.println(
"Saving order: " + orderId);
}
}
Now inject both dependencies:
class OrderService {
private final PaymentGateway paymentGateway;
private final OrderRepository orderRepository;
public OrderService(
PaymentGateway paymentGateway,
OrderRepository orderRepository) {
this.paymentGateway = paymentGateway;
this.orderRepository = orderRepository;
}
public void placeOrder(
String orderId,
double amount) {
paymentGateway.pay(amount);
orderRepository.save(orderId);
System.out.println(
"Order placed successfully");
}
}
The application assembles the dependencies:
public class Main {
public static void main(String[] args) {
PaymentGateway paymentGateway =
new UpiPaymentGateway();
OrderRepository repository =
new DatabaseOrderRepository();
OrderService service =
new OrderService(
paymentGateway,
repository);
service.placeOrder("ORD-101", 2500);
}
}
The result is a clean separation:
| Component | Responsibility |
|---|---|
| PaymentGateway | Defines payment behavior. |
| UpiPaymentGateway | Implements UPI payment. |
| OrderRepository | Defines order persistence behavior. |
| DatabaseOrderRepository | Implements database persistence. |
| OrderService | Coordinates order processing. |
| Main | Assembles the object graph. |
Testing the Example
Because the dependencies are injected, we can provide simple test implementations.
class FakePaymentGateway
implements PaymentGateway {
@Override
public void pay(double amount) {
System.out.println(
"Fake payment: " + amount);
}
}
class FakeOrderRepository
implements OrderRepository {
@Override
public void save(String orderId) {
System.out.println(
"Fake save: " + orderId);
}
}
The service can now be tested without connecting to an actual payment system or database:
PaymentGateway payment =
new FakePaymentGateway();
OrderRepository repository =
new FakeOrderRepository();
OrderService service =
new OrderService(payment, repository);
service.placeOrder("TEST-001", 500);
This is one of the strongest practical reasons to learn Dependency Injection: dependencies become replaceable collaborators rather than hard-coded implementation details.
Learning Checkpoint
Ask yourself:
- What dependencies does this class actually require?
- Are those dependencies created inside the class?
- Could they instead be supplied from outside?
- Would constructor injection make the dependency explicit?
- Can a test replace the real dependency with a fake implementation?
- Does the class have so many dependencies that it may be doing too much?
Interview Insights
Question 1: What is Dependency Injection?
Dependency Injection is a technique in which an object's dependencies are supplied from outside rather than being created internally by the object.
Question 2: What are the common types of Dependency Injection?
The commonly discussed types are Constructor Injection, Setter Injection, and Field Injection.
Question 3: Which type of Dependency Injection is generally preferred for required dependencies?
Constructor Injection is generally preferred because required dependencies are explicit and can be established when the object is created.
Question 4: Does Dependency Injection require Spring or another framework?
No. Dependency Injection is a design technique that can be implemented with ordinary Java code. Frameworks can automate dependency creation and wiring.
Question 5: What is the difference between DI and DIP?
DIP is a SOLID design principle about depending on abstractions and inverting dependency direction. DI is a technique for supplying dependencies from outside.
Question 6: How does Dependency Injection improve unit testing?
It allows real dependencies to be replaced with test doubles, such as fakes or mocks, so the class can be tested in isolation.
Question 7: What is a Composition Root?
It is the part of an application where components are assembled and their dependencies are connected.
Question 8: Is creating an object with new always bad?
No. The issue is not the new keyword itself. The design problem occurs when a class unnecessarily controls the construction of replaceable dependencies that should be supplied externally.
Quick Revision
| Concept | Key Idea |
|---|---|
| Dependency | An object required by another object to perform its work. |
| Dependency Injection | Supplies dependencies from outside the dependent class. |
| Constructor Injection | Provides dependencies through the constructor. |
| Setter Injection | Provides dependencies through setter methods. |
| Field Injection | Assigns dependencies directly to fields, commonly through framework support. |
| DIP | A SOLID principle encouraging appropriate dependency on abstractions. |
| Composition Root | Central location where application components are assembled. |
| Main Benefit | Reduces coupling and makes dependencies explicit and replaceable. |
Final Takeaway
Dependency Injection is one of the most practical techniques for building maintainable Java applications. It encourages a class to focus on its own responsibility instead of taking responsibility for constructing every object it needs.
Constructor Injection makes required dependencies visible, interfaces can provide replaceable implementations, and tests can supply lightweight alternatives.
As applications grow, Dependency Injection also provides a foundation for assembling large object graphs without forcing business classes to manage their own dependencies.
Remember the core idea: a class should receive its collaborators rather than being tightly responsible for creating them.
Chapter 40 Complete
With Dependency Injection, all the given topics in Chapter 40: Design Patterns have now been completed.
