skip to content

Interface Segregation Principle

Clients should not be forced to depend on methods they never call, so fat interfaces get split into focused role interfaces. The tell-tale symptom is an implementation full of no-op methods, and interviewers like it because the fix also simplifies mocking.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

6

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

open as a page

What concrete symptoms in a codebase tell you an interface violates the Interface Segregation Principle, and what is the step-by-step refactor to fix it?

level: middleimportance: must knowfreq 62%

basics

~20 s

Look for implementations with empty bodies, return null, or "not supported" exceptions; callers that use one or two methods of a large type; and mocks/fakes full of unused stubs. Fix by extracting one small interface per role and letting classes implement several.

open as a page

What concrete effects does applying the Interface Segregation Principle have on unit testing, test doubles, and build/deployment coupling?

level: middleimportance: should knowfreq 45%

basics

~20 s

Narrow interfaces make test doubles tiny — you fake one or two methods instead of twenty — so tests are shorter and state the collaboration clearly. They also shrink the blast radius of a change: fewer files reference the type, so fewer things recompile, re-mock, or redeploy.

open as a page

Under the Interface Segregation Principle, who should define an interface — the component that implements it or the component that calls it? Explain the difference between a role interface and a header interface.

level: seniorimportance: should knowfreq 34%

basics

~20 s

The caller should define it. An interface written from the caller's needs (a "role interface") stays small and stable. An interface that just mirrors an existing class's full public surface (a "header interface") adds a file but removes no coupling.

open as a page

How are the Interface Segregation Principle and the Liskov Substitution Principle related, and how can violating interface segregation push a design into a substitutability violation?

level: seniorimportance: should knowfreq 38%

basics

~20 s

ISP is about not exposing callers to operations they don't use; LSP is about subtypes being usable anywhere the supertype is. A too-wide interface forces implementers to stub or throw on operations they can't support — and a caller that invokes one then breaks, which is an LSP failure.

open as a page

How does the Interface Segregation Principle apply at a remote/service API boundary rather than inside one codebase, and what are the failure modes of over-segregating there?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

The same idea scales up: don't force every consumer onto one giant shared API. Give each consumer group a contract shaped to its needs. But splitting too far gives chatty calls, many contracts to version, and heavy operational overhead — the fix costs more than the fat interface at some point.

open as a page