Skip to content
Gustavo's Blog
Go back

SOLID — The Dependency Inversion Principle

If you’ve come this far, this concept should feel natural. The Dependency Inversion Principle is quite intuitive once you understand it.

In our credit card solution, you’ve successfully developed all the domain modules — entities, aggregates, services. Everything works. But right now, none of this is persistent. All the models reside in process memory. Suppose we decide to add persistence using MySQL. We need to integrate an external module to handle the database — an ORM like Entity Framework, for example.

The Naive Approach

During that implementation, it’s common for engineers to inject the dependency directly into the service layer:

┌────────────────────────────────────────────┐
│  High Level Module                         │
│                                            │
│  ┌──────────────────┐                      │     ┌──────────────┐
│  │  Service Layer   │─────────────────────────▶  │ MySQL Module │
│  └──────────────────┘                      │     └──────────────┘
│           ▲                                │
│  ┌────────┴──────────────────────────┐     │
│  │  Domain Layer                     │     │
│  │  ┌───┐ ┌───┐ ┌───┐               │     │
│  │  │   │ │   │ │   │               │     │
│  │  └───┘ └───┘ └───┘               │     │
│  └───────────────────────────────────┘     │
└────────────────────────────────────────────┘

At first glance this looks fine. But we’re making our domain directly dependent on an external component. If something changes with the MySQL module, we have to modify our high-level domain module. This violates the core principle: higher-level modules should not be subservient to lower-level modules (or external systems).

You must protect your domain at all costs, ensuring that core business rules take precedence.

Why? Because our goal is to make high-level modules reusable. External libraries are typically implemented once and seldom reused. Our core modules and domain components, on the other hand, are frequently repurposed and updated.

If your code is directly coupled to a MySQL module and that module changes the way it handles certain table rows, you’re forced to modify your domain logic. And these changes are transitive — if a module imported from MySQL is updated, you might also need to update your code accordingly.

// Naive approach — PaymentService directly depends on the concrete repository
public class PaymentService {
    private readonly PaymentRepository _paymentRepository;

    public PaymentService(PaymentRepository paymentRepository)
        => _paymentRepository = paymentRepository;
}

Inverting the Dependency

The first step is to extract the high-level policy from the module that governs the dependency. Ask yourself: why am I calling this external module?

In our case, it’s because we need to persist an amount of money. To do that, we require two essential actions:

  1. Retrieve a credit card.
  2. Save the state of that card.

These two actions represent the core policy — the behaviors we expect from an external persistence module.

So instead of coupling the MySQL module directly to our domain, we depend on an abstraction that encapsulates our policy. Then we make the repository implement that abstraction:

public class PaymentService {
    private readonly IPaymentRepository _paymentRepository;

    public PaymentService(IPaymentRepository paymentRepository)
        => _paymentRepository = paymentRepository;
}
public interface IPaymentRepository {
    public Task<CreditCard> GetCreditCard(int cardId);
    public Task SaveCreditCardState(int cardId);
}

Now PaymentService relies on an abstraction rather than a concrete implementation. Any repository must implement our domain contract — enhancing the system’s flexibility, robustness, and portability.

After the Inversion

┌───────────────────────────────────────────────────────┐
│  High Level Module                                    │
│                                                       │
│  ┌──────────────────┐   ┌─────────────────────────┐  │     ┌──────────────┐
│  │  Service Layer   │──▶│  «interface»            │◀─────  │ MySQL Module │
│  └──────────────────┘   │  IPaymentRepository     │  │     └──────────────┘
│           ▲             └─────────────────────────┘  │
│  ┌────────┴────────────────────────────────┐          │
│  │  Domain Layer                           │          │
│  │  ┌───┐ ┌───┐ ┌───┐                     │          │
│  │  │   │ │   │ │   │                     │          │
│  │  └───┘ └───┘ └───┘                     │          │
│  └─────────────────────────────────────────┘          │
└───────────────────────────────────────────────────────┘

Notice that the direct connection between our domain layer and the external world has been eliminated. Instead of exposing our entire domain module, we now expose only our contracts. The repository must implement our contract to function within our system. We reversed the direction of the dependency arrows, and as a result, all issues with transitive dependencies have been resolved.

When to Abstract

Of course, you’re not expected to abstract every external module. Assess the volatility of the dependency — if it’s prone to frequent changes, abstract it. If a concrete class is stable and no alternative implementations are anticipated, relying on it directly poses minimal risk.

The DIP in practice: our domain shouldn’t rely on external modules. Instead, we define a high-level contract — like IPaymentRepository — to handle tasks such as fetching and saving a credit card. This way, PaymentService depends on an abstraction, keeping our domain safe from changes in external systems.

Only modules likely to change frequently need to be abstracted. Protect the domain; everything else is secondary.


Share this post on:

Previous Post
SOLID — The Interface Segregation Principle
Next Post
SOLID — Liskov Substitution & Design by Contract