SOLID Principles

Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details.

12 min readEssential

1What is DIP?

Real-World Analogy
Power Outlet

Your laptop does not directly connect to the power grid - it uses a standard outlet interface. Both the power company and your laptop depend on this standard. You can use any compatible charger.

Chef and Recipe

A restaurant (high-level) does not depend on one specific chef. It depends on the Chef interface (can cook recipes). Any chef implementing this interface can work there.

Dependency Inversion Principle (DIP) has two parts:
  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

Traditional vs Inverted Dependencies

❌ Traditional
OrderService
(high-level)
depends on
MySQLDatabase
(low-level)
High-level tied to low-level
✅ Inverted
OrderService
(high-level)
depends on
IRepository
(abstraction)
implemented by
MySQL
Mongo

2DIP Violation

Tight Coupling - DIP Violation
// ❌ DIP VIOLATION: High-level directly depends on low-level

// Low-level module
class MySQLDatabase {
    public void save(Order order) {
        System.out.println("Saving order to MySQL: " + order.getId());
    }

    public Order findById(String id) {
        return new Order(id);
    }
}

// High-level module - DIRECTLY depends on MySQL
class OrderService {
    private MySQLDatabase database;  // ❌ Concrete dependency

    public OrderService() {
        this.database = new MySQLDatabase();  // ❌ Creates its own dependency
    }

    public void createOrder(Order order) {
        // Business logic
        order.validate();
        order.calculateTotal();

        // ❌ Tightly coupled to MySQL
        database.save(order);
    }
}

// Problems:
// 1. Cannot switch to MongoDB without modifying OrderService
// 2. Cannot unit test without real MySQL database
// 3. Changes to MySQL affect OrderService
// 4. High-level policy tied to low-level implementation

3DIP-Compliant Solution

Introduce an abstraction. High-level depends on abstraction, low-level implements it:

Dependency Inversion with Abstraction
// ✅ DIP COMPLIANT: Both depend on abstraction

// Abstraction
interface OrderRepository {
    void save(Order order);
    Order findById(String id);
}

// Low-level implements abstraction
class MySQLOrderRepository implements OrderRepository {
    @Override
    public void save(Order order) {
        System.out.println("Saving to MySQL: " + order.getId());
    }

    @Override
    public Order findById(String id) {
        return new Order(id);
    }
}

class MongoOrderRepository implements OrderRepository {
    @Override
    public void save(Order order) {
        System.out.println("Saving to MongoDB: " + order.getId());
    }

    @Override
    public Order findById(String id) {
        return new Order(id);
    }
}

// High-level depends on abstraction
class OrderService {
    private OrderRepository repository;  // ✅ Depends on interface

    // ✅ Dependency injected from outside
    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    public void createOrder(Order order) {
        order.validate();
        order.calculateTotal();
        repository.save(order);  // ✅ Works with any implementation
    }
}

// Usage - Dependency Injection
public class Main {
    public static void main(String[] args) {
        // Choose implementation at runtime
        OrderRepository repo = new MySQLOrderRepository();
        OrderService service = new OrderService(repo);
        service.createOrder(new Order("123"));

        // Switch to MongoDB - no changes to OrderService!
        repo = new MongoOrderRepository();
        service = new OrderService(repo);
        service.createOrder(new Order("456"));

        // For testing - use mock
        repo = new MockOrderRepository();
        service = new OrderService(repo);
    }
}

4Dependency Injection

DI is a technique to implement DIP. Dependencies are provided from outside rather than created inside:

Constructor Injection(Preferred)
Dependencies passed via constructor
OrderService(IRepo repo)
Setter Injection
Dependencies set via setter methods
setRepository(IRepo repo)
Interface Injection
Interface defines setter for dependency
IRepoAware.setRepo(IRepo)
DI Containers/Frameworks
Frameworks like Spring (Java), FastAPI (Python), and NestJS (Node) handle DI automatically. You declare dependencies, and the framework wires them up at runtime.

5Interview Questions

What is the Dependency Inversion Principle?
DIP states: 1) High-level modules should not depend on low-level modules; both should depend on abstractions. 2) Abstractions should not depend on details; details should depend on abstractions. Instead of OrderService depending on MySQLDatabase, both depend on IRepository interface.
What is the difference between DIP and Dependency Injection?
DIP is a principle (what to achieve): depend on abstractions. DI is a technique (how to achieve): inject dependencies from outside. DI is one way to implement DIP. You could also use Service Locator or Factory patterns to achieve DIP, but DI is preferred.
Why is DIP important for testing?
Without DIP, OrderService creates MySQLDatabase internally - you cannot test without a real database. With DIP, you inject IRepository - for tests, inject a MockRepository that simulates database behavior. This enables fast, isolated unit tests without external dependencies.

6Key Takeaways

1DIP: High-level and low-level should both depend on abstractions.
2Abstractions should not depend on details; details depend on abstractions.
3Dependency Injection is a technique to implement DIP.
4Enables loose coupling, easier testing, and flexibility.
5Constructor injection is the preferred DI approach.
6DI frameworks (Spring, FastAPI) automate dependency wiring.