Dependency Injection (DI) is a design pattern where an object receives its dependencies from an external source instead of creating them itself. It separates dependency creation from usage, improving flexibility, testability, and maintainability.
- Reduces tight coupling between classes while improving code reusability and flexibility.
- Makes unit testing easier using mock dependencies and enhances system maintainability and scalability.
Example:
Carclass might depend on aEngineclass to run. Without DI, theCarclass would directly create or manage theEngineinstance within its code, which makes the two classes tightly coupled. This approach can create problems, particularly when you need to test, extend, or modify the classes in the future.

- Dependency Injection solves this problem by injecting the dependencies (like the
Engineones in theCarexample) into the class from an external source, rather than having the class create them. - In simpler terms, DI allows you to "inject" the things a class needs (its dependencies) from the outside, instead of letting the class create or manage them itself.
Four Roles of Dependency Injection
In Dependency Injection, the dependencies of a class are injected from the outside, rather than the class creating or managing its dependencies internally. This pattern has four main roles:

1. Client
The client is the class or component that depends on a service to perform its operations.
- It does not create or manage its dependencies
- It receives required services from the injector
2. Service
The service is the class or component that provides specific functionality needed by the client.
- It contains the actual business logic
- It is designed to be independent of the client
3. Injector
The injector is responsible for creating service instances and supplying them to the client.
- It manages dependency creation and lifecycle
- It injects required services at runtime
4. Interface
The interface defines a contract that specifies what methods a service must implement.
- Clients depend on interfaces, not concrete classes
- It allows easy replacement of service implementations
Uses
Dependency Injection is widely used to build flexible, maintainable, and loosely coupled applications.
- Loose Coupling & Reusability: Objects don’t create their own dependencies, making them independent and reusable.
- Improved Testability: Easily inject mocks or test doubles to test components in isolation.
- Maintainability & Flexibility: DI frameworks simplify managing and configuring dependencies.
- Scalability & Extensibility: Helps handle complex dependency graphs in large applications.
- Cross-Cutting Concerns: Conveniently inject services like logging, security, or caching across multiple components.
Example
Imagine you're building an application that sends notifications to users. You want to make the notification system flexible so you can change the notification provider (email, SMS, push notifications, etc.) without modifying the core application logic.
1. Code Without Dependency Injection:
#include <string>
#include "EmailProvider.h"
class NotificationService {
private:
EmailProvider emailProvider;
public:
void sendNotification(const std::string& message, const std::string& recipient) {
emailProvider.sendEmail(message, recipient);
}
};
public class NotificationService {
private EmailProvider emailProvider = new EmailProvider(); // Tightly coupled to email
public void sendNotification(String message, String recipient) {
emailProvider.sendEmail(message, recipient);
}
}
from EmailProvider import EmailProvider
class NotificationService:
def __init__(self):
self.email_provider = EmailProvider()
def send_notification(self, message, recipient):
self.email_provider.send_email(message, recipient)
class EmailProvider {
sendEmail(message, recipient) {
// Implementation for sending email
}
}
class NotificationService {
constructor() {
this.emailProvider = new EmailProvider();
}
sendNotification(message, recipient) {
this.emailProvider.sendEmail(message, recipient);
}
}
Issues:
- Tight Coupling: The
NotificationServiceis tightly coupled to theEmailProvider, making it difficult to switch to a different provider without code changes. - Testability: Testing
NotificationServicein isolation is challenging as it directly usesEmailProvider.
2. Code With Dependency Injection:
// Interface for different notification providers
#include <iostream>
#include <string>
class NotificationProvider {
public:
virtual void sendNotification(const std::string& message, const std::string& recipient) = 0;
};
// Concrete implementation: Email
class EmailProvider : public NotificationProvider {
public:
void sendNotification(const std::string& message, const std::string& recipient) override {
std::cout << "Sending Email to " << recipient << ": " << message << std::endl;
}
};
// Concrete implementation: SMS
class SMSProvider : public NotificationProvider {
public:
void sendNotification(const std::string& message, const std::string& recipient) override {
std::cout << "Sending SMS to " << recipient << ": " << message << std::endl;
}
};
// Notification service that uses dependency injection
class NotificationService {
private:
NotificationProvider* notificationProvider;
public:
NotificationService(NotificationProvider* notificationProvider) : notificationProvider(notificationProvider) {}
void sendNotification(const std::string& message, const std::string& recipient) {
notificationProvider->sendNotification(message, recipient);
}
};
// Main function
int main() {
// Inject EmailProvider
EmailProvider emailProvider;
NotificationService emailService(&emailProvider);
emailService.sendNotification("Hello via Email!", "abc@example.com");
// Inject SMSProvider
SMSProvider smsProvider;
NotificationService smsService(&smsProvider);
smsService.sendNotification("Hello via SMS!", "123-456-7890");
return 0;
}
// Interface for different notification providers
interface NotificationProvider {
void sendNotification(String message, String recipient);
}
// Concrete implementation: Email
class EmailProvider implements NotificationProvider {
@Override
public void sendNotification(String message, String recipient) {
System.out.println("Sending Email to " + recipient + ": " + message);
}
}
// Concrete implementation: SMS
class SMSProvider implements NotificationProvider {
@Override
public void sendNotification(String message, String recipient) {
System.out.println("Sending SMS to " + recipient + ": " + message);
}
}
// Notification service that uses dependency injection
class NotificationService {
private NotificationProvider notificationProvider;
// Dependency injected via constructor
public NotificationService(NotificationProvider notificationProvider) {
this.notificationProvider = notificationProvider;
}
public void sendNotification(String message, String recipient) {
notificationProvider.sendNotification(message, recipient);
}
}
// Main class
public class GFG {
public static void main(String[] args) {
// Inject EmailProvider
NotificationProvider emailProvider = new EmailProvider();
NotificationService emailService = new NotificationService(emailProvider);
emailService.sendNotification("Hello via Email!", "abc@example.com");
// Inject SMSProvider
NotificationProvider smsProvider = new SMSProvider();
NotificationService smsService = new NotificationService(smsProvider);
smsService.sendNotification("Hello via SMS!", "123-456-7890");
}
}
# Interface for different notification providers
from abc import ABC, abstractmethod
class NotificationProvider(ABC):
@abstractmethod
def sendNotification(self, message: str, recipient: str):
pass
# Concrete implementation: Email
class EmailProvider(NotificationProvider):
def sendNotification(self, message: str, recipient: str):
print(f"Sending Email to {recipient}: {message}")
# Concrete implementation: SMS
class SMSProvider(NotificationProvider):
def sendNotification(self, message: str, recipient: str):
print(f"Sending SMS to {recipient}: {message}")
# Notification service that uses dependency injection
class NotificationService:
def __init__(self, notificationProvider: NotificationProvider):
self.notificationProvider = notificationProvider
def sendNotification(self, message: str, recipient: str):
self.notificationProvider.sendNotification(message, recipient)
# Main function
if __name__ == "__main__":
# Inject EmailProvider
emailProvider = EmailProvider()
emailService = NotificationService(emailProvider)
emailService.sendNotification("Hello via Email!", "abc@example.com")
# Inject SMSProvider
smsProvider = SMSProvider()
smsService = NotificationService(smsProvider)
smsService.sendNotification("Hello via SMS!", "123-456-7890")
// Interface for different notification providers
// Concrete implementation: Email
class EmailProvider {
sendNotification(message, recipient) {
console.log(`Sending Email to ${recipient}: ${message}`);
}
}
// Concrete implementation: SMS
class SMSProvider {
sendNotification(message, recipient) {
console.log(`Sending SMS to ${recipient}: ${message}`);
}
}
// Notification service that uses dependency injection
class NotificationService {
constructor(notificationProvider) {
this.notificationProvider = notificationProvider;
}
sendNotification(message, recipient) {
this.notificationProvider.sendNotification(message, recipient);
}
}
// Main function
(() => {
// Inject EmailProvider
const emailProvider = new EmailProvider();
const emailService = new NotificationService(emailProvider);
emailService.sendNotification("Hello via Email!", "abc@example.com");
// Inject SMSProvider
const smsProvider = new SMSProvider();
const smsService = new NotificationService(smsProvider);
smsService.sendNotification("Hello via SMS!", "123-456-7890");
})();
Output
Sending Email to abc@example.com: Hello via Email! Sending SMS to 123-456-7890: Hello via SMS!
Benefits of using Dependency Injection Design Pattern in this solution above:
- Loose Coupling:
NotificationServiceno longer depends on a specific implementation, making it adaptable to different providers. - Testability: You can easily inject mock providers for testing
NotificationServicein isolation. - Maintainability: Code becomes more modular and easier to manage as dependencies are explicit.
Types of Dependency Injection
There are mainly three types of dependency injection, that are Constructor Injection, Setter Injection and Interface Injection. Let's understand these three approaches to dependency injection using an example with the implementation.
You are building a Vehicle Management System for a car rental service. The system needs to manage cars and their engines. Each car should have an engine type and the system should ensure that the car has all necessary components when it's instantiated.
1. Constructor Injection
With Constructor Injection, dependencies are provided to a class through its constructor when the object is created. This is the most common form of DI because it makes dependencies clear, mandatory, and immutable after the object is constructed.
#include <iostream>
class Engine {
public:
void start() {
std::cout << "Engine started" << std::endl;
}
};
class Car {
private:
Engine engine; // Declaring a dependency on Engine
public:
// Constructor Injection: Dependency is provided through the constructor
Car(Engine engine) : engine(engine) {} // Engine dependency is injected via constructor
void drive() {
engine.start(); // Using the injected Engine dependency
std::cout << "Car is driving" << std::endl;
}
};
int main() {
Engine engine; // Create Engine object (dependency)
// Injecting Engine dependency when creating Car
Car car(engine); // Pass the Engine instance to the constructor
car.drive(); // Call the drive method to use the Engine
return 0;
}
class Engine {
public void start() {
System.out.println("Engine started");
}
}
class Car {
private Engine engine; // Declaring a dependency on Engine
// Constructor Injection: Dependency is provided through the constructor
public Car(Engine engine) {
this.engine = engine; // Engine dependency is injected via constructor
}
public void drive() {
engine.start(); // Using the injected Engine dependency
System.out.println("Car is driving");
}
}
public class Main {
public static void main(String[] args) {
Engine engine = new Engine(); // Create Engine object (dependency)
// Injecting Engine dependency when creating Car
Car car = new Car(engine); // Pass the Engine instance to the constructor
car.drive(); // Call the drive method to use the Engine
}
}
class Engine:
def start(self):
print("Engine started")
class Car:
def __init__(self, engine):
self.engine = engine # Declaring a dependency on Engine
def drive(self):
self.engine.start() # Using the injected Engine dependency
print("Car is driving")
# Main execution
engine = Engine() # Create Engine object (dependency)
# Injecting Engine dependency when creating Car
car = Car(engine) # Pass the Engine instance to the constructor
car.drive() # Call the drive method to use the Engine
class Engine {
start() {
console.log('Engine started');
}
}
class Car {
constructor(engine) {
this.engine = engine; // Declaring a dependency on Engine
}
drive() {
this.engine.start(); // Using the injected Engine dependency
console.log('Car is driving');
}
}
// Main execution
const engine = new Engine(); // Create Engine object (dependency)
// Injecting Engine dependency when creating Car
const car = new Car(engine); // Pass the Engine instance to the constructor
car.drive(); // Call the drive method to use the Engine
Output
Engine started Car is driving
- Engine Class: Has a start() method to simulate starting the engine.
- Car Class: Receives an Engine via its constructor; drive() uses the injected Engine.
- Main Method: Creates an Engine instance, injects it into Car through the constructor, then calls car.drive().
2. Setter Injection
Setter Injection involves providing the dependency via a setter method after the object is created. This approach is more flexible than constructor injection because it allows dependencies to be set or changed after object creation.
#include <iostream>
class Engine {
public:
void start() {
std::cout << "Engine started" << std::endl;
}
};
class Car {
private:
Engine* engine; // Declaring a dependency on Engine
public:
// No constructor injection here. Using setter to inject dependency
void setEngine(Engine* engine) {
this->engine = engine; // Injecting dependency via setter method
}
void drive() {
engine->start(); // Using the injected Engine dependency
std::cout << "Car is driving" << std::endl;
}
};
int main() {
Engine engine; // Create Engine object (dependency)
// Create a Car object without providing the Engine immediately
Car car;
// Inject the Engine dependency using the setter method
car.setEngine(&engine); // Set the dependency via the setter method
car.drive(); // Call the drive method to use the Engine
return 0;
}
class Engine {
public void start() {
System.out.println("Engine started");
}
}
class Car {
private Engine engine; // Declaring a dependency on Engine
// No constructor injection here. Using setter to inject dependency
public void setEngine(Engine engine) {
this.engine = engine; // Injecting dependency via setter method
}
public void drive() {
engine.start(); // Using the injected Engine dependency
System.out.println("Car is driving");
}
}
public class Main {
public static void main(String[] args) {
Engine engine = new Engine(); // Create Engine object (dependency)
// Create a Car object without providing the Engine immediately
Car car = new Car();
// Inject the Engine dependency using the setter method
car.setEngine(engine); // Set the dependency via the setter method
car.drive(); // Call the drive method to use the Engine
}
}
class Engine:
def start(self):
print("Engine started")
class Car:
def __init__(self):
self.engine = None # Declaring a dependency on Engine
# No constructor injection here. Using setter to inject dependency
def set_engine(self, engine):
self.engine = engine # Injecting dependency via setter method
def drive(self):
self.engine.start() # Using the injected Engine dependency
print("Car is driving")
# Create Engine object (dependency)
engine = Engine()
# Create a Car object without providing the Engine immediately
car = Car()
# Inject the Engine dependency using the setter method
car.set_engine(engine) # Set the dependency via the setter method
car.drive() # Call the drive method to use the Engine
class Engine {
start() {
console.log('Engine started');
}
}
class Car {
constructor() {
this.engine = null; // Declaring a dependency on Engine
}
// No constructor injection here. Using setter to inject dependency
setEngine(engine) {
this.engine = engine; // Injecting dependency via setter method
}
drive() {
this.engine.start(); // Using the injected Engine dependency
console.log('Car is driving');
}
}
// Create Engine object (dependency)
const engine = new Engine();
// Create a Car object without providing the Engine immediately
const car = new Car();
// Inject the Engine dependency using the setter method
car.setEngine(engine); // Set the dependency via the setter method
car.drive(); // Call the drive method to use the Engine
Output
Engine started Car is driving
- Engine Class: Has a start() method to simulate engine behavior.
- Car Class: Provides setEngine() to inject the Engine; drive() uses the injected Engine to start the car.
- Main Method: Creates Engine and Car objects, calls setEngine() to inject the Engine, then calls car.drive().
3. Interface Injection
Interface Injection requires the class to implement an interface that provides a method for receiving the dependency. This is less commonly used in Java, but it allows for more flexibility and decoupling.
#include <iostream>
class Engine {
public:
void start() {
std::cout << "Engine started" << std::endl;
}
};
// Define an interface for injecting dependencies
class EngineInjector {
public:
virtual void injectEngine(Engine* engine) = 0; // Method to inject the Engine dependency
};
class Car : public EngineInjector {
private:
Engine* engine; // Declaring a dependency on Engine
public:
// Implement the injectEngine method to set the Engine dependency
void injectEngine(Engine* engine) override {
this->engine = engine; // Dependency injected through the interface method
}
void drive() {
engine->start(); // Using the injected Engine dependency
std::cout << "Car is driving" << std::endl;
}
};
int main() {
Engine engine; // Create Engine object (dependency)
Car car; // Create Car object
car.injectEngine(&engine); // Inject dependency via the injectEngine() method
car.drive(); // Call the drive method to use the Engine
return 0;
}
class Engine {
public void start() {
System.out.println("Engine started");
}
}
// Define an interface for injecting dependencies
interface EngineInjector {
void injectEngine(Engine engine); // Method to inject the Engine dependency
}
class Car implements EngineInjector {
private Engine engine; // Declaring a dependency on Engine
// Implement the injectEngine method to set the Engine dependency
@Override
public void injectEngine(Engine engine) {
this.engine = engine; // Dependency injected through the interface method
}
public void drive() {
engine.start(); // Using the injected Engine dependency
System.out.println("Car is driving");
}
}
public class Main {
public static void main(String[] args) {
Engine engine = new Engine(); // Create Engine object (dependency)
Car car = new Car(); // Create Car object
car.injectEngine(engine); // Inject dependency via the injectEngine() method
car.drive(); // Call the drive method to use the Engine
}
}
class Engine:
def start(self):
print("Engine started")
# Define an interface for injecting dependencies
class EngineInjector:
def inject_engine(self, engine):
pass # Method to inject the Engine dependency
class Car(EngineInjector):
def __init__(self):
self.engine = None # Declaring a dependency on Engine
# Implement the injectEngine method to set the Engine dependency
def inject_engine(self, engine):
self.engine = engine # Dependency injected through the interface method
def drive(self):
self.engine.start() # Using the injected Engine dependency
print("Car is driving")
if __name__ == "__main__":
engine = Engine() # Create Engine object (dependency)
car = Car() # Create Car object
car.inject_engine(engine) # Inject dependency via the inject_engine() method
car.drive() # Call the drive method to use the Engine
class Engine {
start() {
console.log('Engine started');
}
}
// Define an interface for injecting dependencies
class EngineInjector {
injectEngine(engine) {} // Method to inject the Engine dependency
}
class Car extends EngineInjector {
constructor() {
super();
this.engine = null; // Declaring a dependency on Engine
}
// Implement the injectEngine method to set the Engine dependency
injectEngine(engine) {
this.engine = engine; // Dependency injected through the interface method
}
drive() {
this.engine.start(); // Using the injected Engine dependency
console.log('Car is driving');
}
}
const engine = new Engine(); // Create Engine object (dependency)
const car = new Car(); // Create Car object
car.injectEngine(engine); // Inject dependency via the injectEngine() method
car.drive(); // Call the drive method to use the Engine
Output
Engine started Car is driving
- Engine Class: Has a start() method to simulate starting the engine.
- EngineInjector Interface: Declares injectEngine(Engine engine) for injecting an Engine.
- Car Class: Implements EngineInjector; injectEngine() sets the Engine, drive() calls engine.start().
- Main Method: Creates Engine and Car objects, injects the Engine into Car, then calls car.drive().
Advantages
Dependency Injection improves flexibility, maintainability, and testability by reducing direct dependencies between components.
- Increased Modularity & Loose Coupling: Components depend on abstractions instead of concrete implementations, making code cleaner, flexible, and easier to maintain.
- Improved Testability & Reusability: Dependencies can be replaced with mocks for testing, and components can be reused across different contexts.
- Easier Collaboration: Teams can work independently on separate components without worrying about internal dependencies.
Limitations
Although Dependency Injection (DI) improves flexibility and maintainability, it also has some limitations in certain scenarios.
- Increased Complexity: Managing dependencies and configurations can make small applications more complex.
- Runtime Overhead: Incorrect dependency configuration may lead to runtime errors and slight performance overhead.
- Difficult Debugging: Testing and troubleshooting dependency-related issues can sometimes be challenging.