Skip to main content

Command Palette

Search for a command to run...

Dependency Inversion Principle (DIP) in Java

Updated
β€’5 min read
Dependency Inversion Principle (DIP) in Java

Introduction

In software development, one of the biggest challenges is tight coupling β€” when one class directly depends on another concrete class. This makes systems hard to modify, test, and extend.

The Dependency Inversion Principle (DIP) helps solve this problem by introducing abstractions between components.

It is the 5th principle of SOLID and is widely used in modern frameworks like Spring, Hibernate, and enterprise Java applications.


What is Dependency Inversion Principle?

The Dependency Inversion Principle states:

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.

🧠 In Simple Words

πŸ‘‰ Always depend on interfaces, not concrete classes.

Instead of:

Notification β†’ EmailService

Use:

Notification β†’ MessageService ← EmailService
                                  SMSService

This makes your system flexible and maintainable.


Real-World Analogy

Imagine a mobile charger system

  • Mobile = High-level module

  • Charger = Low-level module

  • USB Port = Abstraction

Your mobile doesn't depend on:

  • Samsung Charger

  • Xiaomi Charger

  • OnePlus Charger

It depends on USB interface.

That is Dependency Inversion Principle.


Without Dependency Inversion (Bad Design)

Here the high-level module depends directly on a low-level module.

// Low-Level Module
class EmailService {

    public void sendEmail(String message) {
        System.out.println("Sending Email: " + message);
    }
}

// High-Level Module
class Notification {

    private EmailService emailService;

    public Notification() {

        // Direct dependency (Tight Coupling)
        emailService = new EmailService();

    }

    public void notifyUser(String message) {

        emailService.sendEmail(message);

    }
}

// Main Class
public class Main {

    public static void main(String[] args) {

        Notification notification =
                new Notification();

        notification.notifyUser("Hello User!");

    }

}

🚨 Problems in This Design

Tight coupling between classes
Cannot easily switch to SMS or WhatsApp
Hard to test
Violates Open/Closed Principle
Difficult to extend

If tomorrow you add SMS, you must modify the Notification class.

That is bad design.


Applying Dependency Inversion Principle (Good Design)

Now we introduce an abstraction (interface) between high-level and low-level modules.


Step 1: Create Abstraction (Interface)

// Abstraction
interface MessageService {

    void sendMessage(String message);

}

This interface defines what should be done, not how.


Step 2: Implement Low-Level Modules

// Low-Level Module 1
class EmailService implements MessageService {

    @Override
    public void sendMessage(String message) {

        System.out.println(
                "Sending Email: " + message
        );

    }

}

// Low-Level Module 2
class SMSService implements MessageService {

    @Override
    public void sendMessage(String message) {

        System.out.println(
                "Sending SMS: " + message
        );

    }

}

These classes provide actual implementations.


Step 3: High-Level Module Depends on Interface

// High-Level Module
class Notification {

    private MessageService messageService;

    // Constructor Injection
    public Notification(
            MessageService messageService
    ) {

        this.messageService = messageService;

    }

    public void notifyUser(String message) {

        messageService.sendMessage(message);

    }

}

Now Notification depends on:

MessageService (Interface)

Not:

EmailService (Concrete class)

Step 4: Main Class (Runtime Dependency Injection)

public class Main {

    public static void main(String[] args) {

        // Choose implementation
        MessageService service =
                new EmailService();

        Notification notification =
                new Notification(service);

        notification.notifyUser(
                "Dependency Inversion Applied!"
        );

    }

}

πŸ”„ Execution Flow (Step-by-Step)

Let's understand what happens internally.


Flow Explanation

1️⃣ Main class creates EmailService

EmailService service =
        new EmailService();

2️⃣ EmailService is passed to Notification

Notification notification =
        new Notification(service);

This is called:

πŸ‘‰ Constructor Injection


3️⃣ Notification stores the interface reference

private MessageService messageService;

4️⃣ When notifyUser() is called:

messageService.sendMessage(message);

Actual implementation runs:

EmailService.sendMessage()

πŸ” Switching Implementation Easily

You can switch implementation without modifying Notification.

MessageService service =
        new SMSService();

Notification notification =
        new Notification(service);

notification.notifyUser("Hello via SMS!");

No code changes required.

That is flexibility.


Unit Testing Becomes Easy

You can create mock services:

class MockMessageService
        implements MessageService {

    @Override
    public void sendMessage(String message) {

        System.out.println(
                "Mock Message Sent"
        );

    }

}

Now testing becomes:

βœ” Simple

βœ” Fast

βœ” Independent


🧠 Core Concept Summary

Without DIP

Notification β†’ EmailService

Tight coupling ❌


With DIP

Notification β†’ MessageService ← EmailService
                                 SMSService

Loose coupling βœ…


Benefits of Dependency Inversion Principle

βœ” Reduces tight coupling

βœ” Improves flexibility

βœ” Makes testing easier

βœ” Supports scalability

βœ” Encourages modular design

βœ” Improves maintainability

βœ” Enables Dependency Injection


When NOT to Use DIP

Avoid DIP when:

Writing very small programs

Simple scripts

No expected extension

Over-engineering risk

Use DIP when:

βœ” Building scalable systems

βœ” Enterprise applications

βœ” Framework-level code

βœ” Systems with multiple implementations


Complete Final Code (Clean Version)

// Abstraction
interface MessageService {

    void sendMessage(String message);

}

// Low-Level Module
class EmailService
        implements MessageService {

    public void sendMessage(String message) {

        System.out.println(
                "Email: " + message
        );

    }

}

// Another Low-Level Module
class SMSService
        implements MessageService {

    public void sendMessage(String message) {

        System.out.println(
                "SMS: " + message
        );

    }

}

// High-Level Module
class Notification {

    private MessageService messageService;

    public Notification(
            MessageService messageService
    ) {

        this.messageService = messageService;

    }

    public void notifyUser(String message) {

        messageService.sendMessage(message);

    }

}

// Main Class
public class Main {

    public static void main(String[] args) {

        MessageService service =
                new EmailService();

        Notification notification =
                new Notification(service);

        notification.notifyUser(
                "DIP Applied Successfully!"
        );

    }

}

🧠 Final Thought

Dependency Inversion Principle transforms rigid systems into flexible architectures by making components depend on contracts instead of implementations.


Design Principles for Java Developers

Part 1 of 10

This series explains core Java design principles and SOLID principles with simple examples, real-world use cases, and interview-focused explanations to help developers write clean, maintainable, and scalable code.

Up next

Interface Segregation Principle (ISP) in Java

SOLID Design Principle "Clients should not be forced to depend on interfaces they do not use." The Interface Segregation Principle (ISP) is one of the five SOLID principles that helps developers des