Dependency Injection(DI) Design Pattern

Last Updated : 13 May, 2026

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: Car class might depend on a Engine class to run. Without DI, the Car class would directly create or manage the Engine instance 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.

class_a
Dependency Injection
  • Dependency Injection solves this problem by injecting the dependencies (like the Engine ones in the Car example) 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:

dependency-injection-di-design-pattern-2
Four Roles of Dependency Injection

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:

C++
#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);
    }
};
Java
public class NotificationService {
    private EmailProvider emailProvider = new EmailProvider(); // Tightly coupled to email

    public void sendNotification(String message, String recipient) {
        emailProvider.sendEmail(message, recipient);
    }
}
Python
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)
JavaScript
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 NotificationService is tightly coupled to the EmailProvider, making it difficult to switch to a different provider without code changes.
  • Testability: Testing NotificationService in isolation is challenging as it directly uses EmailProvider.

2. Code With Dependency Injection:

C++
// 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;
}
Java
// 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");
    }
}
Python
# 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")
JavaScript
// 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: NotificationService no longer depends on a specific implementation, making it adaptable to different providers.
  • Testability: You can easily inject mock providers for testing NotificationService in 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.

C++
#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;
}
Java
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
    }
}
Python
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
JavaScript
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.

C++
#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;
}
Java
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
    }
}
Python
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
JavaScript
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.

C++
#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;
}
Java
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
    }
}
Python
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
JavaScript
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.
Comment

Explore