2026-04-30
The Dependency Inversion Principle (DIP) — the "D" in SOLID — states two things: (1) high-level modules should not depend on low-level modules; both should depend on abstractions, and (2) abstractions should not depend on details; details should depend on abstractions. This sounds academic until you feel the pain of violating it.
Imagine an OrderService that directly instantiates a PostgresRepository and a StripePaymentGateway. Your high-level business logic — processing orders — is now welded to Postgres and Stripe. Want to swap to MySQL during a migration? Need to run tests without hitting Stripe's API? You're rewriting OrderService.
The fix is to invert the dependency direction. Instead of OrderService reaching down to concrete implementations, you define interfaces that the high-level module owns:
OrderRepository — an interface with methods like save(order) and findById(id)PaymentGateway — an interface with charge(amount, token)Now PostgresRepository implements OrderRepository, and StripeGateway implements PaymentGateway. The critical shift: the interfaces live with the high-level module, not with the implementations. The low-level modules depend upward on those abstractions, not the other way around.
This is different from simply "coding to an interface." DIP is about who owns the abstraction. If your OrderRepository interface lives in the postgres package, you've technically used an interface but haven't inverted anything — your domain still depends on your infrastructure package.
A real-world example: a notification system that sends alerts via email, Slack, and SMS. Without DIP, your AlertService imports all three clients. With DIP, you define a Notifier interface in the alerting domain. Each channel implements it. Adding PagerDuty means writing one new class — AlertService never changes.
Rule of thumb: count your import statements. If a high-level module imports more than 2-3 infrastructure packages directly, you're likely violating DIP. Each direct infrastructure import is a coupling point that makes testing harder and change riskier.
Common mistakes to avoid:
OrderRepository interface that exposes executeSqlQuery() defeats the purpose. The abstraction should reflect domain operations, not implementation details.