Skip to content

Dependency Inversion

A structural refactoring method — instantiates Behavior-Preserving Refactoring

Refactors a rigid dependency by making both sides depend on a stable abstraction, so a volatile detail can be swapped or restructured without disturbing the code that used it.

Dependency Inversion targets one specific structural pathology — a high-level policy hard-wired to a low-level detail — and fixes it by introducing a stable abstraction that both sides depend on, flipping the direction of the dependency. Unlike a broad "clean this up," it is a named, surgical transformation with a known before-and-after shape: the detail stops being something the policy reaches down to, and becomes an implementation of an abstraction the policy owns. Crucially, it changes only internal structure — the abstraction is internal and the observable behavior is untouched; a caller that used to get a result still gets exactly the same result across the new seam.

Example

An order service calls a specific SMS vendor's SDK directly, everywhere it needs to notify a customer. Every notify site imports the vendor library, so the service can't be exercised in a test without the vendor, and switching providers would mean editing dozens of call sites. Dependency Inversion: define a small Notifier interface — send(message, recipient) — expressed in the order service's own terms and owned by the order service; make the vendor SDK an adapter that implements it; and point every call site at the interface instead of the vendor. Now the high-level order logic depends on its own abstraction, and the vendor detail depends on that same abstraction — the dependency has been inverted. The vendor becomes swappable and mockable, coupling drops sharply, and the customer still receives the identical message; observable behavior never moved.

How it works

  • Locate the rigid dependency. Find where high-level policy is bound directly to a volatile low-level detail.
  • Introduce an abstraction owned by the high-level side. Express the interface in the policy's language, not the detail's.
  • Make both sides depend on it. The detail becomes an adapter implementing the abstraction; every call site points at the abstraction.
  • Preserve behavior across the seam. The adapter reproduces exactly what the direct call did, so nothing observable changes.

Tuning parameters

  • Abstraction breadth — a narrow interface for one detail versus a general port for a whole category. Broader decouples more but risks a leaky, speculative abstraction.
  • Ownership placement — which module owns the interface. It should sit on the consumer/policy side; getting this backwards silently re-couples them.
  • Number of implementations — invert for a single swap versus designing for many adapters. More generality costs simplicity now for flexibility later.
  • Seam granularity — how much detail hides behind the boundary. Too much makes a god-interface; too little leaves the coupling in place.

When it helps, and when it misleads

Its strength is converting a hard-wired, untestable coupling into a swappable, mockable seam without changing behavior — it is the enabling move behind testability, vendor independence, and isolating a churn hotspot. It is a codified principle, the "D" of the SOLID set, and the same structure underlies ports-and-adapters (hexagonal) architecture.[1]

Over-applied, it breeds needless indirection: interfaces with a single implementation that add a layer and hide the real call, making code harder to follow rather than easier. The abstraction can also leak the very detail it was meant to hide — a Notifier that exposes SMS-specific fields is not really inverted. The classic misuse is adding interfaces reflexively to look "clean" instead of to solve a demonstrated coupling or testing pain. The discipline is to invert where there is a real need — a swap, a test seam, a change hotspot — and to keep the abstraction expressed in the consumer's language.

How it implements the components

The internal-restructuring side of the archetype — the components a structural method operates:

  • internal_restructuring_scope — it defines and executes a specific internal change: re-pointing dependencies at an owned abstraction.
  • structural_quality_target — its aim is a named structural improvement: decoupling, inverted dependency direction, and testability.
  • current_architecture_baseline — it reads the current coupling map to find the rigid dependency it will invert.

It restructures internals but does not verify or record: the compatibility_check and behavior_preservation_test that prove the seam behavior-neutral are the Backward Compatibility Test and Compatibility Test Suite, mechanizing the edit across call sites is the Automated Refactoring Tool, and the change_rationale_log is the Architecture Decision Record.

Notes

Dependency inversion (which way the dependency points) is routinely confused with dependency injection (how an implementation is handed to an object). They often appear together but are separable — you can inject without inverting and invert without injecting. This mechanism is about the direction: pointing high-level code at an abstraction it owns. Because the seam is exactly where a mistake would silently change behavior, perform the inversion under green tests: an adapter that fails to perfectly reproduce the original direct call is the one subtle way this "behavior-preserving" move stops preserving behavior.

References

[1] The Dependency Inversion Principle — high-level modules should depend on abstractions, not on low-level modules — is the "D" of the SOLID principles; the same shape underlies hexagonal (ports-and-adapters) architecture. Used correctly here as the transformation's target structure.