Case file — A10

Interface Segregation Principle

Small, focused surfaces.

No caller depends on operations it never uses. Several narrow types beat one wide “role” interface. The fat interface is a coupling smell with a respectable pseudonym.

ByAdam LewisPublished3 May 2026Reading12 minVersionv1.0ConfidenceHigh
§0b

Opinion

I have spent enough time refactoring God-interfaces to know the pattern is the same every time. Two consumers grow over a year. One of them needs three methods; the other needs four; the union is seven. Someone wires both consumers to the seven-method interface because the type-system wants it, and now changing any of those seven methods forces a recompile, a retest, and a careful re-read of code that genuinely does not care. ISP is the rule that says: stop. Two narrow interfaces beat the union; the calls are smaller; the rebuilds are smaller; the bugs are smaller.

Modern TypeScript has the structural-typing equivalent built in. Pick<User, "id" | "name"> replaces User; a function parameter declares the smallest object shape it actually reads. That is ISP done at the call site, every time the call site is written. The discipline is not Java-shaped overhead; it is the modern type-system's default when the team uses it.

Pair ISP with A11 Dependency Inversion: the abstraction the high level depends on must be narrow enough to satisfy without leaking detail. Pair it with F6 Encapsulation: the public surface is the only surface, and ISP says that surface is per-client. Pair it with T1 Domain-Driven Types: the type encodes the design, and the design includes the consumer's actual needs. The four rules together make a codebase whose interfaces grow by intention rather than by accumulation.

Copy a note and link

Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.

No caller depends on operations it never uses. Several narrow types beat one wide role-interface. In structural-typing languages, prefer per-callsite narrowing — `Pick<User, 'id'>` over `User` — when the function only reads the smaller shape.

/tenet/interface-segregation-principle/A10
§0c

AI eyes only

Rule: small, focused surfaces. Clients depend on the slice they use, never the whole.

Reject: function parameters typed as the full domain object when only one or two fields are read. Reject: God-interfaces with a dozen methods. Reject: forcing a caller to satisfy members it does not use.

Generate: parameters typed as Pick<Type, "field"> of the fields actually read. Interfaces split by client need, not by source class.

Diagnostic: list the members the function actually accesses. If the parameter type declares more than that list, narrow the type.

§0d

Why?

  • Smaller blast radius on every refactor. Touching a method that lives on a narrow interface forces a recompile of three files, not thirty. The build graph shrinks where it matters most.
  • Smaller mocks in tests. A mock of a three-method interface is three lines; a mock of a seven-method God-interface is seven lines plus the maintenance cost of the four methods the test never calls.
  • The interface is shaped by the consumer's needs, not the implementation's accidents. Two consumers with disjoint needs get two interfaces; the type system tells the truth instead of accumulating a union.
  • TypeScript's Pick, Go's structural interfaces and Rust's trait bounds make compliance close to free. Per-callsite interface narrowing is a one-liner in every modern type system.
  • Pairs with F6 Encapsulation: the public surface is the only surface, and ISP says that surface is per-client. Smaller surfaces are easier to honour and easier to change.
  • Easier onboarding. A new contributor reading a narrow interface knows what the consumer needs in under a minute; a wide interface tells them what every consumer might use, which is a different question.
  • Plays well with discriminated unions. When the “capability” is a discriminator (printable + stapleable + faxable), the union is the type and the consumer asks the type system which capability is present.
  • Agents reach for the wide type by default because autocomplete prefers high-coverage types. With ISP loaded, the model picks the smallest object shape its function actually reads — a discipline a senior reviewer applies in code review.
The receipts
Origins, quoted passages, evidence, the strongest counter-argument and the reply.
§1

Origins

The principle came out of Robert C. Martin's consulting work for Xerox in the mid-1990s. The Xerox printer engineering team had a single Job class that represented every operation the multifunction device could perform — print, staple, fax. Every client module had to depend on every method of Job; touching the stapling logic forced a rebuild of the print and fax modules. Martin proposed splitting Job into role-specific interfaces — Printable, Stapleable, Faxable — so each client saw only what it used. He published the case study in The Interface Segregation Principle, C++ Report August 1996.1Robert C. Martin“The Interface Segregation Principle,” C++ Report, August 1996. The Xerox case study with the fat `Job` class. The original published statement.

The principle landed in book form in Agile Software Development: Principles, Patterns, and Practices (Prentice Hall, 2002) as the “I” of SOLID, and was restated in Clean Architecture (2017), Ch. 10.2Robert C. MartinClean Architecture (Pearson, 2017), Ch. 10 “The Interface Segregation Principle.” Modern restatement; emphasises the architectural reading where the segregation operates at the package boundary. Martin's framing remained Java-shaped: classes, interfaces, implements keywords, and the explicit-interface tradition that comes with nominal type systems. The structural-typing world (Go, TypeScript, OCaml) gets ISP almost for free — ask for the smallest object shape your function reads, and you have done it.

Bertrand Meyer's 1988 Object-Oriented Software Construction was the parallel tradition. Meyer treated client-supplier contracts as the primary unit of design; the contract is what the client requires, and the supplier is built to satisfy it. ISP is the same instinct expressed at the interface scale instead of the method scale.3Bertrand MeyerObject-Oriented Software Construction, 1st ed. (Prentice Hall, 1988). Client-supplier contracts as the primary unit of design; the contract is what the client requires. The principle's name is Martin's; the underlying instinct is older.

Modern languages took the principle further. Go's structural interfaces (“an interface is satisfied by any type that has the methods”) make ISP the path of least resistance — the language idiom is “the bigger the interface, the weaker the abstraction” (Rob Pike).6Rob Pike“The bigger the interface, the weaker the abstraction.” Go Proverbs (2015). The Go community's verdict; ISP as the language idiom rather than the optional principle. Rust's trait coherence rules and trait-bound generic constraints land in the same place from the type-theory direction. TypeScript's Pick, Omit and structural intersection types make per-callsite interface narrowing a one-liner. The principle outlived the C++ Report; the languages followed it.

§2

Quotes

Clients should not be forced to depend upon interfaces that they do not use.

Robert C. Martin · The Interface Segregation Principle (1996)

The bigger the interface, the weaker the abstraction.

Rob Pike · Go Proverbs (2015)

If appropriate interface types exist, then parameters, return values, variables, and fields should all be declared using interface types.

Joshua Bloch · Effective Java 3e (2017), Item 64

We design role interfaces from the perspective of the calling object: the methods are precisely those that the caller needs.

Steve Freeman & Nat Pryce · Growing Object-Oriented Software (2010)
§3

Evidence

Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.

  1. 01
    Robert C. Martin · 1996
    The original Xerox case study. A fat `Job` class forces every client to depend on every method; ISP is the fix.
  2. 02
    Robert C. Martin · 2017
    Modern restatement; emphasises the architectural reading where segregation operates at the package boundary.
  3. 03
    Robert C. Martin · 2002
    First book-length treatment. ISP as the I of SOLID; worked examples in C++ and Java.
  4. 04
    Bertrand Meyer · 1997
    Client-supplier contracts as the primary unit of design. ISP is the same instinct expressed at the interface scale.
  5. 05
    Joshua Bloch · 2017
    Item 64 “Refer to objects by their interfaces.” The pragmatic Java-shaped operationalisation: depend on the smallest interface that satisfies the call.

Sixteen sources spanning Martin's 1996 piece and the SOLID canon, with Meyer and Bloch operationalising the rule for OO and Java. The modern structural-typing traditions (TypeScript, Go, Rust) and test-doubles literature sit further down. The qualifiers carry the “sometimes a wide interface is cheaper” reading; the opposers carry the “ISP is just narrow types” reading.

§4

Examples

Viewing: TypeScript.
Avoid
Filehedgehog-platform.ts
// Before: a feeder depends on twelve methods to use two.interface HedgehogPlatform {  feed(id: HedgehogId, grams: number): void;  weigh(id: HedgehogId): number;  track(id: HedgehogId): GpsFix;  release(id: HedgehogId, site: Site): void;  breed(pair: BreedingPair): void;  medicate(id: HedgehogId, drug: Drug): void;  // ... vaccinate, triage, microchip, quarantine, report, ...  audit(): AuditReport;}function feedShift(platform: HedgehogPlatform, id: HedgehogId) {  platform.feed(id, 50);  platform.weigh(id);}
Prefer
Filehedgehog-roles.ts
// After: three role interfaces. Each consumer depends on what it uses.interface Feeder { feed(id: HedgehogId, grams: number): void; weigh(id: HedgehogId): number; }interface Tracker { track(id: HedgehogId): GpsFix; release(id: HedgehogId, site: Site): void; }interface Veterinarian { medicate(id: HedgehogId, drug: Drug): void; }function feedShift(feeder: Feeder, id: HedgehogId) {  feeder.feed(id, 50);  feeder.weigh(id);}
§4b

Enforcement

Viewing: TypeScript.

Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.

RuleToolCatches
@typescript-eslint/no-unused-varstypescript-eslintfunction parameters the body never reads. The cheapest possible signal that the parameter type is wider than it needs to be.
@typescript-eslint/no-empty-interfacetypescript-eslintempty interfaces. Either the interface should be a `Pick`-shaped narrowing, or the consumer didn't actually need an interface at all.
@typescript-eslint/prefer-function-typetypescript-eslintinterfaces that wrap a single call signature. A function type is the narrower expression; the interface is over-spec.
sonarjs/cognitive-complexityeslint-plugin-sonarjsfunctions doing too much. Often the symptom of a parameter type carrying too much; narrow it and complexity drops.
sonarjs/no-useless-intersectioneslint-plugin-sonarjsintersection types where one operand is `unknown` or `any`. The intersection adds a name without narrowing; the consumer still gets the wide shape.
max-classes-per-fileESLint corefiles with multiple classes. Often the symptom of one fat class that should have been split into role-specific types.
max-paramsESLint corelong parameter lists. The function-level shape of the same problem; the function is asking for too much from too many sources.
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import sonarjs from 'eslint-plugin-sonarjs';

export default tseslint.config({
  files: ['**/*.{ts,tsx}'],
  plugins: { sonarjs },
  rules: {
    '@typescript-eslint/no-unused-vars': ['error', { args: 'all', argsIgnorePattern: '^_' }],
    '@typescript-eslint/no-empty-interface': 'error',
    '@typescript-eslint/prefer-function-type': 'error',
    'sonarjs/cognitive-complexity': ['error', 15],
    'sonarjs/no-useless-intersection': 'error',
    'max-classes-per-file': ['error', 1],
    'max-params': ['error', 3],
  }
});
§4c

AI rules

File.cursor/rules/a10-interface-segregation.mdc
---
description: Prickles A10 — Interface Segregation Principle
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---

## Prickles A10 — Interface Segregation Principle

Build interfaces around clients, not implementations. No caller depends on operations it never uses.

When two consumers of one interface have disjoint method needs, split the interface into two role-specific interfaces. Several narrow types beat one wide role.

In structural-typing languages, prefer per-callsite narrowing — `Pick<User, 'id' | 'name'>` over `User` — when the function only reads the smaller shape.

Refuse a parameter type wider than the function reads. Refuse a base class with methods only some subclasses honour. Refuse a God-interface that two consumers reach into for opposite reasons.

Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.

§5

Counter-argument

Counter

The strongest steelman is the proliferation cost.5Wiki C2 community“Interface Segregation Principle” discussion. Catalogues the proliferation cost — too many narrow interfaces become noise when consumer needs are small or stable. Splitting one interface into many means more types to name, more files to navigate, more import lines per consumer, and the canonical “which interface does X implement?” question every time the type is extended. Some practitioners argue that for small teams with stable domains, a wider interface is genuinely cheaper to maintain than several narrow ones. The implication for ISP: the rule is right when interfaces have many consumers with disjoint needs, and over-applied when they do not. The rule is correct; the discipline is knowing when it earns its weight.

§6

Counter-argument retort

Reply

The proliferation-cost counter is correct in the small and wrong in the large.5Wiki C2 community“Interface Segregation Principle” discussion. Catalogues the proliferation cost — too many narrow interfaces become noise when consumer needs are small or stable. For a single team with a stable domain and three implementations of one interface, splitting saves nothing and costs naming overhead. The rule is right when interfaces have many consumers with disjoint needs; the discipline is knowing when that condition holds. The reply: the structural-typing path makes the cost of compliance close to zero. Pick<User, "id" | "name"> is not a separate named interface to maintain; it is a shape that lives at the call site. Apply ISP at the smallest scale the language supports, and the proliferation problem dissolves.

The composition-first counter — “don't use interfaces; use composition; ISP becomes irrelevant” — lands in the same place as the parallel counter to A9 Liskov Substitution. ISP is not a recommendation to use interfaces; it is the rule for when you do. If you are composing rather than implementing, you have already satisfied the rule by construction. The principle is the discipline at the decision point, not the recommendation to choose interfaces.

The harder objection is the test-doubles one. A narrow interface with three methods needs a mock with three methods; a wider interface with seven methods needs a mock with seven. ISP makes the test-double surface smaller, which is good in isolation but adds setup overhead across many test files. The reply: the small-mock burden is a Liskov problem in disguise. A mock that satisfies a narrow interface is closer to behavioural substitutability than a mock of a wide interface; the test that uses the narrow mock is closer to a behaviour test than a coupling test.9Steve Freeman & Nat PryceGrowing Object-Oriented Software, Guided by Tests (Addison-Wesley, 2010). The role-based-interface design that pairs with ISP — narrow interfaces drive easier mocks and easier behaviour-focused tests. Smaller surface is the same property in the test suite that it is in the production code.

The genuine residue is naming. Many narrow interfaces need many names, and naming is the hardest problem in programming. Pair ISP with F2 Intention-Revealing Names: the name should describe what the consumer needs, not what the implementation provides. Renderable is a capability; UserService is a class; the first survives ISP and the second doesn't.

§7

Notes

  1. [1]Robert C. Martin“The Interface Segregation Principle,” C++ Report, August 1996. The Xerox case study with the fat `Job` class. The original published statement.
  2. [2]Robert C. MartinClean Architecture (Pearson, 2017), Ch. 10 “The Interface Segregation Principle.” Modern restatement; emphasises the architectural reading where the segregation operates at the package boundary.
  3. [3]Bertrand MeyerObject-Oriented Software Construction, 1st ed. (Prentice Hall, 1988). Client-supplier contracts as the primary unit of design; the contract is what the client requires.
Disagree? Found a hole in the argument? Take issue with this tenet →
Last revised: 2026-04-27