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 2026Revised13 Oct 2026Reading12 minVersionv1.2ConfidenceHigh
§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 and 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 means 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 and 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 reads. That is ISP done at the call site, every time the call site is written. The discipline is not Java-shaped overhead but 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 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
Copy for paste
§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 read. Interfaces split by client need, not by source class.

Diagnostic: list the members the function 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 means a recompile of three files, not thirty. The build graph shrinks at the points that see the most change.
  • 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 rather than 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 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.1

The principle landed in book form in Agile Software Development: Principles, Patterns, and Practices (Prentice Hall, 2002)4 as the “I” of SOLID, and was restated in Clean Architecture (2017), Ch. 10.2 Martin's vocabulary 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.3 The principle's name is Martin's. The 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).6 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.7 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, parameters, return values, variables, and fields should all be declared using interface types.

Joshua Bloch · Effective Java Programming Language Guide (2001), Item 34

This process results in a system of narrow interfaces each of which defines a role in an interaction between objects, rather than wide interfaces that describe all the features provided by a class.

Freeman, Pryce, Mackinnon & Walnes · Mock Roles, not Objects (2004)
§3

Evidence

16 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 published statement of the rule. Worked through the TimedDoor/Timer interface-pollution example and the ATM UI segregated into DepositUI, WithdrawUI and TransferUI.
  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.
  1. 06
    Steve Freeman & Nat Pryce · 2010
    Role-based interface design. Narrow interfaces drive easier mocks, easier behaviour tests, smaller blast radius on refactor.
  2. 07
    Rob Pike · 2015
    The Go community's verdict. ISP as language idiom rather than optional principle. Structural interfaces make compliance the path of least resistance.
  3. 08
    TypeScript team · 2014 onwards
    `Pick`, `Omit` and structural intersection types make per-callsite interface narrowing a one-liner. The proliferation-cost objection dissolves.
  4. 09
    Wiki C2 community · 1996 onwards
    Catalogues the proliferation cost. Too many narrow interfaces become noise when consumer needs are small or stable.
  5. 10
    Jim Blandy & Jason Orendorff · 2021
    Trait-bound generic constraints make ISP the type-system's idiom. `fn print<T: Printable>(item: T)` asks only for what it uses.
  6. 11
    Sean Parent · 2013
    Composition-over-interfaces: ISP becomes irrelevant if you compose rather than implement. The reply: ISP is the rule for when you do use interfaces.
  7. 12
    Sandi Metz · 2018
    Treats role-based interfaces with the same caution it treats inheritance. The “most interfaces are wrong” reading; ISP is the carve-out for the cases where you need them.
  8. 13
    Vladimir Khorikov · 2020
    Argues mocks of narrow interfaces are closer to behavioural-substitutability tests. The test-doubles literature backs ISP from the testing side.
  9. 14
    Michael Feathers · 2004
    The “Extract Interface” dependency-breaking technique is ISP applied to legacy code: pull a small role interface off the fat class so the new code depends only on the methods it calls.
  10. 15
    Wikipedia · 2024
    Cleanest single-page reference. Carries the Xerox `Job` origin story that Martin's own article does not, and summarises the ATM example in prose.
  11. 16
    Eric Evans · 2003
    Aggregate boundaries and repository interfaces. The aggregate's interface is shaped by what the application service uses; ISP at the strategic-design layer.

Sixteen sources spanning Martin's 1996 piece and the SOLID canon, with Meyer and Bloch operationalising the rule for OO and Java.8 The modern structural-typing traditions (TypeScript, Go, Rust) and test-doubles literature sit further down. The qualifiers hold the “sometimes a wide interface is cheaper” reading; the opposer holds the “compose instead of implementing and ISP never arises” reading.

§4

Examples

Viewing: TypeScript.
Avoid

Before a feeder depends on twelve methods to use two.

Filehedgehog-platform.ts
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

After three role interfaces. Each consumer depends on what it uses.

Filehedgehog-roles.ts
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-object-typetypescript-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 { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import sonarjs from 'eslint-plugin-sonarjs';

export default defineConfig({
  files: ['**/*.{ts,tsx}'],
  extends: [tseslint.configs.base],
  plugins: { sonarjs },
  rules: {
    '@typescript-eslint/no-unused-vars': ['error', { args: 'all', argsIgnorePattern: '^_' }],
    '@typescript-eslint/no-empty-object-type': '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
---
alwaysApply: true
---

## 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.

Bounce a parameter type wider than the function reads. Bounce a base class with methods only some subclasses honour. Bounce 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.5 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.5 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, but 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 under another name. A mock that satisfies a narrow interface is closer to behavioural substitutability than a mock of a wide interface; the test built on the narrow mock is closer to a behaviour test than a coupling test.9 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 rather than 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 TimedDoor/Timer interface-pollution example and the segregated ATM UI. The original published statement.
  2. [2]Robert C. Martin: Clean 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 Meyer: Object-Oriented Software Construction, 1st ed. (Prentice Hall, 1988). Client-supplier contracts as the primary unit of design, where the contract is what the client requires.
Disagree? Found a hole in the argument? Take issue with this tenet →
Last revised: 2026-10-13