skip to content

A very stable component must call code that lives in a highly volatile component, which would violate the Stable Dependencies Principle. How do you fix the dependency direction without deleting the call?

level: seniorimportance: must knowfreq 30%

answer

  1. DIP inverts the arrow you can't delete
  2. Interface named for the need, not the supplier
  3. Separate interface component: Ce=0, I=0
  4. Volatile implements, main wires it
  5. Control flow ≠ source dependency

basics

~20 s

Invert the dependency: define the needed operation as an interface owned by (or extracted next to) the stable side, have the volatile component implement it, and inject the implementation at runtime. The compile-time arrow now points from volatile to stable.

solid answer

~50 s

Apply the Dependency Inversion Principle at component scale. Instead of the stable component importing the volatile one, declare an abstract interface describing what the stable side needs. Put that interface either inside the stable component or, better, in a tiny separate component that contains nothing but abstractions — it has Ce = 0 and is purely abstract, so its instability is ~0 and it is safe for anyone to depend on. The volatile component now implements that interface, so its arrow points down toward the abstraction; the stable component depends only on the abstraction. Some third party — a main/composition root, a DI container, a plugin loader, a service registry — wires the concrete implementation in at runtime. Source-code dependencies now oppose the flow of control: control still goes stable → volatile at runtime, but compile-time dependencies go volatile → stable. That is precisely the trick that makes plugin architectures, ports-and-adapters and Clean Architecture work.

code

pseudocode · 17 lines
pseudocode
// component: report-output-api   (interfaces only; Ce = 0, I = 0)
interface ReportOutput {
    write(report: Report): void
}

// component: core-reporting     (stable, many dependents)
class ReportService(private out: ReportOutput) {
    fun run(data: Data) = out.write(buildReport(data))   // control flows outward...
}

// component: pdf-export         (volatile; depends DOWN on the api)
class PdfOutput : ReportOutput {
    override fun write(report: Report) { /* churns freely */ }
}

// component: main               (composition root; I = 1.0)
ReportService(PdfOutput()).run(data)   // ...but source dependencies flow inward

go deeper

for a junior

Say you'd introduce an interface the stable side depends on, have the volatile component implement it, and pass the implementation in — that is the core idea.

for a middle

Add the wiring layer (main / DI container) and show the before/after arrow diagram; note the interface must be named for the need.

for a senior

Distinguish source dependency from flow of control, argue for a separate interface component with Ce = 0, and name the alternatives (move code, split component, event-based inversion) plus the cost of indirection.

for a principal

Discuss ownership and release cadence: whoever owns the contract controls both sides' shipping. Talk about contract versioning, DTO placement, and enforcing the direction in build configuration.

### The problem shape Suppose `Stable` (many dependents, I ≈ 0.1) needs to invoke behaviour that genuinely belongs in `Flexible` (I ≈ 0.9) — a report renderer, a payment gateway, a notification channel, a UI theme. Drawing `Stable → Flexible` violates SDP: the volatile component is now frozen by a widely-depended-on dependent. Deleting the call is not an option; the behaviour is needed. ### The move: Dependency Inversion at component granularity **Dependency Inversion Principle (DIP):** depend on abstractions, not on concretions; high-level policy must not depend on low-level detail — both should depend on an abstraction. Applied between components rather than classes, it lets you *reverse an arrow you cannot delete*. Steps: 1. **Name what the stable side needs, in its own vocabulary.** Not `PdfRenderer` but `ReportOutput`; not `StripeClient` but `PaymentGateway`. The abstraction expresses the *requirement*, not the *supplier*. (This is the difference between a real inversion and a fake one that just renames the concretion.) 2. **Place the abstraction.** Two options: - *Inside the stable component.* Simple, zero extra artifacts. The stable component now owns the contract. - *In a dedicated interface component* (`report-output-api`, a "port" package). It contains only interfaces/abstract types and DTOs, has no outgoing dependencies (Ce = 0, so I = 0) and is highly abstract, making it a maximally safe dependency target. Preferred when several components on both sides share the contract, or when the stable component is itself large. 3. **Make the volatile component implement it.** `Flexible` now depends on the abstraction — arrow points from I ≈ 0.9 down to I ≈ 0.0. SDP satisfied. 4. **Wire it at runtime.** Something outside both — a `main`/composition root, a dependency-injection container, a service loader, a plugin registry, a factory chosen by configuration — instantiates the concrete class and hands it to the stable component as the interface type. The composition root is the *most unstable* component in the system by design: it depends on everything and nothing depends on it (I = 1). ### The key insight: flow of control vs. source dependency At runtime, control still flows `Stable → Flexible` (the stable policy calls the volatile detail). But the *source-code / compile-time* dependency now flows `Flexible → abstraction ← Stable`. DIP is the only mechanism that decouples those two directions. Every plugin system in existence is this trick: the application is stable, the plugins are volatile, and plugins depend on the application's SDK — never the reverse. ``` BEFORE (violates SDP) AFTER (satisfies SDP) Stable I=0.1 Stable I=0.1 | | v v Flexible I=0.9 ReportOutput (interface only, I=0) ^ | Flexible I=0.9 (implements it) ^ | Main I=1.0 (wires them together) ``` ### Cost and when not to do it - **Indirection has a price.** Every inversion adds an interface, a wiring point and a place where "go to definition" stops being useful. Do not invert every dependency reflexively — invert across boundaries you expect to change independently or that cross a volatility gradient. - **Only volatile dependencies need inverting.** Depending directly on a frozen platform type (a string type, a date type, a mature standard-library collection) is fine; those are already maximally stable. Wrapping the standard library "for purity" is cargo cult. - **A leaky abstraction inverts nothing.** If the interface mirrors one implementation's quirks (`ReportOutput.setPdfCompressionLevel(...)`), the stable side is still coupled to the detail; a second implementation will not fit. Test the abstraction by asking whether a plausible second implementation could satisfy it honestly. - **Watch the DTOs.** If the interface's parameter and return types live in the volatile component, you have not inverted anything — the types drag the dependency back. Data types crossing the boundary must live with the abstraction or with the stable side. ### Alternative fixes worth naming - **Move the code.** If the needed behaviour is actually stable and general, it may simply be in the wrong place; relocate it into the stable component or a shared stable one. Cheapest fix when it applies. - **Split the volatile component.** Often only a stable sliver is needed; extract that sliver into its own stable component and depend on it. - **Invert via events/messages.** The stable side publishes an event; the volatile side subscribes. Same direction reversal, asynchronous flavor; common between services. - **Accept it explicitly.** Sometimes the coupling is small, the volatility overstated, and the indirection not worth it — but say so on purpose, not by omission.

  • Where should the interface live — with the stable component or in its own component?
    Its own component when multiple components on either side share the contract, when you want the abstraction independently versioned, or when the stable component is big and you want a slim dependency target. Inside the stable component when it is the sole consumer — fewer artifacts, same direction, and you can always extract later.
  • Doesn't this just push the problem into the composition root, which now depends on everything?
    Yes, deliberately. The main/composition root is designed to be maximally unstable (I = 1): nothing depends on it, so its churn hurts nobody, and it is the one place allowed to know every concrete type. Concentrating the ugliness there is the point.
  • How do you know the abstraction is a real inversion and not a renamed concretion?
    Ask whether a genuinely different second implementation could satisfy it without contortions, and check that no parameter or return type comes from the volatile component. If the interface exposes the supplier's vocabulary or its data types, the dependency was renamed, not inverted.

A wall socket. The building's wiring is extremely stable and must not depend on any particular appliance; appliances change constantly. So the standard defines a plug shape, the appliance conforms to it, and the wiring depends on nothing but the socket standard. Power flows building → appliance, but the conformance dependency runs appliance → standard.

saying these in an interview costs you the question

  • Claiming the fix is "just add an interface" without inverting ownership — an interface still living in and owned by the volatile component changes nothing
  • Naming the interface after the implementation (IPdfRenderer for PdfRenderer)
  • Letting the interface's parameter/return types stay in the volatile component
  • Thinking DIP reverses the runtime flow of control (it reverses only source dependencies)
  • Inverting every dependency, including on frozen standard-library types
  • Forgetting that something (main/DI container/plugin loader) still must depend on both to wire them

context