principios-solid-dip-inversion-dependencias

Dependency Inversion Principle (DIP) in SOLID

  • 5 min

The Dependency Inversion Principle consists of depending on abstractions, not on concrete implementations.

We close the SOLID block with the letter D: the Dependency Inversion Principle (DIP).

This principle is one of the most impactful in maintainable architectures, because it directly attacks the coupling between business logic and technical details.

The formal definition given by Robert C. Martin consists of 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.

This sounds very technical, so let’s translate it into plain language.

What is high and low level?

  • High-level: Where the important logic, business rules, and the core behavior of your application reside (e.g., OrderManager or TaxCalculator).
  • Low-level: The technical details, the tools, the “plumbing” (e.g., MySQLConnection, FileReader, or SmtpMailer).

What the DIP tells us is that business logic should not depend on technical details, such as the database or an external API.

Imagine a lamp and a wall socket.

  • The house’s electrical installation (high level) does not depend on plugging in a specific Philips lamp (low level).
  • Both depend on a standard abstraction: the European Socket.
  • You can change the lamp without breaking the wall.

The problem: direct dependency

In traditional or structured programming, dependency usually flows from top to bottom.

The ReportManager (high level) creates an instance of SQLDatabase (low level) to fetch the data.

// ❌ BAD DESIGN: Direct dependency
public class ReportManager
{
    // Tight coupling to a detail (SQL)
    private SQLDatabase _database = new SQLDatabase();

    public void Generate()
    {
        var data = _database.GetData(); // If we switch to MongoDB, this breaks
        // ... report logic
    }
}
Copied!

Here, the dependency arrow points downwards: ReportManager —> SQLDatabase

If SQLDatabase changes, ReportManager is affected. This violates the DIP.

Invert the dependency

To comply with the principle, we introduce an interface in between.

  1. The high-level module defines an interface: IData.
  2. The low-level module implements that interface: SQLDatabase : IData.

Now, ReportManager depends on IData. And SQLDatabase also depends on IData.

Notice the change: the dependency arrow of the low-level module has been inverted. It now points upwards (towards the abstraction).

// 1. Abstraction (Contract)
public interface IData
{
    List<string> GetData();
}

// 2. Low-Level Module (Detail)
// Now depends on the abstraction
public class SQLDatabase : IData
{
    public List<string> GetData() { /* ... SQL ... */ }
}

// 3. High-Level Module
public class ReportManager
{
    private IData _database;

    // We ask for the abstraction, not the detail
    public ReportManager(IData database)
    {
        _database = database;
    }
}
Copied!

Now ReportManager is isolated from the database implementation as long as the contract is maintained. It doesn’t matter if it’s SQL, an XML file, or a fake API for testing. It only cares that it fulfills IData.

DIP vs. dependency injection

This is a common confusion: they are not the same thing.

  • Dependency Inversion Principle (DIP): It is the principle (the theoretical concept, the “what”). It tells us to depend on abstractions.
  • Dependency Injection (DI): It is the pattern or technique (the “how”). It is the way to deliver those dependencies to objects, usually via the constructor.

In the previous example:

  • We comply with DIP because we use the IData interface instead of the SQLDatabase class.
  • We use DI by passing the database variable through the constructor public ReportManager(IData database).

Dependency Injection is the most common tool to respect the Dependency Inversion Principle.

Dependency injection containers

In large applications, manually doing new ReportManager(new SQLDatabase()) all the time is tedious.

That’s why inversion of control (IoC) containers exist, like the built-in one in .NET, Spring in Java, or Inversify in TypeScript. These containers are responsible for creating instances and resolving dependencies.

You tell the container: “Hey, when someone asks for IData, give them a SQLDatabase. And it handles all the wiring.

Benefits of the DIP

  1. More robust code: Changes in infrastructure details (database, web services) do not break business logic.
  2. Ease of testing: This is the most immediate benefit. If your class depends on interfaces, you can inject mocks or stubs during unit testing. For example, instead of connecting to the real database to test the report, you inject a FakeDatabase that returns fixed data in memory.
  3. Parallel development: One team can work on the logic (with the interface defined) and another on the database implementation simultaneously.