Abstract Software Architecture
SOLID Principles: Part 5

Dependency Inversion

Decouple your high-level policy from low-level details. Build software that is flexible, testable, and robust.

The Formal Definition

"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 (Uncle Bob)
layers

High-Level Modules

The core business logic or "policy" of your application. It decides what needs to happen (e.g., "Process an order").

settings_input_component

Low-Level Modules

The implementation details or "mechanisms". They decide how it happens (e.g., "Write to SQL database", "Send SMTP email").

Dependency Flow

High Level
Low Level

Tightly Coupled

High Level
Low Level

Loosely Coupled

Interactive Analogy: The Hardwired Home

Imagine a house where the light switch is soldered directly to the lamp. To change the appliance, you have to rip open the wall. That's a violation of DIP.

Architecture Mode Violating DIP

Switch is hard-coded to control the specific Device class directly.

Select Appliance

class Switch
High Level Policy
power interface ISwitchable
class Lamp
Low Level Detail
The Switch
Refactoring Switch Class... Direct dependency requires code change

Code Lab: Order Processor

Compare the implementation. Notice how the "Good" version doesn't need to change the High-Level OrderService even when we switch from Email to SMS.

cancel Violation (Coupled)

OrderService.ts
import { EmailSender } from "./EmailSender";

class OrderService {
    private sender: EmailSender; // Direct dependency!

    constructor() {
        // High-level creates Low-level directly
        this.sender = new EmailSender();
    }

    completeOrder(orderId) {
        console.log("Order completed");
        this.sender.sendEmail("Success!");
    }
}

Problem: To use SMS, you must rewrite OrderService. This violates the Open-Closed Principle.

check_circle Solution (Inverted)

OrderService.ts
import { INotifier } from "./interfaces";

class OrderService {
    private notifier: INotifier; // Depends on Abstraction

    // Dependency Injection (via Constructor)
    constructor(notifier: INotifier) {
        this.notifier = notifier;
    }

    completeOrder(orderId) {
        console.log("Order completed");
        this.notifier.notify("Success!");
    }
}

Benefit: OrderService never changes. Pass in EmailSender or SmsSender at runtime.

Why does this matter?

bug_report

Testability

You can easily swap real databases or APIs with "Mock" versions for unit testing. The high-level code won't know the difference.

extension

Flexibility

Want to switch from MySQL to MongoDB? Or send notifications via Slack instead of Email? Just create a new class implementing the interface.

handyman

Maintainability

Changes in low-level details (like an API update) are isolated. They don't ripple up and break your core business logic.

info

DIP vs Dependency Injection

Don't confuse the two! DIP is the Principle (the architectural goal). Dependency Injection (DI) is a Design Pattern (the technique) used to achieve it. You use DI (passing objects via constructors) to satisfy the DIP.

Knowledge Check

In a DIP-compliant architecture, what does the High-Level Module depend on?