Dependency Inversion Principle

Depend on Abstractions, Not on Concrete Implementations

The Dependency Inversion Principle (DIP), the fifth and final principle of SOLID, states that:

“High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.” — Robert C. Martin

Let’s break this into two separate statements because each carries its own important meaning.

First Statement

High-level modules (those containing business logic) should not depend directly on low-level modules (those handling technical details such as database access or email delivery).

Instead, both layers should depend on abstractions, such as interfaces or abstract classes.

Second Statement

Abstractions themselves should not know about implementation details.

Instead, implementation details should conform to the contract defined by the abstraction.

Simply Put

Do not import concrete implementations where an interface is sufficient.

Real-Life Example: How to Understand It?

Imagine ordering food for home delivery.

You do not call a delivery driver directly. Instead, you call a restaurant and place an order.

The restaurant knows it needs someone capable of delivering the food to your address.

It does not matter which courier performs the delivery. What matters is that every courier can pick up the package and deliver it successfully.

Now imagine you insist that only one specific courier can deliver your food.

If that courier gets sick, quits the job, or is unavailable, you do not receive your meal.

You have become dependent on a specific person instead of the role that person fulfills.

Programming works the same way.

That “role” is an interface—an abstraction.

The restaurant (business logic) does not know anything about the individual courier (the concrete class).

It only knows that it needs someone who fulfills the role of a courier: picking up and delivering packages.

A Thought Exercise

Imagine an OrderProcessor class responsible for processing orders in an online store.

To complete an order, it must save data somewhere.

If OrderProcessor directly creates an instance of MySQLOrderRepository, then you are effectively calling the courier directly.

What happens when you decide to switch to PostgreSQL, MongoDB, or file-based storage for tests?

You must modify a class that should focus on business logic, not infrastructure details.

That is exactly the problem DIP solves.

Example — DIP Violation

Suppose we are building a notification system for an e-commerce platform.

The NotificationService class sends notifications to customers after they place an order.

Someone unfamiliar with DIP might write something like this:

“I need to send emails, so I’ll use EmailSender.”

// Low-level class — concrete implementation
class EmailSender {
 
    public void send(String recipient, String message) {
        // technical detail: sending email via SMTP
        System.out.println(“Sending email to: ” + recipient);
    }
}
// High-level class — business logic
class NotificationService {
 
    // Dependency on a CONCRETE IMPLEMENTATION
    private EmailSender emailSender = new EmailSender();
 
    public void notifyUser(String userEmail, String orderInfo) {
        emailSender.send(
            userEmail,
            “Your order: ” + orderInfo
        );
    }
}

At first glance, the code appears reasonable.

However, let’s see what happens when requirements change—which they always do in real projects.

Scenario 1: Add SMS Notifications

The business wants to support SMS notifications in addition to email.

To achieve this, we must modify NotificationService, even though the business logic itself has not changed.

As a side effect, we are also moving toward violating SRP.

Scenario 2: Unit Testing

In unit tests, we do not want to send real emails.

Unfortunately, there is no easy way to replace EmailSender with a mock because the class creates it internally with new.

Scenario 3: Multiple Email Providers

Suppose we want to support:

  • SendGrid,
  • Mailgun,
  • an internal SMTP server.

Every provider change requires modifying NotificationService, even though it should not care who actually sends emails.

Root Cause

NotificationService depends on a concrete implementation (EmailSender) instead of depending on an abstraction.

Example — DIP-Compliant Design

How do we fix this?

We introduce an abstraction: an interface that defines the contract:

“Anything capable of sending notifications can be used here.”

// Abstraction — the contract
interface NotificationSender {
    void send(String recipient, String message);
}

Now low-level implementations depend on the abstraction:

class EmailSender implements NotificationSender {
 
    @Override
    public void send(String recipient, String message) {
        System.out.println(
            “Email to: ” + recipient +
            ” | ” + message
        );
    }
}

class SmsSender implements NotificationSender {

 

    @Override
    public void send(String recipient, String message) {
        System.out.println(
            “SMS to: ” + recipient +
            ” | ” + message
        );
    }
}

The high-level module also depends on the abstraction:

class NotificationService {
 
    private final NotificationSender sender;
 
    // Dependency Injection
    public NotificationService(
            NotificationSender sender) {
        this.sender = sender;
    }
 
    public void notifyUser(
            String userContact,
            String orderInfo) {
 
        sender.send(
            userContact,
            “Your order: ” + orderInfo
        );
    }
}
Usage
public class Main {

 

    public static void main(String[] args) {

 

        NotificationSender emailSender =
                new EmailSender();

 

        NotificationService serviceA =
                new NotificationService(emailSender);

 

        serviceA.notifyUser(
                “john@example.com”,
                “#12345”
        );

 

        NotificationSender smsSender =
                new SmsSender();

 

        NotificationService serviceB =
                new NotificationService(smsSender);

 

        serviceB.notifyUser(
                “+48 600 000 000”,
                “#12346”
        );
    }
}

What Changed?

1. We Introduced an Interface

NotificationSender defines the contract.

Any notification sender must implement it.

2. We Removed Object Creation from Business Logic

NotificationService no longer creates dependencies using new.

Dependencies are provided externally.

This technique is called Dependency Injection (DI).

DIP is the principle. DI is a common technique used to implement it.

3. New Communication Channels Are Easy to Add

Want:

  • push notifications,
  • Slack messages,
  • Microsoft Teams notifications,
  • WhatsApp alerts?

Simply create another implementation of NotificationSender.

No changes are required in NotificationService.

4. Testing Becomes Easy

Tests can inject a fake sender instead of a real implementation.

Why Is DIP Important?

Flexibility and Replaceable Implementations

When business logic is isolated from technical details, changing:

  • databases,
  • notification channels,
  • external APIs,

does not require changing the business logic itself.

It is like replacing a light bulb while keeping the same socket.

Better Testability

This is one of the biggest practical benefits.

When a class depends on an interface, tests can inject a mock object rather than using the real implementation.

class MockSender implements NotificationSender {

 

    public List<String> sent =
            new ArrayList<>();

 

    @Override
    public void send(
            String recipient,
            String message) {

 

        sent.add(recipient);
    }
}

No emails are sent.

No external systems are contacted.

The business logic is tested in isolation.

Fewer Team Conflicts

When dependencies are based on interfaces, different developers can work independently on:

  • NotificationService,
  • EmailSender,
  • SmsSender.

The interface acts as a contract between teams.

Long-Term Benefits

Large projects that ignore DIP often evolve into what engineers call:

“spaghetti dependencies”

A tangled network of concrete class dependencies.

Every modification creates a chain reaction.

Refactoring becomes dangerous.

Testing becomes difficult.

New developers may need weeks to understand the dependency graph.

DIP is an investment whose value grows alongside the size of the project.

When Should You Apply DIP?

When a Class Connects to External Resources

Examples include:

  • databases,
  • external APIs,
  • file systems,
  • payment gateways,
  • messaging services.

The implementation may change, but business logic should remain unaffected.

When You Want Unit-Testable Code

If a class creates dependencies using:

new SomeService()

you cannot easily replace those dependencies during testing.

If dependencies are provided through constructors or setters, mocks become trivial to use.

In Frameworks That Already Use DIP

Spring Framework is essentially DIP at industrial scale.

Its IoC (Inversion of Control) container creates objects and injects dependencies automatically.

Annotations such as:

@Autowired
@Component
@Service

are mechanisms that support DIP.

@Service
public class OrderService {
 
    private final PaymentGateway paymentGateway;
 
    @Autowired
    public OrderService(
            PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }
}

OrderService depends on the PaymentGateway interface, not on a concrete implementation.

Spring decides which implementation to inject.

The class does not need to know.

Similarly, Hibernate and JPA use abstractions such as repositories and the EntityManager to separate business logic from database-specific details.

Memory Trick

Depend on the contract, not on the contractor.

If you see new ConcreteService() inside a business class, the implementation decision is probably being made in the wrong place.

Point upward to abstractions, not downward to details.

Summary

DIP completes the SOLID principles.

  • SRP says classes should have a single responsibility.
  • OCP says software should be open for extension but closed for modification.
  • LSP ensures subclasses can safely replace base classes.
  • ISP promotes small, focused interfaces.
  • DIP ties everything together by ensuring that high-level logic depends on stable abstractions rather than volatile implementation details.

Define a contract.

Depend on the contract.

Let implementation details serve the contract—not control it.

CATEGORIES:

SOLID Principles

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *

Latest Comments

No comments to show.