---
title: 'Law of Demeter'
source: 'https://academia.sh/en/courses/design-principles/law-of-demeter'
course: 'Design Principles'
language: en
updated: '2026-08-23T07:01:17+00:00'
license: 'CC BY-SA 4.0'
---

# Law of Demeter

Limiting chained access: writing a scan that counts access chain depth, comparing the finding count in two designs that produce the same five outputs, and measuring how many files get edited when the address record is restructured.

The cohesion measure looked at whether a module touches its own names. Once modules start
passing objects to each other, a new question appears: when a function reaches through an
object it received, into a second object, and from there into a third field, how many
separate structures does it have to know about? This dependency shows up neither in the
import graph nor in the cohesion measure, because there is no new import and no new
module-level name involved.

The **law of Demeter** limits this reach. A function should call methods only on: its own
object, the parameters it received, the objects it constructs itself, and its direct parts.
It is summarized as "talk to your neighbor, not your neighbor's neighbor." Its measurable
counterpart is the depth of the access chain: `shipment.code` is one step,
`shipment.recipient.address.province` is three steps, and it creates a dependency on the
shape of the two structures in between.

## The Chained Design

The record is three levels deep, and five separate clients reach into it.

```sh
mkdir -p chained short
```

```js
// chained/record.mjs — nested shipment record
export const RECORD = {
  code: "GN-000001",
  recipient: { name: "A. Yilmaz", address: { province: "Ankara", district: "Cankaya", postalCode: "06500" } },
  tariff: { name: "weight", tier: { max: 5, fee: 6400 } },
};
```

```js
// chained/label.mjs — reaches two steps into the recipient record
export const label = (shipment) =>
  `${shipment.recipient.name} / ${shipment.recipient.address.province} ${shipment.recipient.address.postalCode}`;
```

```js
// chained/fee.mjs — reaches into the tariff's tier and into the address
const ZONE = { "34": 100, "06": 115, "65": 140 };

export const fee = (shipment) =>
  Math.round((shipment.tariff.tier.fee *
    (ZONE[shipment.recipient.address.postalCode.slice(0, 2)] ?? 160)) / 100);
```

```js
// chained/report.mjs — reads province and district separately
export const report = (shipment) =>
  `${shipment.code} ${shipment.recipient.address.province}/${shipment.recipient.address.district} ${shipment.tariff.name}`;
```

```js
// chained/notification.mjs — the notification text walks the same chain
export const notification = (shipment) =>
  `${shipment.code} departed for ${shipment.recipient.address.province}`;
```

```js
// chained/branch.mjs — delivery branch selection
export const branch = (shipment) =>
  `${shipment.recipient.address.province}-${shipment.recipient.address.district} branch`;
```

```js
// chained/setup.mjs — the output of five clients
import { RECORD } from "./record.mjs";
import { label } from "./label.mjs";
import { fee } from "./fee.mjs";
import { report } from "./report.mjs";
import { notification } from "./notification.mjs";
import { branch } from "./branch.mjs";

for (const f of [label, fee, report, notification, branch]) console.log(f(RECORD));
```

```sh
node chained/setup.mjs
```

```
A. Yilmaz / Ankara 06500
7360
GN-000001 Ankara/Cankaya weight
GN-000001 departed for Ankara
Ankara-Cankaya branch
```

## The Chain Scan

The script extracts the access chains in each file, counts their depth, and reports the ones
longer than two steps as findings. Language and runtime objects (such as `Math`, `JSON`,
`console`) do not count as a field chain; the composition root, `setup.mjs`, is also outside
the scan.

```js
// chain-scan.mjs — counts access chain depth; every chain longer than two steps is a finding
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";

const CHAIN = /\b[a-zA-Z_]\w*(?:\.\w+)+/g;
// Language and runtime objects do not count as a field chain.
const EXCLUDED = ["Math", "Object", "String", "Number", "JSON", "console", "process", "Array"];

const root = process.argv[2];
let totalFindings = 0;
let deepest = 0;

for (const d of readdirSync(root).filter((a) => a.endsWith(".mjs")).sort()) {
  if (d === "setup.mjs") continue;
  const text = readFileSync(join(root, d), "utf8").replace(/^\/\/.*$/gm, "");
  const chains = [...text.matchAll(CHAIN)]
    .map((m) => m[0])
    .filter((z) => !EXCLUDED.includes(z.split(".")[0]));
  const depths = chains.map((z) => z.split(".").length - 1);
  const findings = chains.filter((z) => z.split(".").length - 1 >= 2);
  const localDeepest = depths.length === 0 ? 0 : Math.max(...depths);
  totalFindings += findings.length;
  deepest = Math.max(deepest, localDeepest);
  console.log(`  ${d.padEnd(14)} chains=${chains.length} deepest=${localDeepest}` +
    ` findings=${findings.length}${findings.length > 0 ? "  " + findings.join(" ") : ""}`);
}
console.log(`${root.padEnd(9)} total findings=${totalFindings} deepest chain=${deepest}`);
```

```sh
node chain-scan.mjs chained
```

```
  branch.mjs     chains=2 deepest=3 findings=2  shipment.recipient.address.province shipment.recipient.address.district
  fee.mjs        chains=2 deepest=4 findings=2  shipment.tariff.tier.fee shipment.recipient.address.postalCode.slice
  label.mjs      chains=3 deepest=3 findings=3  shipment.recipient.name shipment.recipient.address.province shipment.recipient.address.postalCode
  notification.mjs chains=2 deepest=3 findings=1  shipment.recipient.address.province
  record.mjs     chains=0 deepest=0 findings=0
  report.mjs     chains=4 deepest=3 findings=3  shipment.recipient.address.province shipment.recipient.address.district shipment.tariff.name
chained   total findings=11 deepest chain=4
```

Eleven findings, with the deepest chain at four steps. All five clients know the address
record's field names.

## Shortening the Chain

The way to shorten the chain is to give the intermediate objects behavior. Only one module
knows the shape of the address record; the rest ask it for a result.

```js
// short/record.mjs — the same nested record
export const RECORD = {
  code: "GN-000001",
  recipient: { name: "A. Yilmaz", address: { province: "Ankara", district: "Cankaya", postalCode: "06500" } },
  tariff: { name: "weight", tier: { max: 5, fee: 6400 } },
};
```

```js
// short/address.mjs — only this module knows the shape of the address record
export const address = (record) => ({
  format: () => `${record.province} ${record.postalCode}`,
  zoneCode: () => record.postalCode.slice(0, 2),
  provinceDistrict: () => `${record.province}/${record.district}`,
  province: () => record.province,
});
```

```js
// short/shipment.mjs — shipment asks its neighbor and hands back the result under its own name
import { address } from "./address.mjs";

export const shipment = (record) => {
  const a = address(record.recipient.address);
  return {
    code: record.code,
    labelLine: () => `${record.recipient.name} / ${a.format()}`,
    zoneCode: () => a.zoneCode(),
    provinceDistrict: () => a.provinceDistrict(),
    province: () => a.province(),
    tariffName: () => record.tariff.name,
    tierFee: () => record.tariff.tier.fee,
  };
};
```

```js
// short/label.mjs — one step
export const label = (shipment) => shipment.labelLine();
```

```js
// short/fee.mjs — one step
const ZONE = { "34": 100, "06": 115, "65": 140 };

export const fee = (shipment) =>
  Math.round((shipment.tierFee() * (ZONE[shipment.zoneCode()] ?? 160)) / 100);
```

```js
// short/report.mjs — one step
export const report = (shipment) => `${shipment.code} ${shipment.provinceDistrict()} ${shipment.tariffName()}`;
```

```js
// short/notification.mjs — one step
export const notification = (shipment) => `${shipment.code} departed for ${shipment.province()}`;
```

```js
// short/branch.mjs — one step
export const branch = (shipment) => `${shipment.provinceDistrict().replace("/", "-")} branch`;
```

```js
// short/setup.mjs — the same five outputs
import { RECORD } from "./record.mjs";
import { shipment } from "./shipment.mjs";
import { label } from "./label.mjs";
import { fee } from "./fee.mjs";
import { report } from "./report.mjs";
import { notification } from "./notification.mjs";
import { branch } from "./branch.mjs";

const s = shipment(RECORD);
for (const f of [label, fee, report, notification, branch]) console.log(f(s));
```

```sh
node short/setup.mjs
node chain-scan.mjs short
```

```
A. Yilmaz / Ankara 06500
7360
GN-000001 Ankara/Cankaya weight
GN-000001 departed for Ankara
Ankara-Cankaya branch
  address.mjs    chains=6 deepest=2 findings=1  record.postalCode.slice
  branch.mjs     chains=1 deepest=1 findings=0
  fee.mjs        chains=2 deepest=1 findings=0
  label.mjs      chains=1 deepest=1 findings=0
  notification.mjs chains=2 deepest=1 findings=0
  record.mjs     chains=0 deepest=0 findings=0
  report.mjs     chains=3 deepest=1 findings=0
  shipment.mjs   chains=10 deepest=3 findings=4  record.recipient.address record.recipient.name record.tariff.name record.tariff.tier.fee
short     total findings=5 deepest chain=3
```

The same five outputs, but five findings instead of eleven. More importantly, look at how
the findings are distributed: all five client modules dropped to zero findings, and the
remaining findings gathered in the two modules that resolve the structure. The law of
Demeter does not eliminate knowledge of the structure; it reduces **the number of modules
that know it**.

## The Files a Change Touches

The measure's counterpart shows up in a structural change. The address record is being
reorganized: the `province` and `district` fields move into a nested record named
`location`. `postalCode` stays where it is. The in-place edit is given a backup extension;
GNU and BSD `sed` behave the same way with this form.

```sh
cp -r chained chained-new
cp -r short short-new
NEW='{ location: { province: "Ankara", district: "Cankaya" }, postalCode: "06500" }'
OLD='{ province: "Ankara", district: "Cankaya", postalCode: "06500" }'
sed -i.y "s#$OLD#$NEW#" chained-new/record.mjs short-new/record.mjs
sed -i.y -e 's/\.address\.province/.address.location.province/g' -e 's/\.address\.district/.address.location.district/g' chained-new/*.mjs
sed -i.y -e 's/record\.province/record.location.province/g' -e 's/record\.district/record.location.district/g' short-new/address.mjs
rm -f chained-new/*.y short-new/*.y
node chained-new/setup.mjs
node short-new/setup.mjs
for k in chained short; do
  echo "$k: edited files = $(diff -rq $k $k-new | grep -c '^Files')  ->" \
    "$(diff -rq $k $k-new | sed 's#.*/\([a-z]*\.mjs\) .*#\1#' | tr '\n' ' ')"
done
```

```
A. Yilmaz / Ankara 06500
7360
GN-000001 Ankara/Cankaya weight
GN-000001 departed for Ankara
Ankara-Cankaya branch
A. Yilmaz / Ankara 06500
7360
GN-000001 Ankara/Cankaya weight
GN-000001 departed for Ankara
Ankara-Cankaya branch
chained: edited files = 5  -> branch.mjs label.mjs notification.mjs record.mjs report.mjs
short: edited files = 2  -> address.mjs record.mjs
```

Both versions keep producing the same five lines. The difference is in cost: the same
structural change required editing five files in the chained design and two files in the
short design. In the chained version, the fee module was the only one left untouched, because
it used only the `postalCode` field — which module gets affected, in other words, depended on
which field name that module reached into.

## Misapplying the Law

The wrong way to shorten the chain is to write a delegating method for every intermediate
field. An interface that grows as `shipment.recipientName()`, `shipment.recipientProvince()`,
`shipment.recipientDistrict()`, `shipment.recipientPostalCode()` shortens the chain, but it
copies the same information into method names instead; the interface grows as fields are
added, and the interface bloat measured in the previous lesson comes back. In the short
version above, the `address` object does not return raw fields — it answers the question
being asked: `format`, `zoneCode`, `provinceDistrict`. These are the names of the clients'
questions, not the names of the fields.

The law also has an exception: in records that exist purely to carry data — a serialized
configuration object, or a parsed body at a boundary point — the chain is unavoidable and its
cost is low, because that record is already an external contract. The measure is not whether
a chain exists, but how many modules separately know the same structure.

## Summary

- The law of Demeter limits the objects a function can reach to its own parts, its
  parameters, and the objects it constructs; its measure is the step count of the access
  chain.
- In the design where five clients reach directly into the record, 11 findings and a chain
  as deep as 4 steps were counted.
- Once the intermediate objects were given behavior, the finding count dropped to 5, and all
  five client modules dropped to zero findings; the remaining findings gathered in the two
  modules that resolve the structure.
- When the address record was restructured, 5 files were edited in the chained design and 2
  in the short design; both versions kept producing the same five lines.
- Shortening the chain with one delegating method per field does not fix the measure; the
  intermediate object should answer the clients' questions, not their field names.

## Next Step

In the short version, the `address` object still returns values: `provinceDistrict()` hands
back a string, and the caller makes the decision. Branch selection takes that string and
formats it; notification uses another part of it. When the same rule repeats across several
call sites, the question changes: hand the data out and leave the decision to the client, or
tell the object the decision itself? The next lesson counts how many separate places the same
rule repeats in, and compares it with a version that moves the decision into the data.
