Case file — A11

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.

ByAdam LewisPublished3 May 2026Reading12 minVersionv1.0ConfidenceHigh
§0b

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.6Robert C. MartinClean 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
§0c

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.

§0d

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.
The receipts
Origins, quoted passages, evidence, the strongest counter-argument and the reply.
§1

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.”1Robert 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. 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.2Alistair CockburnHexagonal 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.3Jeffrey Palermo“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.4Robert C. MartinClean 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.7Martin Fowler“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.

§2

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.

Robert C. Martin · The Dependency Inversion Principle (1996)

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.

Alistair Cockburn · Hexagonal Architecture (2005)

DI is about wiring, IoC is about direction, and DIP is about shape.

Brett Schuchert / Martin Fowler · DIP in the Wild (2013)

Static utility classes and singletons are inappropriate for classes whose behavior is parameterized by an underlying resource.

Joshua Bloch · Effective Java 3e (2017), Item 5
§3

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.

  1. 01
    Robert C. Martin · 1996
    The original. Two clauses; the canonical published statement of the rule.
  2. 02
    Robert C. Martin · 2017
    The Dependency Rule. Source code dependencies must point inward; the abstract centre is stable, the concrete edges are volatile.
  3. 03
    Alistair Cockburn · 2005
    Ports and adapters. The application's domain hexagon depends on no I/O; adapters at the edges depend on the domain's ports.
  4. 04
    Jeffrey Palermo · 2008
    Same arrangement as concentric rings; the core depends only on what is inside it.
  5. 05
    Bertrand Meyer · 1997
    Client-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.

§4

Examples

Viewing: TypeScript.
Avoid
Filehedgehog-service.ts
// 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]);  }}
Prefer
Filehedgehog-service.ts
// 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]);  }}
§4b

Enforcement

Viewing: TypeScript.

Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.

RuleToolCatches
import/no-restricted-pathseslint-plugin-importdomain 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 coredomain code instantiating concrete clients. The high-level module is depending on a low-level detail directly.
@typescript-eslint/no-explicit-anytypescript-eslint`any` in interface declarations. Often the smell of a too-wide abstraction that does not actually invert anything.
@typescript-eslint/explicit-module-boundary-typestypescript-eslintmissing 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-importdefault 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.',
    }],
  }
});
§4c

AI rules

File.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.

§5

Counter-argument

Counter

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.5John OusterhoutA 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.

§6

Counter-argument retort

Reply

The over-engineering counter is correct on its own terms.5John OusterhoutA 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.7Martin Fowler“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.

§7

Notes

  1. [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. [2]Alistair CockburnHexagonal 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. [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.
Disagree? Found a hole in the argument? Take issue with this tenet →
Last revised: 2026-04-27