DEV Community

Cover image for Dependency Inversion Principle (DIP)
Anas Hussain
Anas Hussain

Posted on

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (DIP) is the final "D" in SOLID. It states two critical rules:

  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.

In simple terms: Your core business logic should not rely on specific tools or external technologies. Instead, both your logic and your tools should rely on a common blueprint (Interface).


The Real-World Example: Imagine you buy a brand-new smartphone

  • The Bad Design (Tight Coupling): Imagine if the phone manufacturer hardwired the charging cable directly inside your home's living room wall outlet. Your phone would charge perfectly in that one spot. But if you went to a coffee shop, or if you moved to a new house, your phone would be completely useless because it is tightly coupled to one specific wall.
  • The Good Design (Dependency Inversion): The smartphone manufacturer and the house architect agree on a common abstraction: the USB-C port.
  • The phone doesn't care if the power comes from a wall socket, a laptop port, an airplane seat, or a portable power bank.
  • It only depends on the shape of the USB-C contract. You can swap the power source instantly without modifying your phone.

❌ The Wrong Way: Depending directly on low-level tools (Violates DIP)

Here, the high-level NotificationManager depends directly on a specific low-level tool (MySQLDatabase).

class MySQLDatabase {
    public function saveLog($message) {
        echo "Saving log to MySQL database: $message\n";
    }
}

class NotificationManager {
    private $database;

    public function __construct() {
        // ❌ Tightly coupled! High-level class directly instantiates the low-level class.
        $this->database = new MySQLDatabase(); 
    }

    public function sendAlert($message) {
        // Sending alert logic...
        $this->database->saveLog($message);
    }
}
Enter fullscreen mode Exit fullscreen mode

The Problem: If your company decides to switch from MySQL to MongoDB or Firebase next week, you are forced to open up and completely rewrite your NotificationManager class, risking bugs in your alert system.


✅The Right Way: Inverting the dependency (Adheres to DIP)

We introduce an interface (the USB-C port) between the high-level class and the low-level tool. Both now look up to the abstraction.

Step 1: Create the Abstraction (The Blueprint)

interface LoggerInterface {
    public function log(string $message): void;
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Make low-level tools adapt to the blueprint

class MySqlLogger implements LoggerInterface {
    public function log(string $message): void {
        echo "Saved to MySQL: $message\n";
    }
}

class MongoDbLogger implements LoggerInterface {
    public function log(string $message): void {
        echo "Saved to MongoDB: $message\n";
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Inject the abstraction into the high-level class

The NotificationManager now only knows about the interface. It has no idea (and does not care) what database is being used behind the scenes.

class NotificationManager {
    // Rely completely on the Abstraction (Interface)
    public function __construct(
        private LoggerInterface $logger
    ) {}

    public function sendAlert($message) {
        // Sending alert logic...
        $this->logger->log($message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Why this helps you

  • Effortless Tech Swaps: You can change your database, payment gateway, or email provider by changing a single line of setup code, without touching your main business logic.
  • Flawless Testing: You can pass a fake "MockLogger" into the NotificationManager during automated testing so your tests run lightning fast without actually writing data to a real database.
  • Loose coupling: Reduces dependencies between modules, making the code more flexible and easier to test.
  • Flexibility: Enables changes to implementations without affecting clients.
  • Maintainability: Makes code easier to understand and modify.

The main reason 90% of engineers fail to use the Dependency Inversion Principle (DIP) is that they confuse it with Dependency Injection (DI).
They think that by passing a class into a constructor __construct($mysqlDatabase), they are applying DIP. They aren't. Passing a concrete, specific class into another class is just Dependency Injection. DIP requires an Interface (an abstraction) to step in and flip the ownership of the relationship.
Here is how to clear up the confusion and make definitive, master-level architectural decisions.


The 3 Main Confusions Cleared Up

1. Confusion: "Is Dependency Inversion the same thing as Dependency Injection?"

  • Reality: No. They are entirely different concepts that work together:
  • Dependency Injection (DI) is a technique for passing a tool into a class (e.g., passing a key into a lock instead of making the lock build its own key).
  • Dependency Inversion (DIP) is an architectural goal that states high-level policies should not depend on low-level details. You use DI to achieve DIP.

2. Confusion: "Does DIP mean every single new keyword in PHP is a sin?"

  • Reality: No. This confusion leads to massive over-engineering. You do not need interfaces for raw data objects, entities, or value objects (like new User(), new DateTime(), or new Collection()). You only apply DIP to Services and Infrastructure—classes that perform tasks or talk to external resources (like databases, APIs, file systems, and payment gateways).

3. Confusion: "Who actually owns the interface?"

  • Reality: In traditional programming, the low-level database or API team defines the structure, and the business logic has to match it. DIP completely inverts this. The high-level business logic defines what it needs, and the low-level database adapters must bend to fit that shape.

The Ultimate DIP Clean Code Check

You are violating DIP if you see your high-level controllers or business services look like this:

class OrderController {
    public function checkout() {
        // ❌ Direct concrete instantiation. 
        // This controller is trapped inside Stripe's ecosystem forever.
        $payment = new StripePaymentGateway(); 
        $payment->charge();
    }
}
Enter fullscreen mode Exit fullscreen mode

The 3-Step Decision Matrix (How to Choose)

When deciding whether to create an abstraction line for a class in PHP, use this mental map:

START DIP_Design_Check

    IF Class_Talks_To_External_Resources_Outside_RAM IS True THEN

        IF Tool_Belongs_To_External_Vendor_Or_Package IS True THEN
            EXECUTE Apply_DIP_Wrap_In_Interface_Immediately

        ELSE
            IF Highly_Likely_To_Change_Tools IS True THEN
                EXECUTE Apply_DIP_Interface
            ELSE
                EXECUTE Direct_Type_Hint_Is_Fine
            END IF
        END IF

    ELSE
        IF Is_Core_Business_Entity IS True THEN
            EXECUTE Do_NOT_Use_DIP_Instantiate_Safely_With_New
        ELSE
            EXECUTE Leave_It_Alone
        END IF

    END IF

END DIP_Design_Check
Enter fullscreen mode Exit fullscreen mode
  1. The "Boundary" Rule: If a class crosses the boundary of your application to talk to the outside world (a payment provider, an SMS gateway, Amazon S3 storage), always apply DIP. Create an interface.
  2. The "Volatile Dependency" Test: Ask yourself: "If this software package or vendor shuts down tomorrow, goes bankrupt, or triples their prices, how many files do I have to rewrite?" If the answer is more than one config or provider file, your system lacks Dependency Inversion.
  3. The Mockability Factor: When writing unit tests, do you need to avoid hitting a live server (like preventing a real charge on a credit card)? If yes, you need an interface so you can swap the real service with a MockPaymentGateway during your test sweeps.

Summary for Decision Making

  • High-level modules (Controllers, Domain Use Cases, Business Logic) contain the identity and rules of your app.
  • Low-level modules (MySQL, Redis, AWS, Twilio, Stripe) are merely utility workers.

Refference
Wikepedia


The Laravel Blueprint: Mastering DIP (example)

Let's look at how a Junior developer tightly couples their code to a specific tool, and how a Laravel Master uses Contracts to execute Dependency Inversion.

❌ The Junior Approach (Violates DIP)

Imagine your Laravel application needs to upload user avatars to Amazon S3. A junior developer injects the concrete S3 client directly into a service.

namespace App\Services;

use Aws\S3\S3Client; // ❌ Tightly coupled to Amazon's official SDK!

class ProfileAvatarService
{
    protected $s3;

    public function __construct()
    {
        // ❌ The high-level service creates its own low-level tool.
        $this->s3 = new S3Client([
            'version' => 'latest',
            'region'  => 'us-east-1'
        ]);
    }

    public function updateAvatar($user, $file)
    {
        // Concrete Amazon-specific method execution
        $this->s3->putObject([
            'Bucket' => 'my-avatars',
            'Key'    => "avatars/{$user->id}.jpg",
            'Body'   => fopen($file, 'r')
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Why this fails Laravel Mastery: Your app is trapped in AWS. If your company moves to Google Cloud Storage or DigitalOcean Spaces to save money, you have to find every file referencing S3Client, delete the code, and completely rewrite your upload logic.


The Laravel Mastery Approach (Adheres to DIP)

Laravel handles this using its native Storage Contracts (Illuminate\Contracts\Filesystem\Filesystem). Your high-level business code only talks to the contract. The infrastructure tools (S3, local disk, SFTP) are forced to adapt to what the contract demands.

Step 1: Depend entirely on the Abstraction

Your service doesn't know about Amazon, Google, or local hard drives. It only knows about the Laravel Filesystem contract.

namespace App\Services;

// We import the standard abstract Contract, NOT a concrete SDK package!
use Illuminate\Contracts\Filesystem\Filesystem;

class ProfileAvatarService
{
    // Laravel's Service Container automatically injects the active driver here
    public function __construct(
        protected Filesystem $storage
    ) {}

    public function updateAvatar($user, $file)
    {
        // The common "HDMI plug" method that works on ANY storage system
        $this->storage->put("avatars/{$user->id}.jpg", file_get_contents($file));
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Swap the low-level details in .env

Because both your service and Laravel's drivers depend on the same underlying abstraction, you don't need to change a single line of PHP code to swap out the technology. You just update your configuration or your environment file:

# Switch from Amazon S3 to Local Storage instantly without breaking the app:
FILESYSTEM_DISK=s3
# FILESYSTEM_DISK=local
# FILESYSTEM_DISK=gcs
Enter fullscreen mode Exit fullscreen mode

Step 3: How Laravel wires it up under the hood

If you ever build a custom storage solution (like a custom database-driven file system), you register it in a Service Provider using the Service Container:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Filesystem\Filesystem;
use App\Services\CustomDatabaseStorageDriver;

class StorageServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Tell Laravel: Whenever a class asks for the Filesystem Contract,
        // hand them our custom driver implementation.
        $this->app->bind(Filesystem::class, function ($app) {
            return new CustomDatabaseStorageDriver();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Why this makes you a Laravel Master

  1. Seamless Testing: When writing a feature test for uploading an avatar, you can call Storage::fake('avatars'). Laravel instantly swaps the real storage engine with an in-memory virtual disk. Your tests execute instantly, and you never accidentally upload junk test images to a live Amazon S3 account.
  2. Plugin Architecture: Your application becomes modular. You can change your cache systems, database engines, mail dispatchers, and queue runners on the fly because your controllers and services only interact with framework contracts.

Top comments (0)