Dependency Inversion Principle
Depend on abstractions.
High-level policy doesn't depend on volatile detail. Both hinge on a stable abstraction. The concrete adapters sit at the edges; the inside of the system is built from interfaces.
Opinion
I have lost track of the times I have refactored a service that “just” called the database directly into one that depends on a repository interface. The line-of-code count barely changes; the testability changes by an order of magnitude. DIP is not a bureaucratic detour; it is the rule that allows a real database to be swapped for an in-memory fake during a test, a third-party email service for a no-op during a CI run, an HTTP client for a recorded fixture during local development. Every team that ships software has rediscovered this rule the hard way; the only argument left is whether to teach it once or pay for it forever.
The acronym confusion is the load-bearing one. SOLID's D is class-level: depend on an abstract type, not a concrete one. The Stable Dependencies Principle is package-level: depend in the direction of stability.6Clean Architecture (2017), Ch. 14 “Component Coupling.” Spells out the link: SDP + SAP combined ≈ DIP for components. Analogy, not identity. Martin himself spells out the link: SDP plus the Stable Abstractions Principle (SAP) is “DIP for components”. That is an analogy, not an identity. SDP asks which folder may import which folder; DIP asks what shape that import should be. Two graphs, one engineer who writes both.
Pair DIP with A9 Liskov Substitution: the abstraction depends on the concrete adapter honouring its contract. Pair it with A10 Interface Segregation: the abstraction must be narrow enough to satisfy without leaking detail. Pair it with A5 Thin Handlers: the handler is a transport adapter and runs at the edge. Pair it with A2 Three-Tier Hoisting: the inward-pointing import direction the tier system codifies.
Copy a note and link
Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.
High-level policy doesn't depend on volatile detail; both hinge on a stable abstraction. Domain at the centre talks only to interfaces; concrete adapters sit at the edges. Apply where volatility lives — database, email, third-party API — and leave alone where it doesn't. /tenet/dependency-inversion-principle/A11
AI eyes only
Rule: depend on abstractions, not concretions. Volatile details are injected, never instantiated.
Reject: new ConcreteService() inside a constructor or function body. Reject: hard-coded URLs, file paths, or SDK clients. Reject: importing a database driver from a domain function.
Generate: constructor or function parameters typed as interfaces. The composition root wires the concrete implementation. Test code injects fakes against the same interface.
Diagnostic: if the unit cannot be tested without the real database, network, or filesystem, the dependency is concrete and must be inverted.
Why?
- Testability follows for free. The high-level code accepts its dependencies; tests pass fakes. A unit test runs in milliseconds because the expensive boundary is mocked at the abstraction.
- Adapters are replaceable when the vendor changes or the third-party service breaks. Switch from PostgreSQL to MySQL, from SendGrid to Resend, from REST to GraphQL — the domain layer doesn't notice.
- The domain layer reads as the business problem, not as the integration shim. The names in the inner code are the names the domain expert uses; the integration details live at the edges where they belong.
- Pairs with A2 Three-Tier Hoisting. Inward-pointing imports follow from inward-pointing dependencies; the tier system is DIP applied to the file tree.
- Faults are isolated. A vendor outage takes down one adapter; the domain layer runs against a degraded-mode fake or a circuit breaker. The blast radius is the boundary, not the application.
- CI cost drops. Tests don't need a real database, real email service or real third-party API to run. The slow tests stay at the integration layer; the fast tests cover the domain comprehensively.
- Plays well with Hexagonal, Onion and Clean Architecture. The architectural shape is the rule's consequence, not its cause; the rule is the dependency direction at the seams.
- Agents reach for the concrete by default. With DIP loaded, every constructor that takes a concrete service is flagged; the model writes the abstraction, the wiring, and the test double in one pass instead of three reviews.
Origins
Robert C. Martin's “The Dependency Inversion Principle,” C++ Report May/June 1996, gave the rule the name and the canonical statement. The two clauses appear in paragraph one of the article: “A. High-level modules should not depend upon low-level modules. Both should depend upon abstractions. B. Abstractions should not depend upon details. Details should depend upon abstractions.”1“The Dependency Inversion Principle,” C++ Report, May/June 1996. The two clauses appear in paragraph one. The canonical published statement of the rule. Martin justifies the word “inversion” on the same page: traditional structured-design methods produce systems where high-level modules depend on low-level modules; well-designed object-oriented systems invert that dependency. The rule landed in book form in Agile Software Development (2002) as the “D” of SOLID.
The architectural shape that follows from DIP appeared in three near-simultaneous formulations. Alistair Cockburn's Hexagonal Architecture (2005) names the ports-and-adapters pattern: the application's domain hexagon depends on no I/O; the adapters at the edges depend on the domain's ports.2Hexagonal Architecture (2005). Ports-and-adapters: the application's domain hexagon depends on no I/O; the adapters at the edges depend on the domain's ports. Jeffrey Palermo's Onion Architecture (2008) presents the same arrangement as concentric rings, with the core depending only on what is inside it.3“The Onion Architecture: part 1” (2008). The same arrangement as concentric rings, with the core depending only on what is inside it. Robert C. Martin's Clean Architecture (2017) restates both as the “Dependency Rule”: source code dependencies must point inward, toward the stable, abstract centre.4Clean Architecture (Pearson, 2017), Ch. 11 “The Dependency Inversion Principle.” The “Dependency Rule”: source code dependencies must point inward. Three names, one architecture; DIP is the rule that makes the arrows point the right way.
The European tradition reached the same place from contracts. Bertrand Meyer's Object-Oriented Software Construction (1988, 2nd ed. 1997) treats the supplier as providing a service to the client; the client depends on the contract, not the implementation. DIP is the dependency-graph expression of the same client-supplier thinking; the European pre-condition / post-condition tradition and the American interface / implementation tradition were always describing the same shape from two angles.
The practical follow-on is dependency injection. Martin Fowler's 2004 article Inversion of Control Containers and the Dependency Injection Pattern separated the principle from the technique: DIP is the design rule, DI is the wiring technique that satisfies it.7“Inversion of Control Containers and the Dependency Injection Pattern” (2004). Separates the principle (DIP) from the technique (DI). Spring, Guice, Dagger, NestJS, FastAPI, ASP.NET Core all ship DI as a first-class container concept; the rule that motivated them is the rule the SOLID literature named two decades earlier.
Quotes
High-level modules should not depend upon low-level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.
Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.
DI is about wiring, IoC is about direction, and DIP is about shape.
Static utility classes and singletons are inappropriate for classes whose behavior is parameterized by an underlying resource.
Evidence
Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.
- 01The original. Two clauses; the canonical published statement of the rule.
- 02Clean Architecture, Ch. 11SupportsThe Dependency Rule. Source code dependencies must point inward; the abstract centre is stable, the concrete edges are volatile.
- 03Hexagonal ArchitectureSupportsPorts and adapters. The application's domain hexagon depends on no I/O; adapters at the edges depend on the domain's ports.
- 04“The Onion Architecture, part 1”SupportsSame arrangement as concentric rings; the core depends only on what is inside it.
- 05Client-supplier contracts; the client depends on the contract, not the implementation. The European route to the same shape.
Sixteen sources spanning Martin's 1996 paper and the SOLID canon, plus the Hexagonal/Onion/Clean architecture lineage (Cockburn, Palermo) and Meyer's contract tradition. Practical examples from the Java/Python ecosystems sit further down. The qualifiers carry the over-engineering critique; the opposers carry the “DI is just wiring; DIP is overhead” reading from working teams who have shipped without it.
Examples
// Before: HedgehogService news up Postgres. Untestable.import { PostgresClient } from "pg-driver";export class HedgehogService { private readonly db: PostgresClient; constructor() { this.db = new PostgresClient({ url: process.env.DB_URL }); } rescue(h: Hedgehog): Promise<void> { return this.db.query("INSERT INTO hedgehogs VALUES ($1, $2)", [h.id, h.weight]); }}
// After: HedgehogService depends on a port. Postgres adapter sits at the edge.interface HedgehogStore { save(h: Hedgehog): Promise<void>;}export class HedgehogService { constructor(private readonly store: HedgehogStore) {} rescue(h: Hedgehog): Promise<void> { return this.store.save(h); }}export class PostgresHedgehogStore implements HedgehogStore { constructor(private readonly db: PostgresClient) {} save(h: Hedgehog): Promise<void> { return this.db.query("INSERT INTO hedgehogs VALUES ($1, $2)", [h.id, h.weight]); }}
// Before: HedgehogService news up Postgres. Untestable.import { PostgresClient } from "pg-driver";export class HedgehogService { private readonly db: PostgresClient; constructor() { this.db = new PostgresClient({ url: process.env.DB_URL }); } rescue(h: Hedgehog): Promise<void> { return this.db.query("INSERT INTO hedgehogs VALUES ($1, $2)", [h.id, h.weight]); }}
// After: HedgehogService depends on a port. Postgres adapter sits at the edge.interface HedgehogStore { save(h: Hedgehog): Promise<void>;}export class HedgehogService { constructor(private readonly store: HedgehogStore) {} rescue(h: Hedgehog): Promise<void> { return this.store.save(h); }}export class PostgresHedgehogStore implements HedgehogStore { constructor(private readonly db: PostgresClient) {} save(h: Hedgehog): Promise<void> { return this.db.query("INSERT INTO hedgehogs VALUES ($1, $2)", [h.id, h.weight]); }}
// Before: HedgehogService news up Postgres. Untestable.import { PostgresClient } from "pg-driver";export class HedgehogService { constructor() { this.db = new PostgresClient({ url: process.env.DB_URL }); } async rescue(h) { return this.db.query("INSERT INTO hedgehogs VALUES ($1, $2)", [h.id, h.weight]); }}
// After: HedgehogService depends on a port. Postgres adapter sits at the edge./** @typedef {{ save: (h: Hedgehog) => Promise<void> }} HedgehogStore */export class HedgehogService { constructor(store) { this.store = store; } async rescue(h) { return this.store.save(h); }}/** @implements {HedgehogStore} */export class PostgresHedgehogStore { constructor(db) { this.db = db; } async save(h) { return this.db.query("INSERT INTO hedgehogs VALUES ($1, $2)", [h.id, h.weight]); }}
# Before: HedgehogService news up Postgres. Untestable.from pg import PostgresClientclass HedgehogService: def __init__(self) -> None: self.db = PostgresClient(url=os.environ["DB_URL"]) async def rescue(self, h: Hedgehog) -> None: await self.db.query("INSERT INTO hedgehogs VALUES ($1, $2)", h.id, h.weight)
# After: HedgehogService depends on a port. Postgres adapter sits at the edge.from typing import Protocolclass HedgehogStore(Protocol): async def save(self, h: Hedgehog) -> None: ...class HedgehogService: def __init__(self, store: HedgehogStore) -> None: self.store = store async def rescue(self, h: Hedgehog) -> None: await self.store.save(h)class PostgresHedgehogStore(HedgehogStore): def __init__(self, db: PostgresClient) -> None: self.db = db async def save(self, h: Hedgehog) -> None: await self.db.query("INSERT INTO hedgehogs VALUES ($1, $2)", h.id, h.weight)
// Before: HedgehogService news up Postgres. Untestable.import com.prickles.pg.PostgresClient;public class HedgehogService { private final PostgresClient db = new PostgresClient(System.getenv("DB_URL")); public CompletableFuture<void> rescue(Hedgehog h) { return db.query("INSERT INTO hedgehogs VALUES ($1, $2)", h.id(), h.weight()); }}
// After: HedgehogService depends on a port. Postgres adapter sits at the edge.interface HedgehogStore { CompletableFuture<void> save(Hedgehog h);}public class HedgehogService { private final HedgehogStore store; public HedgehogService(HedgehogStore store) { this.store = store; } public CompletableFuture<void> rescue(Hedgehog h) { return store.save(h); }}public class PostgresHedgehogStore implements HedgehogStore { private final PostgresClient db; public PostgresHedgehogStore(PostgresClient db) { this.db = db; } @Override public CompletableFuture<void> save(Hedgehog h) { return db.query("INSERT INTO hedgehogs VALUES ($1, $2)", h.id(), h.weight()); }}
// Before: HedgehogService news up Postgres. Untestable.use App\Pg\PostgresClient;final class HedgehogService { public function __construct() { $this->db = new PostgresClient(getenv('DB_URL')); } public function rescue(Hedgehog $h): void { $this->db->query("INSERT INTO hedgehogs VALUES ($1, $2)", [$h->id, $h->weight]); }}
// After: HedgehogService depends on a port. Postgres adapter sits at the edge.interface HedgehogStore { public function save(Hedgehog $h): void;}final class HedgehogService { public function __construct(private readonly HedgehogStore $store) {} public function rescue(Hedgehog $h): void { $this->store->save($h); }}final class PostgresHedgehogStore implements HedgehogStore { public function __construct(private readonly PostgresClient $db) {} public function save(Hedgehog $h): void { $this->db->query("INSERT INTO hedgehogs VALUES ($1, $2)", [$h->id, $h->weight]); }}
Enforcement
Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| import/no-restricted-paths | eslint-plugin-import | domain code importing from infrastructure or adapters. The DIP violation at the file-tree level — the import direction is wrong. |
| no-restricted-syntax (new SmtpClient) | ESLint core | domain code instantiating concrete clients. The high-level module is depending on a low-level detail directly. |
| @typescript-eslint/no-explicit-any | typescript-eslint | `any` in interface declarations. Often the smell of a too-wide abstraction that does not actually invert anything. |
| @typescript-eslint/explicit-module-boundary-types | typescript-eslint | missing types on module-boundary functions. The boundary between layers must be explicit in the type system; implicit types are an unstated abstraction. |
| import/no-default-export (in adapters) | eslint-plugin-import | default exports in adapter files. Default exports hide which contract the adapter is implementing; named exports keep the abstraction explicit. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
plugins: { import: importPlugin },
rules: {
'import/no-restricted-paths': ['error', {
zones: [
{
target: './src/domain',
from: ['./src/infrastructure', './src/adapters'],
message: 'Domain must not import from infrastructure or adapters — DIP violation.',
},
],
}],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'error',
'no-restricted-syntax': ['error', {
selector: 'NewExpression[callee.name=/^(SmtpClient|HttpClient|MysqlClient|RedisClient)$/]',
message: 'Inject the abstraction; do not instantiate concrete clients in domain code.',
}],
}
});Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| import/no-restricted-paths | eslint-plugin-import | domain code importing from infrastructure or adapters. The DIP violation at the file-tree level — the import direction is wrong. |
| no-restricted-syntax (new SmtpClient) | ESLint core | domain code instantiating concrete clients. The high-level module is depending on a low-level detail directly. |
| @typescript-eslint/no-explicit-any | typescript-eslint | `any` in interface declarations. Often the smell of a too-wide abstraction that does not actually invert anything. |
| @typescript-eslint/explicit-module-boundary-types | typescript-eslint | missing types on module-boundary functions. The boundary between layers must be explicit in the type system; implicit types are an unstated abstraction. |
| import/no-default-export (in adapters) | eslint-plugin-import | default exports in adapter files. Default exports hide which contract the adapter is implementing; named exports keep the abstraction explicit. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
plugins: { import: importPlugin },
rules: {
'import/no-restricted-paths': ['error', {
zones: [
{
target: './src/domain',
from: ['./src/infrastructure', './src/adapters'],
message: 'Domain must not import from infrastructure or adapters — DIP violation.',
},
],
}],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'error',
'no-restricted-syntax': ['error', {
selector: 'NewExpression[callee.name=/^(SmtpClient|HttpClient|MysqlClient|RedisClient)$/]',
message: 'Inject the abstraction; do not instantiate concrete clients in domain code.',
}],
}
});Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| import/no-restricted-paths | eslint-plugin-import | domain code importing from infrastructure or adapters. Same shape as TypeScript; same DIP failure at the file tree. |
| no-restricted-syntax | ESLint core | instantiation of concrete clients in domain code. The high-level module reaching for a low-level detail. |
| import/no-cycle | eslint-plugin-import | cycles in the import graph. A cycle between domain and infrastructure means the dependency direction is non-monotonic; DIP is dead. |
| dependency-cruiser forbidden rules | dependency-cruiser | the same inward-pointing rule as a standalone CI step. Useful for non-ESLint surfaces (build scripts, scaffolds, monorepo boundary checks). |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
plugins: { import: importPlugin },
rules: {
'import/no-restricted-paths': ['error', {
zones: [
{
target: './src/domain',
from: ['./src/infrastructure', './src/adapters'],
message: 'Domain must not import from infrastructure or adapters — DIP violation.',
},
],
}],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'error',
'no-restricted-syntax': ['error', {
selector: 'NewExpression[callee.name=/^(SmtpClient|HttpClient|MysqlClient|RedisClient)$/]',
message: 'Inject the abstraction; do not instantiate concrete clients in domain code.',
}],
}
});Apply these rules in pyproject.toml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| forbidden contract | import-linter | domain code importing from infrastructure or adapters. The DIP violation at the package graph; cleanest expression for Python codebases. |
| layers contract | import-linter | violations of the inward-pointing layered architecture. Domain → application → infrastructure as a single ordered constraint. |
| Protocol classes (PEP 544) | Python typing module | wide concrete types where a structural Protocol would do. The Pythonic shape of DIP; mypy enforces the narrowing. |
| strict mode | mypy | missing types at module boundaries. The seam between layers must be typed; implicit `Any` defeats the inversion. |
| FastAPI Depends | FastAPI | hardcoded dependencies in route handlers. `Depends(get_repo)` injects the abstraction at the framework layer; the handler stays thin and testable. |
| C0415 (import-outside-toplevel) | Pylint | lazy imports inside functions — often a workaround for circular dependencies that DIP would have prevented. |
pyproject.tomlconfiguration snippet
[importlinter]
root_packages =
myapp
[importlinter:contract:domain-imports-only-protocols]
name = Domain layer must depend only on Protocol abstractions
type = forbidden
source_modules =
myapp.domain
forbidden_modules =
myapp.infrastructure
myapp.adapters
[tool.mypy]
strict = true
disallow_subclassing_any = trueApply these rules in src/test/java/com/example/archtests/DependencyInversionTest.java. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| onionArchitecture() | ArchUnit | violations of the concentric-rings DIP shape. ArchUnit's purpose-built rule for asserting domain ↔ application ↔ infrastructure boundaries. |
| noClasses().resideInAPackage()...dependOnClassesThat() | ArchUnit | domain code depending on infrastructure code. The general-purpose package-dependency assertion. |
| S2326 (Field should be declared final) | SonarQube Java | non-final injected dependencies. Constructor-injected fields should be final; mutability defeats the inversion. |
| S6068 (Excessive constructor parameters) | SonarQube Java | constructors taking too many dependencies. Often the symptom of DIP applied without ISP — the abstraction has too wide a surface. |
| AvoidInstantiatingObjectsInLoops | PMD | loop-bodies instantiating concrete services. Often the smell of a missing DI seam in tight code. |
| Spring @Autowired on constructors | Spring Framework | the standard DI surface in the Java ecosystem. The framework-side enforcement: constructor injection is the pattern; field injection is discouraged. |
src/test/java/com/example/archtests/DependencyInversionTest.javaconfiguration snippet
package com.example.archtests;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
@AnalyzeClasses(packages = "com.example", importOptions = ImportOption.DoNotIncludeTests.class)
public class DependencyInversionTest {
@ArchTest
static final ArchRule onion =
onionArchitecture()
.domainModels("..domain.model..")
.domainServices("..domain.service..")
.applicationServices("..application..")
.adapter("persistence", "..infrastructure.persistence..")
.adapter("rest", "..infrastructure.rest..");
@ArchTest
static final ArchRule domain_does_not_depend_on_infrastructure =
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAPackage("..infrastructure..");
}Apply these rules in deptrac.yaml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| ruleset (layer dependencies) | Deptrac | domain code importing from infrastructure or adapters. The cleanest expression of DIP for PHP codebases — equivalent to ArchUnit's rule for Java. |
| Architecture rules | PHPArkitect | namespace-level architectural assertions. Forbid Domain from depending on Infrastructure; require Application to depend only on Domain. |
| level: max | PHPStan | type leakage at module boundaries. Concrete types appearing where the contract was supposed to be abstract. |
| DependencyInjectionOverInjectionPattern (Symfony) | Symfony Framework | service definitions in the container. The framework-side enforcement; autowiring on constructors is the standard pattern. |
| Deprecated 'new' in domain (PHPMD CouplingBetweenObjects) | PHPMD | high coupling counts in domain classes. Often the symptom of `new ConcreteClient()` in domain code; PHPMD surfaces the count even when the rule itself is hard to articulate as a sniff. |
| Squiz.PHP.Heredoc | PHP_CodeSniffer | inline SQL or HTML in domain code. Often the marker of a missing infrastructure adapter — the domain is reaching for transport detail. |
deptrac.yamlconfiguration snippet
deptrac:
paths:
- ./src
layers:
- name: Domain
collectors:
- type: directory
regex: src/Domain/.*
- name: Application
collectors:
- type: directory
regex: src/Application/.*
- name: Infrastructure
collectors:
- type: directory
regex: src/Infrastructure/.*
ruleset:
Domain: ~
Application:
- Domain
Infrastructure:
- Domain
- ApplicationAI rules
.cursor/rules/a11-dependency-inversion.mdc---
description: Prickles A11 — Dependency Inversion Principle
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---
## Prickles A11 — Dependency Inversion Principle
Depend on abstractions. High-level policy doesn't depend on volatile detail; both hinge on a stable abstraction.
Concrete adapters sit at the edges. The domain at the centre talks only to interfaces; the integrations live where they belong — at the boundary.
Apply the rule where volatility lives. Database, email provider, third-party API — invert. Stable utilities, time, console output — leave alone unless tests need a fake.
Refuse a constructor that takes a concrete service when a contract would do. Refuse a `new SmtpClient()` in domain code. Refuse an import direction that crosses inward.Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.
Counter-argument
The strongest steelman is the over-engineering counter. John Ousterhout's A Philosophy of Software Design argues that excess interfaces are a cost: each abstraction is a layer of indirection a reader has to navigate, a name to remember, an extra file to maintain.5A Philosophy of Software Design (2018, 2nd ed. 2021). The over-engineering critique: pass-through methods and shallow abstractions are debt. “Pass-through” methods that just delegate to the next layer are the canonical anti-shape; an abstraction that exists only because “the SOLID book said so” is debt the team will pay for in onboarding time. The implication for DIP is sharp: the rule is right when the dependency is genuinely volatile, and over-applied when it isn't. Wrapping console.log in a Logger interface when nobody will ever swap it for a different logger is theatre.
Counter-argument retort
The over-engineering counter is correct on its own terms.5A Philosophy of Software Design (2018, 2nd ed. 2021). The over-engineering critique: pass-through methods and shallow abstractions are debt. An abstraction over a stable concrete is theatre. The rule should not be read as “wrap every dependency in an interface”; it should be read as “invert the dependency on volatile detail.” The discipline is naming what is volatile. Database technology is volatile (you might switch from PostgreSQL to MySQL). Email providers are volatile (Mandrill, SendGrid, Resend, depending on the year). Logging targets are volatile only if your operations team is changing observability stacks. Time is rarely volatile but is occasionally faked in tests, so a Clock abstraction earns its keep. Apply DIP where volatility lives; leave it off where the dependency genuinely isn't going to change.
The “DI is just wiring” objection — that DIP devolves into container configuration and yet another framework dependency — misreads the rule.7“Inversion of Control Containers and the Dependency Injection Pattern” (2004). Separates the principle (DIP) from the technique (DI). DIP is the design property; DI is one technique that satisfies it. Constructor injection without a container is enough; pure factory functions are enough; React's props pattern is dependency injection by another name. The rule is satisfied whenever a high-level module accepts its dependencies through its public surface rather than reaching for them itself. The container is a deployment convenience, not a definition.
The hardest objection is the simplicity-first reading from Rich Hickey, John Ousterhout, and the Go community: “most code doesn't need this”. The reply: most code that doesn't need DIP is also code that doesn't have a unit test, doesn't outlive its first vendor change, and doesn't survive a third-party outage. If your codebase genuinely doesn't need testability, replaceability or fault isolation, the rule doesn't apply — you have built a script, not a system. As soon as the script crosses into either category, the rule starts paying for itself.
The genuine residue is naming and granularity. DIP says depend on an abstraction; it doesn't say which abstraction. Pair it with A10 Interface Segregation — the abstraction must be narrow enough to satisfy without leaking detail; with F2 Intention-Revealing Names — the abstraction's name describes what the consumer needs; and with A2 Three-Tier Hoisting — the inward-pointing import direction the tier system codifies. The four together turn DIP from a slogan into a working practice.
Notes
- [1]Robert C. Martin — “The Dependency Inversion Principle,” C++ Report, May/June 1996. The two clauses appear in paragraph one. The canonical published statement of the rule.
- [2]Alistair Cockburn — Hexagonal Architecture (2005). Ports-and-adapters: the application's domain hexagon depends on no I/O; the adapters at the edges depend on the domain's ports.
- [3]Jeffrey Palermo — “The Onion Architecture: part 1” (2008). The same arrangement as concentric rings, with the core depending only on what is inside it.