skip to content

The Dependency Inversion Principle is not free. What criterion decides which dependencies are worth inverting, and where does over-applying it hurt an architecture?

level: principalimportance: nice to knowfreq 33%

answer

  1. volatility, not layer labels
  2. SDP: depend toward stability; SAP: stable ⇒ abstract
  3. costs: indirection, runtime wiring errors, wrong abstraction
  4. lowest-common-denominator port forfeits capability
  5. contract tests give ports real semantics

basics

~20 s

Invert where the dependency is volatile — external systems, vendors, frameworks, anything likely to change or vary. Leave stable things (value types, standard library, pure functions) alone. Too many abstractions add indirection, lowest-common-denominator interfaces, and runtime-only failures without buying substitutability.

solid answer

~50 s

The deciding criterion is **volatility**, not layer labels: invert dependencies on things that change for reasons independent of your policy — databases, vendor SDKs, transports, clocks, randomness, frameworks, other teams' modules. Depending directly on genuinely stable things (language primitives, well-established standard-library types, pure functions) is correct; wrapping them adds cost with no substitution ever occurring. This connects to the Stable Dependencies Principle (depend in the direction of stability) and the Stable Abstractions Principle (stable components should be abstract). Over-application hurts: each port is more code and indirection; wiring moves errors from compile time to runtime/startup; abstractions designed before a second implementation exists usually leak the first one; "lowest common denominator" ports forfeit real capabilities of the underlying technology (bulk operations, native query features, transactional semantics); and deep indirection makes stack traces and onboarding worse. The mature stance: invert at architectural boundaries deliberately, and depend directly inside a cohesive module.

go deeper

for a junior

Say DIP is valuable at boundaries like databases and external services, and that adding interfaces everywhere just adds files.

for a middle

Name the volatility criterion, give examples of stable vs volatile dependencies, and mention indirection and startup-time wiring errors as costs.

for a senior

Cover lowest-common-denominator ports, leaked semantics, contract tests, and concrete heuristics for where to place boundaries.

for a principal

Frame abstractions as bets on axes of change with real costs; tie to SDP/SAP, build/deploy and team boundaries, migration strategies, and the discipline of removing boundaries that never paid off.

## The criterion: volatility, not "level" People apply DIP by layer diagram ("domain must never touch infrastructure") and then find themselves wrapping `String`, `List`, and the date library. The sharper rule, stated by Robert Martin among others, is: **depend directly on things that are stable; invert dependencies on things that are volatile.** A dependency is *volatile* when any of these apply: - It changes for reasons unrelated to your business rules (vendor releases, framework upgrades, ops decisions). - It reaches outside the process: network, disk, database, message broker, external API. - It is non-deterministic: clock, random, UUID generation, environment. - It is not fully built/decided yet, or is owned by another team with its own release cadence. - Several variants are plausible: multi-cloud, multi-tenant, region-specific rules, a strangler migration. A dependency is *stable* when it changes rarely, changes compatibly, and would never realistically be substituted: primitives, collections, mature standard-library facilities, pure math, your own value objects. ## Sibling principles at component scale - **Stable Dependencies Principle (SDP)**: depend in the direction of stability. A component with many dependents is hard to change; it should depend on things even harder to change. - **Stable Abstractions Principle (SAP)**: a component that is stable (many dependents) should be *abstract*, so it can be extended without modification; a volatile component should be concrete. DIP is the class-scale mechanism by which SDP+SAP are achieved. Together they explain why an interface-only "contract" module can be depended on by everyone: it's maximally stable *and* maximally abstract, so it doesn't imprison anyone. ## The costs, itemized 1. **Indirection tax.** Every port adds a type, a file, and a navigation hop. At scale, comprehension and debugging slow down; a stack trace of proxies and adapters is materially harder to read. 2. **Compile-time → runtime error migration.** With wiring done by a container or config, a missing or ambiguous binding surfaces at startup (or worse, on first request) rather than at compile time. Mitigations: pure DI with a compile-checked composition root, compile-time DI (annotation-processor based), or startup self-checks that resolve the whole graph eagerly. 3. **Premature/wrong abstraction.** An interface designed against exactly one implementation almost always encodes that implementation's assumptions — its error model, its transaction semantics, its paging behaviour. The second implementation then doesn't fit and the abstraction is either mangled or bypassed. 4. **Lowest-common-denominator ports.** To keep a port implementable by every candidate technology you strip out what makes each good: bulk upserts, native full-text search, streaming, cursor paging, database-side aggregation. Result: a portable but slow system, portable to a database you will never adopt. Often the honest choice is to commit to a technology and depend on it directly in a well-contained module. 5. **Leaky semantics.** Failure modes, ordering guarantees, idempotency and transactionality are part of a contract but rarely written into an interface. Adapters silently differ, and the policy that was "decoupled" breaks when the adapter is swapped. If a port has semantics, they must be specified *and* covered by a shared contract test suite that every adapter runs. 6. **Organizational cost.** More modules means more build wiring, more review surface, more onboarding, sometimes cross-team negotiation for contract changes. ## The counter-costs of *not* inverting Don't over-rotate. Under-inversion produces: business logic that cannot be tested without infrastructure (slow, flaky suites), a core that recompiles on every driver bump, vendor lock-in that only reveals itself during a migration, and an inability to run the system locally or in CI without external services. Those are usually more expensive than a handful of ports. ## Practical heuristics used by experienced architects - **Invert at I/O boundaries by default.** Anything crossing the process boundary gets a port; the test fake alone repays it. - **Do not invert inside a cohesive module.** Classes that change together, ship together, and are owned by one team should just call each other. - **Prefer few, wide-value boundaries over many thin ones.** Boundaries are expensive; place them where change actually happens. - **Wait for the second implementation** for anything that is not clearly at a boundary; extraction later is a mechanical refactor. - **Write contract tests** for every port so all adapters are held to the same behaviour. - **Enforce direction mechanically** (architecture tests / module boundaries) rather than by convention. - **Reassess.** A dependency's volatility changes over time; a boundary that stopped paying rent can be collapsed. ## A useful framing An abstraction is a bet that a particular axis of change will occur. Placing many bets is not free; each one costs comprehension and constrains capability. Senior judgment is picking the few axes where change is likely or where the testability payoff is immediate — and being willing to remove a boundary that never paid off.

  • How do you keep a port's semantics — not just its signatures — consistent across adapters?
    Write a shared contract/verification test suite that every adapter must pass, covering the behaviours the policy relies on: uniqueness violations, not-found, ordering, idempotent retries, atomicity of a use-case operation. An interface constrains shape; only executable contract tests constrain behaviour. In-memory fakes must run the same suite, otherwise tests pass against a fake that no real adapter matches.
  • Isn't a database-agnostic repository port always worth having?
    Rarely, if the motivation is portability — teams almost never switch database engines, and the abstraction usually blocks the engine's best features. The stronger justifications are testability, keeping domain code free of persistence concerns, and enabling parallel development. Decide on those grounds, and allow the adapter to exploit the engine fully behind a use-case-shaped port.
  • How would you decide to remove an existing abstraction?
    Check whether it ever gained a second meaningful implementation, whether its signatures have drifted to mirror the one implementation, and whether consumers bypass it. If the axis of change it bet on never materialized and it isn't buying test isolation, inline it — the removal is safest while it still has a single implementation.

Standardized connectors in a building. Power sockets and water fittings are standardized because appliances change often; you do not put a swappable adapter between two welded steel beams. Standardization costs money and constrains what each side can do, so you spend it only where change is expected.

saying these in an interview costs you the question

  • Applying DIP by layer label rather than by volatility, ending up wrapping stable standard-library types.
  • "Abstractions are always good" — ignoring indirection, capability loss and runtime-wiring costs.
  • Designing a port against one implementation and assuming a second will fit.
  • Assuming an interface guarantees behavioural compatibility without contract tests.
  • Justifying every repository abstraction with a database swap that will never happen, while forfeiting the engine's real capabilities.
  • Treating boundaries as permanent — never revisiting one that stopped paying for itself.

context