skip to content

A high-level OrderService calls a low-level EmailSender. After applying the Dependency Inversion Principle, which arrows change and which stay the same? Explain the difference between direction of control flow and direction of source-code dependency.

level: middleimportance: must knowfreq 62%

answer

  1. two arrows: calls vs imports
  2. calls unchanged, imports flipped
  3. interface owned by the caller's package
  4. composition root does the wiring
  5. arrows disagree ⇒ boundary crossed

basics

~20 s

Control flow stays the same: OrderService still calls the email code at runtime. The source dependency flips: OrderService declares a Notifier interface, EmailSender implements it, so the low-level code now references the high-level module's abstraction rather than the reverse.

solid answer

~50 s

Two different arrows are involved. *Flow of control* is who calls whom while the program runs — that remains `OrderService → EmailSender` before and after; the business rule still triggers the email. *Source-code dependency* is who names whom at compile time — before, `OrderService` imports `EmailSender`; after, `OrderService` declares an abstraction (`Notifier`) it owns, `EmailSender` implements it and imports it, so the arrow points `EmailSender → Notifier`. Wherever those two arrows point in opposite directions you are looking at an architectural boundary, and DIP is the tool that creates one. The concrete class is bound at runtime by a composition root (or DI container) that constructs `OrderService(SmtpNotifier())`. Practical consequences: the policy package compiles and unit-tests without any mail library; the mail vendor can be swapped by adding a class and editing one wiring line; and change in the detail cannot ripple into the policy.

code

pseudocode · 13 lines
pseudocode
// runtime call:   OrderService  ---->  SmtpNotifier   (unchanged)
// source import:  SmtpNotifier  ---->  Notifier       (inverted)

package billing            // high-level, imports nothing infrastructural
interface Notifier { fun notify(to: Address, m: Message) }
class OrderService(val notifier: Notifier) { ... }

package infra
import billing.Notifier
class SmtpNotifier(val smtp: SmtpClient) : Notifier { ... }

package app                // composition root: the only omniscient module
main() { OrderService(SmtpNotifier(SmtpClient(cfg))) }

go deeper

for a junior

Say the call direction stays the same and the import direction flips because the interface sits with the caller.

for a middle

Draw both arrows, name constructor injection and a composition root, and mention that policy now unit-tests with a fake.

for a senior

Add ownership/placement of the interface, boundary-crossing data types, and how you'd enforce the import direction in the build.

for a principal

Generalize: opposing arrows define architectural boundaries; discuss build/deploy independence, plugin architecture, and where you deliberately do not create boundaries.

## Two arrows people conflate Every collaboration between two modules has at least two independent directions: | Arrow | Question it answers | When it exists | |---|---|---| | **Flow of control** | Who invokes whom? | Runtime | | **Source-code dependency** | Who *names* whom — imports, references the type, calls the concrete constructor? | Compile / build / read time | In a naive layered design both arrows point the same way, top-down. DIP deliberately makes them disagree. ## Before ``` [ billing package ] [ infra package ] OrderService ──import──▶ EmailSender OrderService ──calls───▶ EmailSender (runtime) ``` Consequences: - `billing` cannot compile without `infra` and its mail library. - A unit test of `OrderService` drags in an SMTP client (or must monkey-patch it). - Changing the mail vendor's API forces edits inside the business rules. - The build graph makes `billing` a *client* of infrastructure — the least stable thing in the system. ## After ``` [ billing package ] [ infra package ] OrderService ──uses──▶ «interface» Notifier ◀──implements/import── SmtpNotifier OrderService ────────────calls───────────────────────────────────▶ SmtpNotifier (runtime) ``` What changed: **only the import arrow**. What did not change: the runtime call, the fact that an email is sent, the sequence of operations. Crucially the interface `Notifier` lives in — and is owned by — the `billing` side. If you instead put `Notifier` inside `infra`, `billing` still imports `infra`, and nothing was inverted; you merely added indirection. ## Who picks the implementation? Something must still connect the two. The standard answer is a **composition root**: a single place at the program's entry point (a `main`, a module config, a container registration file) that knows all concrete types and wires them together. It is the *only* place allowed to depend on everything. Because it sits at the outermost edge, it does not pollute the dependency graph of the policy. Ways to inject: - **Constructor injection** — collaborator is required, object is always valid; the default choice. - **Setter/property injection** — for genuinely optional collaborators; risks half-built objects. - **Method/parameter injection** — pass the dependency per call when it varies per invocation. - **Service locator / global registry** — hides the dependency; generally an anti-pattern because it re-introduces a source dependency on the locator and makes requirements invisible. ## How to *see* the inversion in a real codebase - Look at the import list of the policy package. Zero references to vendors, drivers, frameworks → inverted. - Draw the module graph and count arrows crossing the boundary. All crossings should point inward, toward policy. - Try to delete the infrastructure module. If the policy module still compiles (and its tests still run with fakes), the inversion is real. ## Edge cases - **Data types crossing the boundary**: if `Notifier.notify(msg: MimeMessage)` uses a vendor type, the abstraction depends on a detail — clause two of DIP is violated even though an interface exists. Define your own boundary types. - **Callbacks / plugins**: a plugin architecture is DIP at the system scale — the host defines the plugin interface; plugins depend on the host, never the reverse. - **Language mechanics**: in dynamically typed languages there may be no explicit interface; the "abstraction" is the agreed duck-typed shape, and the inversion is still real as long as the policy module never names a concrete detail type or constructs it.

  • How would you verify in a real repository that the inversion actually holds?
    Inspect or enforce the import graph: the policy module must have no imports of infrastructure packages or vendor libraries. Tools like ArchUnit, dependency-cruiser, import-linter or a build-level module boundary can fail the build when an arrow points the wrong way. A practical smoke test: remove the infra module and check the policy module still compiles.
  • Doesn't the composition root violate DIP, since it depends on everything?
    No — that concentration is the point. Someone must know the concrete types; DIP just says it should be one outermost module that nothing else depends on, so its knowledge cannot ripple into policy.

saying these in an interview costs you the question

  • Saying the low-level module now calls the high-level one at runtime.
  • Defining the interface inside the infrastructure package and calling it inversion.
  • Letting the abstraction's signatures use vendor/framework types, which re-couples policy to the detail.
  • Using a global service locator or static singleton to "decouple", which hides the dependency instead of inverting it.
  • Believing a DI container is required — hand-wiring in main achieves the same inversion.

context