---
title: 'Dependency Inversion'
source: 'https://academia.sh/en/courses/design-principles/dependency-inversion-principle'
course: 'Design Principles'
language: en
updated: '2026-08-23T07:01:17+00:00'
license: 'CC BY-SA 4.0'
---

# Dependency Inversion

Turning which side an abstraction is defined on into a number: counting the direction of the import edges between the rule and carrier packages across three versions, measuring the contract's owner and its number of incoming edges, and testing with a run whether the rule module can load without the carrier package.

The split contracts stood next to the clients, but that placement was a separate
decision. How many pieces an interface is split into and which package those pieces are
defined in are independent of each other: a small contract can also live next to the
implementation. In that case, the client binds to a small interface but still imports
the package where the implementation lives.

**Dependency inversion** is SOLID's fifth item, and it takes on exactly this question:
the high-level rule should bind not to the low-level implementation but to an
abstraction both sides bind to; and the abstraction should be something the rule
defines, not the implementation. The Data Access Layer and Business Logic course
audited this rule through the import graph between layers, and measured the violation
count and the size of the import closure. The question here is narrower and its measure
is different: which direction do the edges between the two packages point, and which
package does the contract file stand in?

## Three Placements

The codebase is split into two packages. `rule/` holds the fee rule, and `carrier/`
holds the carrier-specific base fee calculations. The same behavior is written with
three different placements.

```sh
mkdir -p direct/rule direct/carrier partial/rule partial/carrier
mkdir -p inverted/rule inverted/carrier
```

In the first placement, there is no abstraction at all; the rule directly imports the
two concrete modules and chooses between them itself.

```js
// direct/carrier/domestic.mjs — domestic carrier's base fee
export const domesticBase = (s) => (s.weight <= 1 ? 3900 : s.weight <= 5 ? 6400 : 11800);
```

```js
// direct/carrier/express.mjs — express carrier's base fee
export const expressBase = (s) => 5200 + Math.ceil(s.weight) * 900;
```

```js
// direct/rule/fee.mjs — high-level rule imports the two concrete carriers itself
import { domesticBase } from "../carrier/domestic.mjs";
import { expressBase } from "../carrier/express.mjs";

const ZONE = { "34": 100, "06": 115, "65": 140 };
const zoneFactor = (address) => ZONE[address.slice(0, 2)] ?? 160;

export function fee(shipment) {
  const base = shipment.carrier === "express" ? expressBase(shipment) : domesticBase(shipment);
  return Math.round((base * zoneFactor(shipment.address)) / 100);
}
```

In the second placement, there is a contract, but it is defined in the carrier package.

```js
// partial/carrier/contract.mjs — contract exists, but defined in the carrier package
export function validateCarrier(c) {
  if (typeof c?.base !== "function") throw new TypeError("carrier contract missing: base");
  return c;
}
```

```js
// partial/carrier/domestic.mjs — domestic carrier satisfying the contract
import { validateCarrier } from "./contract.mjs";

export const domestic = validateCarrier({
  name: "domestic",
  base: (s) => (s.weight <= 1 ? 3900 : s.weight <= 5 ? 6400 : 11800),
});
```

```js
// partial/carrier/express.mjs — express carrier satisfying the contract
import { validateCarrier } from "./contract.mjs";

export const express = validateCarrier({
  name: "express",
  base: (s) => 5200 + Math.ceil(s.weight) * 900,
});
```

```js
// partial/rule/fee.mjs — rule binds to the contract, but the contract lives in the carrier package
import { validateCarrier } from "../carrier/contract.mjs";

const ZONE = { "34": 100, "06": 115, "65": 140 };
const zoneFactor = (address) => ZONE[address.slice(0, 2)] ?? 160;

export function fee(carrier, shipment) {
  validateCarrier(carrier);
  return Math.round((carrier.base(shipment) * zoneFactor(shipment.address)) / 100);
}
```

In the third placement, the contract file is moved into the rule package exactly as it
is. The file's content does not change, only its location does.

```js
// inverted/rule/contract.mjs — contract defined in the rule package
export function validateCarrier(c) {
  if (typeof c?.base !== "function") throw new TypeError("carrier contract missing: base");
  return c;
}
```

```js
// inverted/rule/fee.mjs — rule imports only its own contract
import { validateCarrier } from "./contract.mjs";

const ZONE = { "34": 100, "06": 115, "65": 140 };
const zoneFactor = (address) => ZONE[address.slice(0, 2)] ?? 160;

export function fee(carrier, shipment) {
  validateCarrier(carrier);
  return Math.round((carrier.base(shipment) * zoneFactor(shipment.address)) / 100);
}
```

```js
// inverted/carrier/domestic.mjs — implementation, imports the rule's contract
import { validateCarrier } from "../rule/contract.mjs";

export const domestic = validateCarrier({
  name: "domestic",
  base: (s) => (s.weight <= 1 ? 3900 : s.weight <= 5 ? 6400 : 11800),
});
```

```js
// inverted/carrier/express.mjs — implementation, imports the rule's contract
import { validateCarrier } from "../rule/contract.mjs";

export const express = validateCarrier({
  name: "express",
  base: (s) => 5200 + Math.ceil(s.weight) * 900,
});
```

All three versions produce the same fee.

```js
// same-result.mjs — all three versions produce the same fee; the difference is not in behavior
const SHIPMENT = { carrier: "express", weight: 3.0, address: "06500" };

const { fee: directFee } = await import("./direct/rule/fee.mjs");
const { fee: partialFee } = await import("./partial/rule/fee.mjs");
const { express: partialExpress } = await import("./partial/carrier/express.mjs");
const { fee: invertedFee } = await import("./inverted/rule/fee.mjs");
const { express: invertedExpress } = await import("./inverted/carrier/express.mjs");

console.log("direct   =", directFee(SHIPMENT), "cents");
console.log("partial  =", partialFee(partialExpress, SHIPMENT), "cents");
console.log("inverted =", invertedFee(invertedExpress, SHIPMENT), "cents");
```

```sh
node same-result.mjs
```

```
direct   = 9085 cents
partial  = 9085 cents
inverted = 9085 cents
```

## Counting the Edges by Direction

The difference is visible only in the import graph. The script below reads every module
in both packages, resolves the relative import paths, and counts the edges that cross
the package boundary by direction. Its second measure concerns the abstraction itself:
which package does the contract file stand in, and how many modules bind to it?

```js
// direction-measure.mjs — counts the import edges between the two packages and the abstraction's owner
import { readdirSync, readFileSync } from "node:fs";
import { join, relative, resolve, dirname } from "node:path";

const IMPORT = /^\s*(?:import|export)[^;'"]*from\s+["'](\.[^"']+)["']/gm;
const pkg = (path) => path.split("/")[0];
const root = process.argv[2];
const edges = [];

for (const p of ["rule", "carrier"]) {
  for (const name of readdirSync(join(root, p)).filter((a) => a.endsWith(".mjs")).sort()) {
    const source = `${p}/${name}`;
    for (const m of readFileSync(join(root, source), "utf8").matchAll(IMPORT)) {
      edges.push([source, relative(root, resolve(root, dirname(source), m[1]))]);
    }
  }
}

const outward = edges.filter(([a, b]) => pkg(a) !== pkg(b));
for (const [a, b] of outward) console.log(`  ${a} -> ${b}`);
const count = (p) => outward.filter(([a]) => pkg(a) === p).length;
console.log(`${root.padEnd(9)} rule -> carrier = ${count("rule")}` +
  `   carrier -> rule = ${count("carrier")}`);

const abstractions = [...new Set(edges.map(([, b]) => b))].filter((b) => b.endsWith("contract.mjs"));
if (abstractions.length === 0) {
  console.log(`${root.padEnd(9)} no abstraction defined`);
}
for (const a of abstractions) {
  const incoming = edges.filter(([, b]) => b === a).length;
  console.log(`${root.padEnd(9)} abstraction owner = ${pkg(a)}, incoming edges = ${incoming}`);
}
```

```sh
node direction-measure.mjs direct
node direction-measure.mjs partial
node direction-measure.mjs inverted
```

```
  rule/fee.mjs -> carrier/domestic.mjs
  rule/fee.mjs -> carrier/express.mjs
direct    rule -> carrier = 2   carrier -> rule = 0
direct    no abstraction defined
  rule/fee.mjs -> carrier/contract.mjs
partial   rule -> carrier = 1   carrier -> rule = 0
partial   abstraction owner = carrier, incoming edges = 3
  carrier/domestic.mjs -> rule/contract.mjs
  carrier/express.mjs -> rule/contract.mjs
inverted  rule -> carrier = 0   carrier -> rule = 2
inverted  abstraction owner = rule, incoming edges = 3
```

The three lines separate three distinct situations. The first version has two edges
from the rule to the implementation and no abstraction. The second version has an
abstraction, with three incoming edges — meaning the contract genuinely does its job —
but the number of boundary-crossing edges dropped from two to one, not to zero: the rule
still imports the carrier package. The third version has the same contract, with the
same three incoming edges, standing in the rule package, and every boundary-crossing
edge points in the reverse direction.

The second version is the principle's most common half-implementation. It is not
enough for the abstraction to exist; **who defines it** is the measure's real
determinant.

## The Rule Without an Implementation

Edge direction looks like an abstract measure; its payoff is concrete. From each
version, a copy containing only the rule package is extracted, and an attempt is made to
load the rule module on its own.

```js
// try-rule.mjs — can the rule module load without the carrier package
const root = process.argv[2];
try {
  const m = await import(`./${root}/rule/fee.mjs`);
  console.log(`${root.padEnd(16)} -> loaded, exports: ${Object.keys(m).join(", ")}`);
} catch (e) {
  console.log(`${root.padEnd(16)} -> failed to load, ${e.code}`);
}
```

```sh
for k in direct partial inverted; do
  mkdir -p "$k-only"
  cp -r "$k/rule" "$k-only/rule"
  node try-rule.mjs "$k-only"
done
```

```
direct-only      -> failed to load, ERR_MODULE_NOT_FOUND
partial-only     -> failed to load, ERR_MODULE_NOT_FOUND
inverted-only    -> loaded, exports: fee
```

The second version, which has a contract, also failed to load. Whether the rule can be
resolved independently of the implementation depends not on the contract's existence
but on the contract being defined on the rule's side. This is the measure's numeric
payoff: the rule cannot stand on its own until the number of edges going from the rule
to the implementation is zero.

## The Limit and Cost of the Principle

In the third version, the fee module does not know which carrier it will work with; the
carrier is given to it from outside. This is the **dependency injection** built in the
Application Architecture: Routing, State and Data and Unit Testing and Test-Driven
Development courses, and it appears here as a consequence of the principle: a module
that does not construct its own dependency has to get it from somewhere. That place is
the composition root.

The cost of inverting can also be counted: in the third version, one extra contract
file stands between the two packages, and the carrier choice is now outside the fee
module instead of inside it. Where the implementation is known to be singular and
unchanging, this surplus has no payoff. The principle does not ask for every dependency
to be inverted; it asks for the side that changes faster to bind to the side that
changes slower. If the carrier list changes more often than the fee rule, the direction
is inverted; if the reverse is true, the measurement says the reverse too.

## Summary

- Dependency inversion asks for the high-level rule to bind to an abstraction rather
  than the implementation, and for that abstraction to be defined by the rule.
- The measure is the direction of the import edges that cross the package boundary: 2,
  1, and 0 edges were counted from the rule to the carrier; 0, 0, and 2 edges were
  counted from the carrier to the rule.
- The abstraction's existence is not enough: in the version whose contract stood in the
  carrier package, the rule still imported the carrier package even though the number of
  incoming edges was 3.
- The payoff of edge direction was seen with a run: without the carrier package, only
  the third version's rule module loaded; the first two gave `ERR_MODULE_NOT_FOUND`.
- The principle inverts not every dependency but the dependencies that point at the
  faster-changing side; the contract file and the outside-injection apparatus are its
  cost.

## Next Step

The five principles said **whom** a unit binds to: how many actors, which axis of
extension, which contract, how large an interface, which side the abstraction is on.
None of them tied the bond itself to a measure. How is the strength of the bond between
two modules counted — the number of shared names, the number of parameters passed,
state changed in common? How is it understood whether the parts inside a module truly
belong to each other? The next topic builds these two measures and turns the judgments
the principles have so far left qualitative into numbers.
