---
title: 'Dependency Injection'
source: 'https://academia.sh/en/courses/design-patterns/dependency-injection'
course: 'Design Patterns'
language: en
updated: '2026-08-23T07:01:14+00:00'
license: 'CC BY-SA 4.0'
---

# Dependency Injection

Moving the creation responsibility from the using code to the composition root: comparing a fee module that builds its own dependencies against a version that takes them at call time by direct dependency count and import closure size, counting the file edited when the discount policy changes, and showing that the closure does not disappear but moves.

The previous lesson took the instance-producing version of the registry through tests, but it
never said where the module using that registry gets the instance from. If a module calls
`newRegistry` inside its own body, every module builds its own registry; if it imports the
singleton version, global state comes back. The third option is to never build the object at
all.

**Dependency injection** is a mechanism already defined in the Application Architecture:
Routing, State and Data and Unit Testing and Test-Driven Development courses; it is not
redefined here. What this lesson asks is narrower: which numbers show the difference between a
module building its own dependency and one taking it from outside. The measures come from the
Design Principles course: a module's **direct dependency count** and its **import closure
size**.

## Provider Modules

Both versions use the same three pieces: tariff objects, a registry holding them, and a discount
policy. These pieces are shared between the two versions.

```js
// provider/tariff.mjs — two tariff objects
export const STANDARD = { name: "standard", baseFee: (s) => 3900 + Math.ceil(s.weight) * 900 };
export const EXPRESS = { name: "express", baseFee: (s) => 6400 + Math.ceil(s.weight) * 1500 };
```

```js
// provider/registry.mjs — tariff registry; produces an instance loaded with two tariffs
import { STANDARD, EXPRESS } from "./tariff.mjs";

export class TariffRegistry {
  #tariffs = new Map();

  register(tariffObject) {
    this.#tariffs.set(tariffObject.name, tariffObject);
    return this;
  }

  tariff(name) {
    const t = this.#tariffs.get(name);
    if (t === undefined) throw new RangeError(`unknown tariff: ${name}`);
    return t;
  }
}

export const newRegistry = () => new TariffRegistry().register(STANDARD).register(EXPRESS);
```

```js
// provider/discount.mjs — two discount policies
export const contractedDiscount = (shipment, raw) =>
  shipment.contractNumber === null ? 0 : Math.round(raw * 0.12);

export const noDiscount = () => 0;
```

## A Module That Builds Its Own Dependency

In the first version, the fee module imports everything it needs and builds the registry itself.
The caller sees only the `fee` function.

```js
// own/fee.mjs — imports its own dependencies and builds them itself
import { newRegistry } from "../provider/registry.mjs";
import { contractedDiscount } from "../provider/discount.mjs";

const ZONE = { "34": 100, "06": 115, "65": 140 };
const REGISTRY = newRegistry();

export function fee(shipment) {
  const baseFee = REGISTRY.tariff(shipment.tariff).baseFee(shipment);
  const raw = Math.round((baseFee * (ZONE[shipment.address.slice(0, 2)] ?? 160)) / 100);
  return raw - contractedDiscount(shipment, raw);
}
```

```js
// own/main.mjs — the caller imports only the fee function
import { fee } from "./fee.mjs";

export const EXAMPLES = [
  { tariff: "standard", weight: 2.4, address: "34100", contractNumber: "S1042" },
  { tariff: "express", weight: 2.4, address: "06500", contractNumber: null },
];

for (const s of EXAMPLES) console.log(`own     ${s.tariff.padEnd(9)} ${fee(s)} cents`);
```

```sh
node own/main.mjs
```

```
own     standard  5808 cents
own     express   12535 cents
```

This version's call site is the shortest of all: a single import and a single call. Its cost is
not at the call site; it is in whom the fee module is bound to.

## A Module That Takes Its Dependency at Call Time

In the second version, the fee module imports nothing. The registry and the discount policy are
given at call time; the module produces a calculating function.

```js
// injected/fee.mjs — takes dependencies at call time; imports no module
const ZONE = { "34": 100, "06": 115, "65": 140 };

export const makeFeeCalculator = ({ registry, discount }) => (shipment) => {
  const baseFee = registry.tariff(shipment.tariff).baseFee(shipment);
  const raw = Math.round((baseFee * (ZONE[shipment.address.slice(0, 2)] ?? 160)) / 100);
  return raw - discount(shipment, raw);
};
```

The wiring moves to the composition root. It builds the pieces, picks which discount policy to
use, and produces the calculator.

```js
// injected/main.mjs — composition root: builds the pieces, picks a policy, injects them
import { newRegistry } from "../provider/registry.mjs";
import { contractedDiscount, noDiscount } from "../provider/discount.mjs";
import { makeFeeCalculator } from "./fee.mjs";

const POLICY = { contracted: contractedDiscount, none: noDiscount };
const choice = process.argv[2] ?? "contracted";
const fee = makeFeeCalculator({ registry: newRegistry(), discount: POLICY[choice] });

export const EXAMPLES = [
  { tariff: "standard", weight: 2.4, address: "34100", contractNumber: "S1042" },
  { tariff: "express", weight: 2.4, address: "06500", contractNumber: null },
];

for (const s of EXAMPLES)
  console.log(`injected ${choice.padEnd(11)} ${s.tariff.padEnd(9)} ${fee(s)} cents`);
```

```sh
node injected/main.mjs
node injected/main.mjs none
```

```
injected contracted  standard  5808 cents
injected contracted  express   12535 cents
injected none        standard  6600 cents
injected none        express   12535 cents
```

The first two lines match the first version's output exactly. The last two show what happens
when the discount policy changes: the contracted shipment's fee rises from 5808 to 6600 cents,
and the shipment with no contract does not change. No file was edited between the two runs.

## Counting Dependencies and the Closure

The measurer below gives the direct import count of the given modules and their import
closure's size. The closure is the transitive set of every module a module reaches.

```js
// closure.mjs — direct dependency count and import closure size of the given modules
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";

const IMPORT = /(?:from|import)\s*["'](\.[^"']+)["']/g;
const deps = (path) => [...readFileSync(path, "utf8").matchAll(IMPORT)].map((m) => m[1]);

function closure(path, seen = new Set()) {
  for (const dep of deps(path)) {
    const target = resolve(dirname(path), dep);
    if (!seen.has(target)) {
      seen.add(target);
      closure(target, seen);
    }
  }
  return seen;
}

for (const path of process.argv.slice(2))
  console.log(`${path.padEnd(18)} direct-deps=${deps(path).length} ` +
    `import-closure=${closure(path).size}`);
```

```sh
node closure.mjs own/fee.mjs injected/fee.mjs own/main.mjs injected/main.mjs
```

```
own/fee.mjs        direct-deps=2 import-closure=3
injected/fee.mjs   direct-deps=0 import-closure=0
own/main.mjs       direct-deps=1 import-closure=4
injected/main.mjs  direct-deps=3 import-closure=4
```

The first two lines give the gain. The fee module's direct dependency count fell from 2 to 0,
and its import closure from 3 to 0. A closure of zero means no concrete name appears in the
module's text at all: the registry class's name, the tariff objects' names, and the discount
function's name are all absent from the fee module.

The last two lines say what the pattern actually does. Both versions' total closure is 4. The
closure did not disappear; it **moved**: in the first version it sat above the fee module, in
the second above the composition root, whose direct dependency count rose from 1 to 3. This is
exactly what moving the creation responsibility looks like in the measure — one module's gained
independence is another module's gained dependency count.

## The File Edited When Policy Changes

The second measure comes from this request: remove the discount policy. The script below copies
both trees, applies the request to each, runs the results, and counts the files edited. The
in-place edit is given a backup extension; GNU and BSD `sed` behave the same way here.

```sh
cp -r own own-no-discount
cp -r injected injected-no-discount
sed -i.y 's/contractedDiscount/noDiscount/g' own-no-discount/fee.mjs
rm -f own-no-discount/*.y
node own-no-discount/main.mjs
node injected-no-discount/main.mjs none

for p in own injected; do
  echo "$p edited file = $(diff -rq $p $p-no-discount | grep -c '^Files ')"
  echo "$p fee module changed = $(diff -q $p/fee.mjs $p-no-discount/fee.mjs > /dev/null && echo no || echo yes)"
done
```

```
own     standard  6600 cents
own     express   12535 cents
injected none        standard  6600 cents
injected none        express   12535 cents
own edited file = 1
own fee module changed = yes
injected edited file = 0
injected fee module changed = no
```

The first four lines show both trees produce the same fees; the `own` label in the first two
lines is fixed in the module's text, so it stays that way in the copy too. The last four lines
give the measure: in the first version, satisfying the request edited one file, and that file
was the fee module itself. In the second version, no file was edited; the policy choice was
already outside the composition root, a value given at run time.

This is the same split found in the Open–Closed Principle lesson, and the last line is the
deciding one: the module computing the fee changed in the first version, and did not change in
the second.

## Cost

The pattern's cost sits in three places. First, call-site length: getting a fee calculator is no
longer a single import but three imports plus a construction call. Second, the composition
root's direct dependency count rising from 1 to 3; the composition root is unavoidably the one
module that knows every concrete piece, and its growth is proportional to the component count.

The third is not readability but the number of calls that must be traced. In the first version,
which discount policy `fee` uses is written in the same file, on the import line. In the second,
that information is absent from the fee module; finding it means reading the composition root
and tracing which entry of the `POLICY` table got chosen. The answer to one question has spread
from one file to two.

This is why dependency injection is applied where a dependency is genuinely subject to change.
The zone table is left inside the fee module in both versions: it is a fixed table, does not
change during the process, and taking it from outside would not lower the closure — it would
only grow the composition root.

## Summary

- Dependency injection takes the creation responsibility away from the code using the object and
  moves it to the composition root; the pattern's question is not how the object gets built but
  who builds it.
- The fee module's direct dependency count fell from 2 to 0 and its import closure from 3 to 0;
  no concrete part's name appears in the module's text.
- Total closure is 4 in both versions: the closure did not disappear, it moved to the
  composition root, whose direct dependency count rose from 1 to 3.
- Removing the discount policy edited 1 file in the first version, and that file was the fee
  module; 0 files were edited in the second.
- The cost is a longer call site, a composition root that grows with component count, and a
  dependency's concrete identity only findable by reading the composition root.

## Next Step

The five mechanisms in this topic solved the question of who builds an object and how: gathering
type selection in one place, producing matching pieces together, sequencing a multi-step
construction, copying an existing object, and where to hold a single instance, and moving the
building itself to the composition root. All five shared one assumption: the object being built
was the library's own object, and the library wrote its interface too, so making the pieces fit
was something that could be arranged. The next topic addresses **how** objects get combined, and
its first problem sits exactly where that assumption fails: working with an interface whose
shape the library does not decide. Every outside carrier provider brings its own vocabulary, its
own units, and its own way of reporting errors; the next lesson takes up resolving that mismatch
in a single place, and measures its gain, again, by the number of files touched.
