Skip to content

Dependency Inversion Principle (DIP)

Business logic should not depend on details like SMS vendors or databases. Both depend on an interface that the business side owns.

Builds on Interfaces and Dependency injection

Stage 2core4 min read

Principle (restated)

The Dependency Inversion Principle says:

  • 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 plain words: business code says what it needs through a fixed contract. Email, SMS, database, or vendor code then fits that contract. The contract should use business words and should not copy a vendor's API.


What the Good Code Does

Key components

  • NotificationChannel is the abstraction. It promises void send(String msg) and does not say how the message travels.
  • EmailService and SMSService are concrete, low-level classes that implement it.
  • NotificationService is the high-level class. Its field type is NotificationChannel, not EmailService or SMSService.
  • Main is the composition point, the one place where objects are created and connected. It picks the implementation and passes it into the service. The real Main builds two services: one with EmailService and one with SMSService.

Code flow (conceptual)

  1. Main creates a concrete channel, such as new EmailService().
  2. Main passes it to the NotificationService constructor.
  3. NotificationService stores it as a NotificationChannel.
  4. When notify(msg) runs, the service calls notificationChannel.send(msg).
  5. Java picks the real send method at runtime, based on the object that was passed in (runtime dispatch).

The service still reaches email or SMS at runtime. What changed is the source-code dependency: the service now imports the interface and not the detail.

Passing the dependency in through the constructor is dependency injection. The class receives what it needs from outside instead of building it.


How this satisfies DIP

  1. High-level code depends on an abstraction. NotificationService mentions only NotificationChannel.
  2. Low-level code also depends on that abstraction. EmailService and SMSService implement the interface.
  3. The dependency arrows meet at the contract. The high-level class uses it and the details follow it.
  4. Implementations are replaceable. WhatsApp, push, or a test fake can be passed in without editing NotificationService.
  5. Construction sits at the edge. Main, a factory, or a DI container chooses the concrete object.

The best version of this design keeps NotificationChannel next to the high-level business code. If it sat in a vendor package and took vendor-specific parameters, the business side would still have to speak the vendor's language.


Benefits (detailed)

  • Extensibility / Open-Closed: add a channel by implementing NotificationChannel. The service stays unchanged.
  • Testability: pass in a fake that records the message, so no real email or SMS goes out.
  • Single Responsibility: NotificationService runs the use case. The sender classes own the delivery details.
  • Maintainability: when an API or library changes, only the concrete class changes.
  • Reuse and composition: the same service can run with different channels in different environments.
  • Explicit dependencies: the constructor shows what the class needs before it can work.

A tiny fake keeps a test predictable. It stores the message passed to send, and the test checks that value. No credentials or waiting are needed.


Design choices and alternatives

  • Constructor injection, used here, makes required dependencies visible and lets fields be final.
  • Setter injection suits an optional dependency, but it allows a half-configured object.
  • A factory or provider helps when implementations are expensive to create or chosen per request.
  • A DI container can create and connect a large set of objects. It is convenient but not required for DIP.
  • For several channels, inject List<NotificationChannel> and loop, or inject a registry when the choice is made at runtime.

DIP and dependency injection are related but different. Injecting a concrete SMSService is DI, yet the high-level class still depends on a detail. Depending on NotificationChannel is the design decision that follows DIP.


Small checklist to recognize DIP compliance

  • Does the high-level class depend on an interface shaped around its own need?
  • Do concrete classes implement that interface instead of being created by the high-level class?
  • Can a new implementation be added without changing the high-level module?
  • Can a test substitute a fake without network, database, or clock setup?
  • Does the business package avoid imports from the vendor package?

Several "yes" answers are a good sign. An interface alone does not prove DIP. Who owns it, what it is called, and which way the dependencies point still matter.


Example improvements you might consider later

  • Make the channel field final so the required dependency cannot change after construction.
  • Rename notify(String) to notifyCustomer(String), because Object.notify() already has a well-known meaning in thread signalling.
  • Add a recipient value instead of sending only a message string.
  • Use a registry when configuration or request data selects a channel.
  • Add logging, retries, and error handling as wrappers so the transport classes stay focused.
  • For asynchronous delivery, return a future or publish a notification request to a queue.

These are later improvements. You do not need them to understand the principle.


Conclusion

The refactor inverts the source dependency. NotificationService depends on the NotificationChannel abstraction, and the concrete senders implement it. Runtime calls still reach a real sender, but the business rule no longer knows which delivery technology does the work.

Do not create an interface for every class. The extra layer pays off around details that change, do I/O, or need test fakes. Pure calculations and stable value objects can stay concrete.

Compare with BadCode and read DIP-Notes.md for payment-gateway, clock, DI, and IoC examples.

In this topic

After code5 files

GoodCode/EmailService.java10 linesGitHub
package SOLID.DIP.GoodCode;public class EmailService implements NotificationChannel {    @Override    public void send(String msg) {        System.out.println("Sending Email  : " + msg);    }}
GoodCode/Main.java13 linesGitHub
package SOLID.DIP.GoodCode;public class Main {    @SuppressWarnings("unused")    public static void main(String[] args) {        NotificationService emailNotification = new NotificationService(new EmailService());        emailNotification.notify("Your order has been shipped");        NotificationService smsNotification = new NotificationService(new SMSService());        smsNotification.notify("OTP is 1234");    }}
GoodCode/NotificationChannel.java6 linesGitHub
package SOLID.DIP.GoodCode;public interface NotificationChannel {    void send(String msg);}
GoodCode/NotificationService.java14 linesGitHub
package SOLID.DIP.GoodCode;public class NotificationService {    private NotificationChannel notificationChannel;    public NotificationService(NotificationChannel channel) {        this.notificationChannel = channel;    }    public void notify(String msg) {        notificationChannel.send(msg);    }}
GoodCode/SMSService.java10 linesGitHub
package SOLID.DIP.GoodCode;public class SMSService implements NotificationChannel {    @Override    public void send(String msg) {        System.out.println("Sending SMS : " + msg);    }}

Done with this topic?

How well do you know it?

PreviousInterface Segregation Principle (ISP)
Practise 7 questions