skip to content

What does the Interface Segregation Principle (ISP) state, and what problem is it meant to prevent?

level: juniorimportance: must knowfreq 78%

answer

  1. clients ≠ implementers: client = caller
  2. fat interface → forced stubs / UnsupportedOperation
  3. Xerox printer Job class, 1-hour rebuild
  4. split by role, one class implements many
  5. not a method-count limit

basics

~20 s

ISP — the "I" in SOLID — says no caller should be forced to depend on operations it never uses. Instead of one large interface with many unrelated methods, define several small ones, each matching what a particular caller actually needs.

solid answer

~50 s

ISP states that clients should not be forced to depend on methods they do not use. A "fat" interface bundles operations that belong to different roles, so every caller and every implementer is coupled to the union of all of them. The consequences are concrete: implementers must supply stubs or error-throwing bodies for operations that make no sense for them; a change to a method only one client cares about ripples into every implementer, every mock, and every compilation unit that references the type. The fix is to split the fat interface along role lines — a set of narrow interfaces, each cohesive around one responsibility — and let a class implement several of them if it genuinely plays several roles. ISP is about the shape of the dependency edge, not about a hard method-count limit; the test is whether any client is dragged along for operations it never invokes.

code

typescript · 23 lines
typescript
// Fat interface: every implementer pays for every role
interface Machine {
  print(doc: Doc): void;
  staple(doc: Doc): void;
  fax(doc: Doc, num: string): void;
}

class CheapPrinter implements Machine {
  print(d: Doc) { /* real */ }
  staple(d: Doc) { throw new Error("unsupported"); } // lie
  fax(d: Doc, n: string) { throw new Error("unsupported"); } // lie
}

// Segregated by role
interface Printer { print(doc: Doc): void; }
interface Stapler { staple(doc: Doc): void; }
interface Fax     { fax(doc: Doc, num: string): void; }

class CheapPrinter2 implements Printer { print(d: Doc) { /* real */ } }
class OfficeMFP implements Printer, Stapler, Fax { /* all three, honestly */ }

// The caller now states exactly what it needs
function printReport(p: Printer, d: Doc) { p.print(d); }

go deeper

for a junior

State the sentence correctly, stress that "client" means caller, and give the printer/fax example with forced empty implementations.

for a middle

Add the concrete costs — forced stubs, recompilation ripple, mock bloat — and show the refactor: extract role interfaces, have one class implement several.

for a senior

Frame it as controlling the dependency edge; distinguish it from SRP; note the refactor is runtime-neutral and discuss when segregation stops paying (interface explosion, callers needing intersections).

for a principal

Connect ISP to Dependency Inversion and ports-and-adapters (interfaces owned by the consumer), to capability minimization as a security property, and to module/service boundaries where the ripple cost is deployment coordination rather than compile time.

## The rule > **"Clients should not be forced to depend on methods they do not use."** — Robert C. Martin. ISP is the **I** in **SOLID** (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion). ## Every term, defined - **Interface** — a named set of operation signatures (name, parameters, return type) with no implementation. Most languages have an explicit construct (`interface`, `protocol`, `trait`, abstract base class); in dynamically typed languages the "interface" is the implicit set of methods a caller invokes. ISP applies in both cases. - **Client** — any code that *calls through* the interface. Note: the client is the **caller**, not the implementer. This is the most commonly misread word in the principle. - **Implementer** — the concrete type that supplies bodies for the interface's operations. - **"Depend on"** — the client's source references the type; a change to that type can force the client to recompile, be re-reviewed, re-mocked, or re-deployed. Dependency is transitive over the *whole* type, not just the members you call — which is exactly why unused methods still hurt. - **Fat interface** (also "polluted" or "header" interface) — one interface carrying operations that serve several unrelated roles. - **Role interface** — a narrow interface named for the *capability a caller needs* (`Printer`, `Stapler`, `PasswordHasher`), rather than for the object that happens to provide it. ## The origin story (useful to cite) Martin derived ISP at **Xerox**, on software for a multifunction printer/copier. A single enormous `Job` class had accumulated every operation for every device — print, staple, fax, bind. Any change to one job type forced a rebuild and redeploy of the whole system, which took roughly an hour. The remedy was to put **role interfaces** in front of `Job` — `PrintJob`, `StapleJob` — so each client depended only on the operations it invoked, and edits stopped rippling. ## Why unused methods actually cost something 1. **Forced implementations.** Every implementer must provide *all* members. Types that legitimately play only part of the role end up with empty bodies, `return null`, or `throw UnsupportedOperationException` — a lie in the type system. 2. **Recompilation / redeployment coupling.** Changing a signature only one client cares about invalidates every file that names the interface. In compiled languages this is build time; in service ecosystems it is coordinated releases. 3. **Test friction.** A hand-rolled fake or a strict mock must satisfy every member, so tests carry stubs for behavior they never exercise. 4. **Conceptual load.** A reader of the client cannot tell which of the twelve methods matter here; the narrow interface documents the actual requirement. 5. **Accidental capability.** Handing a caller a fat interface hands it powers it should not have — an audit reader that also exposes `delete()` is a security and correctness hazard, not just a style issue. ## What ISP is *not* - **Not "interfaces must have ≤ N methods."** A cohesive interface with eight closely related operations, all used together by its clients, is fine. Two methods used by disjoint clients already violate ISP. - **Not "every class needs an interface."** Extracting a one-implementation `IFoo` mirror of `Foo` is header-interface noise; it does not segregate anything. - **Not the same as SRP.** Single Responsibility is about why a *module/class* changes (its reasons to change). ISP is about what a *client* is exposed to at the dependency edge. They are related — fat interfaces usually sit in front of low-cohesion classes — but a perfectly cohesive class can still be presented behind several role interfaces for different callers. ## How to apply it 1. Look at each caller and list which members it actually invokes. 2. Group callers by the set they use — those groups *are* the roles. 3. Extract one interface per role, named after the capability the caller needs. 4. Let the existing concrete class implement all of them (most languages allow multiple interface implementation), so no runtime structure has to change. Composition of narrow interfaces into a broader one (`interface ReadWriteStore : ReadStore, WriteStore`) is available when a caller genuinely needs both. 5. Delete the fat interface, or keep it only as the composed alias. This refactor is usually **non-breaking at runtime**: the object graph is unchanged, only the static types at the call sites narrow. ## Trade-off, stated honestly Segregation adds type count and one more level of indirection to navigate. Over-segregating — an interface per method — produces a namespace of anemic types that no one can hold in their head, and forces callers that need three capabilities to declare three parameters or an intersection type. The stopping condition is "no client depends on what it doesn't use," not "maximize the number of interfaces."

  • Does ISP mean I should never have an interface with more than three or four methods?
    No. The criterion is client usage, not size. An interface whose members are always used together by all its clients is cohesive at any reasonable size; an interface with two methods used by two disjoint clients already violates ISP.
  • If a class needs to play several roles, doesn't splitting the interface just move the fatness into the class?
    The class is allowed to be broad — it implements several role interfaces. ISP constrains what each *caller* sees, not how many capabilities one object provides. If the class itself has low cohesion, that is an SRP question, answered separately.
  • Does ISP apply in a duck-typed language with no interface keyword?
    Yes. The interface there is the implicit set of methods a function invokes on its argument. A function that calls only `read()` should be documented and tested against that minimal contract rather than demanding a full-featured object, and protocol/structural types can make it explicit.

A universal remote with 60 buttons for a TV, a Blu-ray player, and an air conditioner. If you own only the TV, 40 buttons do nothing — yet whenever the AC vendor relabels a button, a new remote ships to you too. ISP is handing each device owner a remote with only their buttons; the same underlying hardware still drives everything.

saying these in an interview costs you the question

  • Saying "clients" means the classes that implement the interface — clients are the callers
  • Reducing ISP to an arbitrary method-count cap
  • Claiming ISP requires an interface for every class (header interfaces)
  • Treating ISP and SRP as the same principle with different names
  • Believing unused methods are harmless because "you just don't call them" — they still couple you to change, testing, and capability
  • Splitting into one-method interfaces everywhere and calling the result cleaner

context