Skip to main content

Command Palette

Search for a command to run...

Dependency Inversion Principle (DIP) in Java

Updated
โ€ข5 min readโ€ขView as Markdown
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.


More from this blog

Booster TechLab

27 posts

Booster TechLab is a developer-focused publication sharing interview-ready and real-world content on programming, frameworks, system design, and modern tech to help developers grow faster.