---
title: 'Interface Segregation Principle'
source: 'https://academia.sh/en/courses/design-principles/interface-segregation-principle'
course: 'Design Principles'
language: en
updated: '2026-08-23T07:01:17+00:00'
license: 'CC BY-SA 4.0'
---

# Interface Segregation Principle

Counting the cost a bloated carrier interface imposes on clients: scanning and comparing the number of methods each client genuinely calls against the number of methods the interface imposes, measuring the total method count in fake dependencies, and finding with a run how many fakes break when a ninth method is added to the interface.

The contract test ran through two methods, and every tariff genuinely used both. This
balance breaks once the discussion moves to the carrier side: carrier selection, route
planning, and delivery tracking are gathered onto the same object. Once a single carrier
interface is built, a client that only calculates the fee also becomes bound to the
route and tracking methods.

The **interface segregation principle** forbids this: no client should be forced to
depend on methods it does not use. The principle's measure is a comparison of two
separate numbers — the number of methods a client **calls** and the number of methods
the interface **imposes**.

## A Single, Wide Contract

In the first version, all eight methods are gathered into a single contract. The
contract is written as a list and a validator that checks it; clients call the validator
to make sure the object they received satisfies the contract.

```js
// v1/carrier.mjs — single, wide carrier contract
export const CARRIER_CONTRACT = [
  "zoneFactor", "baseFee", "transferPoints", "estimatedDuration",
  "state", "received", "printLabel", "cancel",
];

export function validateContract(c) {
  const missing = CARRIER_CONTRACT.filter((name) => typeof c?.[name] !== "function");
  if (missing.length > 0) throw new TypeError(`carrier contract missing: ${missing.join(", ")}`);
  return c;
}
```

Three clients bind to this contract. Each exports the contract it binds to under the
name `REQUIRED`; the measurement script will read this name.

```js
// v1/fee-calculation.mjs — fee client: calls two methods, requires eight
import { validateContract, CARRIER_CONTRACT } from "./carrier.mjs";

export const REQUIRED = CARRIER_CONTRACT;

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

```js
// v1/route-planner.mjs — route client
import { validateContract, CARRIER_CONTRACT } from "./carrier.mjs";

export const REQUIRED = CARRIER_CONTRACT;

export function route(carrier, source, destination) {
  validateContract(carrier);
  const points = carrier.transferPoints(source, destination);
  return { points, hours: carrier.estimatedDuration(points) };
}
```

```js
// v1/tracking.mjs — tracking client
import { validateContract, CARRIER_CONTRACT } from "./carrier.mjs";

export const REQUIRED = CARRIER_CONTRACT;

export function deliver(carrier, trackingNumber, signature) {
  validateContract(carrier);
  carrier.received(trackingNumber, signature);
  return carrier.state(trackingNumber);
}
```

## Measuring the Two Numbers

The measurement script scans each client's text for calls that begin with `carrier.`
and counts the distinct method names it finds. The imposed count is the length of the
contract the client binds to.

```js
// interface-measure.mjs — number of methods a client calls vs. what the interface imposes
import { readFileSync } from "node:fs";

const CALL = /\bcarrier\.(\w+)\(/g;
const root = process.argv[2];
const clients = process.argv.slice(3);
let usedTotal = 0;
let imposedTotal = 0;

for (const name of clients) {
  const text = readFileSync(`${root}/${name}`, "utf8");
  const used = new Set([...text.matchAll(CALL)].map((m) => m[1]));
  const { REQUIRED } = await import(`./${root}/${name}`);
  usedTotal += used.size;
  imposedTotal += REQUIRED.length;
  console.log(`${name.padEnd(20)} used = ${used.size}  imposed = ${REQUIRED.length}` +
    `  surplus = ${REQUIRED.length - used.size}`);
}
console.log(`${root}: used total = ${usedTotal}, imposed total = ${imposedTotal}`);
```

```sh
node interface-measure.mjs v1 fee-calculation.mjs route-planner.mjs tracking.mjs
```

```
fee-calculation.mjs  used = 2  imposed = 8  surplus = 6
route-planner.mjs    used = 2  imposed = 8  surplus = 6
tracking.mjs         used = 2  imposed = 8  surplus = 6
v1: used total = 6, imposed total = 24
```

Six calls correspond to twenty-four dependencies. The surplus is not conceptual: each
client is bound to the name and existence of six methods it does not use.

## The Cost of the Fake Dependency

The first concrete bill for the surplus arrives in fake dependencies. For each client,
fakes are written that carry only the methods that client calls.

```js
// fake-minimal.mjs — each fake carries only the methods its own client calls
export const FAKE = {
  fee: { zoneFactor: () => 115, baseFee: () => 6400 },
  route: { transferPoints: (s, d) => [s, "41", d], estimatedDuration: (p) => p.length * 6 },
  tracking: { state: () => "delivered", received: () => undefined },
};
```

```js
// try-fake.mjs — runs the three clients with the given fakes, counts what breaks and its cost
const root = process.argv[2];
const { FAKE } = await import(process.argv[3]);
const { fee } = await import(`./${root}/fee-calculation.mjs`);
const { route } = await import(`./${root}/route-planner.mjs`);
const { deliver } = await import(`./${root}/tracking.mjs`);

const jobs = [
  ["fee", () => fee(FAKE.fee, { weight: 3.0, address: "06500" })],
  ["route", () => route(FAKE.route, "34", "06")],
  ["tracking", () => deliver(FAKE.tracking, "TK-1", "A.Y.")],
];

let broken = 0;
for (const [name, f] of jobs) {
  try {
    console.log(`${name.padEnd(9)} -> ${JSON.stringify(f())}`);
  } catch (e) {
    broken += 1;
    console.log(`${name.padEnd(9)} -> ${e.message}`);
  }
}
const methods = Object.values(FAKE).reduce((t, s) => t + Object.keys(s).length, 0);
console.log(`${root} + ${process.argv[3]}: fake methods = ${methods}, broken clients = ${broken}`);
```

```sh
node try-fake.mjs v1 ./fake-minimal.mjs
```

```
fee       -> carrier contract missing: transferPoints, estimatedDuration, state, received, printLabel, cancel
route     -> carrier contract missing: zoneFactor, baseFee, state, received, printLabel, cancel
tracking  -> carrier contract missing: zoneFactor, baseFee, transferPoints, estimatedDuration, printLabel, cancel
v1 + ./fake-minimal.mjs: fake methods = 6, broken clients = 3
```

All three clients broke. For it to work, every fake must carry all eight methods; six
of them are padding that will never be called.

```js
// fake-full.mjs — padded fakes that satisfy the wide contract
const NOOP = () => undefined;

export const FAKE = {
  fee: {
    zoneFactor: () => 115, baseFee: () => 6400,
    transferPoints: NOOP, estimatedDuration: NOOP, state: NOOP, received: NOOP,
    printLabel: NOOP, cancel: NOOP,
  },
  route: {
    transferPoints: (s, d) => [s, "41", d], estimatedDuration: (p) => p.length * 6,
    zoneFactor: NOOP, baseFee: NOOP, state: NOOP, received: NOOP,
    printLabel: NOOP, cancel: NOOP,
  },
  tracking: {
    state: () => "delivered", received: NOOP,
    zoneFactor: NOOP, baseFee: NOOP, transferPoints: NOOP, estimatedDuration: NOOP,
    printLabel: NOOP, cancel: NOOP,
  },
};
```

```sh
node try-fake.mjs v1 ./fake-full.mjs
```

```
fee       -> 7360
route     -> {"points":["34","41","06"],"hours":18}
tracking  -> "delivered"
v1 + ./fake-full.mjs: fake methods = 24, broken clients = 0
```

The fake apparatus rose from six methods to twenty-four. The same load shows up in real
implementations too: a carrier that provides only tracking service must also fill in all
eight methods.

## Splitting the Contract by Client

In the second version, the eight methods are split into as many contracts as there are
clients. The criterion for the split is not the methods' subject matter but the client
that calls them.

```js
// v2/contracts.mjs — three small, client-specific contracts
export const FEE_CONTRACT = ["zoneFactor", "baseFee"];
export const ROUTE_CONTRACT = ["transferPoints", "estimatedDuration"];
export const TRACKING_CONTRACT = ["state", "received"];

export const validate = (name, list) => (c) => {
  const missing = list.filter((m) => typeof c?.[m] !== "function");
  if (missing.length > 0) throw new TypeError(`${name} contract missing: ${missing.join(", ")}`);
  return c;
};
```

```js
// v2/fee-calculation.mjs — binds only to the fee contract
import { validate, FEE_CONTRACT } from "./contracts.mjs";

export const REQUIRED = FEE_CONTRACT;
const validateFee = validate("fee", FEE_CONTRACT);

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

```js
// v2/route-planner.mjs — binds only to the route contract
import { validate, ROUTE_CONTRACT } from "./contracts.mjs";

export const REQUIRED = ROUTE_CONTRACT;
const validateRoute = validate("route", ROUTE_CONTRACT);

export function route(carrier, source, destination) {
  validateRoute(carrier);
  const points = carrier.transferPoints(source, destination);
  return { points, hours: carrier.estimatedDuration(points) };
}
```

```js
// v2/tracking.mjs — binds only to the tracking contract
import { validate, TRACKING_CONTRACT } from "./contracts.mjs";

export const REQUIRED = TRACKING_CONTRACT;
const validateTracking = validate("tracking", TRACKING_CONTRACT);

export function deliver(carrier, trackingNumber, signature) {
  validateTracking(carrier);
  carrier.received(trackingNumber, signature);
  return carrier.state(trackingNumber);
}
```

A carrier that offers all eight methods satisfies all three contracts; a split interface
does not require a split implementation. What changes is only what the client knows.

```sh
node interface-measure.mjs v2 fee-calculation.mjs route-planner.mjs tracking.mjs
node try-fake.mjs v2 ./fake-minimal.mjs
```

```
fee-calculation.mjs  used = 2  imposed = 2  surplus = 0
route-planner.mjs    used = 2  imposed = 2  surplus = 0
tracking.mjs         used = 2  imposed = 2  surplus = 0
v2: used total = 6, imposed total = 6
fee       -> 7360
route     -> {"points":["34","41","06"],"hours":18}
tracking  -> "delivered"
v2 + ./fake-minimal.mjs: fake methods = 6, broken clients = 0
```

The imposed total dropped from twenty-four to six, and the fake apparatus dropped from
twenty-four methods to six.

## The Ninth Method

The real difference appears once the interface grows. An insurance premium calculation
is needed, and the new method belongs to the fee side. Both trees are copied and the
change is applied to both. The in-place edit is given a backup extension; GNU and BSD
`sed` behave the same this way.

```sh
cp -r v1 v1-new
cp -r v2 v2-new
sed -i.y 's/"printLabel", "cancel",/"printLabel", "cancel", "insurancePremium",/' v1-new/carrier.mjs
sed -i.y 's/FEE_CONTRACT = \["zoneFactor", "baseFee"\]/FEE_CONTRACT = ["zoneFactor", "baseFee", "insurancePremium"]/' v2-new/contracts.mjs
rm -f v1-new/*.y v2-new/*.y
node try-fake.mjs v1-new ./fake-full.mjs
node try-fake.mjs v2-new ./fake-minimal.mjs
```

```
fee       -> carrier contract missing: insurancePremium
route     -> carrier contract missing: insurancePremium
tracking  -> carrier contract missing: insurancePremium
v1-new + ./fake-full.mjs: fake methods = 24, broken clients = 3
fee       -> fee contract missing: insurancePremium
route     -> {"points":["34","41","06"],"hours":18}
tracking  -> "delivered"
v2-new + ./fake-minimal.mjs: fake methods = 6, broken clients = 1
```

The single method added to the fee side broke all three clients at once in the wide
interface; in the split interface it broke only the relevant one. The route planner's
test, which had no knowledge of the insurance premium at all, broke in the first version
and did not break in the second.

## The Criterion and Limit of Splitting

The correct axis for splitting an interface is the client. Grouping methods by subject
matter — "the ones related to calculation," "the ones related to registration" — does
not give the same result; two different clients can use different subsets of the same
subject. The criterion is the scan result: which client calls which method names.

Splitting also has a cost. Three contracts mean three validators and three names; and
if a client genuinely calls all four of four methods, splitting it in two does not
reduce the surplus, it only raises the file count. The principle's threshold is in the
measurement: if the surplus is zero, the gain from splitting is also zero.

## Summary

- The interface segregation principle forbids a client from staying dependent on
  methods it does not use; its measure is the difference between the number of methods
  called and the number imposed.
- In the wide contract, the three clients' total calls came to 6 and the imposed
  dependencies to 24; in the split contracts, both came to 6.
- The first bill for the surplus is in fake dependencies: to satisfy the wide contract,
  the fake apparatus rose from 6 methods to 24, 18 of them padding that is never called.
- When a ninth method was added to the contract, 3 clients broke in the wide version
  and 1 client broke in the split version.
- The axis of the split is not the methods' subject matter but the client that calls
  them; where the surplus measures zero, splitting only raises the file count.

## Next Step

The split contracts stood in the `v2/contracts.mjs` file, next to the clients. This
placement is not an accident: who defines the contract is a decision separate from how
many pieces the interface is split into. Had the contract been defined next to the
carrier implementation, clients would still bind to a split interface, but they would
still import the side where the implementation lives. The next lesson compares these two
placements by extracting the import direction, and turns which side the abstraction is
defined on into a number.
