Interface Segregation Principle (ISP)
No client should be forced to depend on methods it doesn't use. The case for many small interfaces over one fat one.
Summary#
The Interface Segregation Principle is the I in SOLID and the easiest to dismiss as obvious until you find yourself stubbing out four methods just to compile a class that genuinely only needs one. Robert Martin’s formulation: no client should be forced to depend on methods it does not use.
Said plainly: if an interface bundles methods that different callers consume different subsets of, the callers that consume only one subset are still coupled to the rest — they import them, link against them, and break when any of the unused methods change their signature. ISP says split the interface along the lines of the callers, so each caller depends on only the surface it actually uses.
The canonical illustration is the fat printer interface: one IMachine that declares print, scan, fax, and staple, used by a multi-function machine and by a humble print-only device that has no business knowing about scanning. ISP names the smell; the refactor is to slice the interface.
Why it matters#
ISP is SRP applied to interfaces rather than classes. The two principles share a parent — don’t bundle unrelated things — and the costs of violating ISP are visible at the same diff-level as SRP violations.
Three concrete reasons interviewers care:
- The compiler keeps lying about your dependencies. A class that depends on
IMachineand only ever callsprint()looks coupled to a small surface but is actually coupled to the whole interface. Any change toscan()orfax()ripples to every implementer, including the print-only one. The dependency graph in your head and the dependency graph the compiler enforces drift apart. - Test doubles get heavier. A mock of a fat interface has to stub every method, even the ones the test does not touch. The cost is friction at every test boundary. Many small interfaces let each test mock only what it needs.
- It is the predicate that makes DIP useful. DIP says “depend on abstractions.” If the abstraction you depend on is a 20-method god interface, you depend on every method’s stability. ISP makes DIP’s abstractions actually abstract.
The principle’s value is measured at the implementation level: how many throw new UnsupportedOperationException("not supported by this device") stubs do you have in the codebase? ISP shrinks that count to zero.
How it works#
Spotting the violation#
The most reliable detector is the implementer that throws or returns dummy values from methods it cannot meaningfully support. Whenever a class implementing an interface has methods that begin with “this one doesn’t apply to me” or “this one is a no-op,” ISP is being violated.
A worked example. A draft office-equipment interface:
public interface IMachine { void print(Document d); void scan(Document d); void fax(Document d); void staple(Document d);}
public final class MultiFunctionMachine implements IMachine { @Override public void print(Document d) { /* ... */ } @Override public void scan(Document d) { /* ... */ } @Override public void fax(Document d) { /* ... */ } @Override public void staple(Document d){ /* ... */ }}
public final class OldPrinter implements IMachine { @Override public void print(Document d) { /* prints */ } @Override public void scan(Document d) { throw new UnsupportedOperationException(); } @Override public void fax(Document d) { throw new UnsupportedOperationException(); } @Override public void staple(Document d){ throw new UnsupportedOperationException(); }}OldPrinter cannot scan, fax, or staple — yet it must declare those methods to satisfy the interface, and any caller holding an IMachine reference might call them. That is two problems for the price of one: an ISP violation (the printer depends on methods it does not use) and an LSP violation (substituting OldPrinter for an IMachine will throw where callers do not expect throws).
The reason both principles fire together is not coincidence — fat interfaces produce LSP-broken implementations as a natural by-product. ISP fixes both at once.
The refactor#
Slice the interface along caller-cohort lines. Each capability becomes its own interface; classes implement only the capabilities they actually support.
public interface Printer { void print(Document d);}
public interface Scanner { void scan(Document d);}
public interface Fax { void fax(Document d);}
public interface Stapler { void staple(Document d);}
public final class MultiFunctionMachine implements Printer, Scanner, Fax, Stapler { @Override public void print(Document d) { /* ... */ } @Override public void scan(Document d) { /* ... */ } @Override public void fax(Document d) { /* ... */ } @Override public void staple(Document d) { /* ... */ }}
public final class OldPrinter implements Printer { @Override public void print(Document d) { /* prints */ }}OldPrinter no longer claims to scan. Callers that only need to print depend on Printer, not IMachine — and so they no longer transitively depend on the fax module, the scan driver, or anything else. MultiFunctionMachine is a single class implementing multiple capability interfaces, which is exactly the design intent.
Composing capabilities for callers#
Once interfaces are sliced, callers compose what they need by demanding intersections. A function that needs both scanning and printing declares the conjunction explicitly:
public final class CopyJob { private final Scanner scanner; private final Printer printer;
public CopyJob(Scanner scanner, Printer printer) { this.scanner = scanner; this.printer = printer; }
public void copy(Document d) { scanner.scan(d); printer.print(d); }}The caller could be handed two separate devices, or one multi-function device passed twice — CopyJob neither knows nor cares. The dependency is now precisely Scanner ∧ Printer, with no fax or staple in sight.
For cases where one parameter must satisfy multiple interfaces, Java’s generics carry the intersection cleanly: <T extends Scanner & Printer> declares a type parameter that must implement both.
The other side — when ISP has been over-applied#
The opposite failure mode is interface dust: slicing every method into its own interface, so a working multi-function machine ends up implementing ten of them and every consumer has to construct ten parameters.
Under-applied (fat interface). IMachine with 20 methods. Every implementer stubs the ones it cannot do. Every caller imports a transitive forest.
Over-applied (interface dust). IPrintColour, IPrintGrayscale, IPrintDuplex, IPrintSimplex. Now MultiFunctionMachine declares twelve interfaces and the constructor takes twelve fields.
The signal that ISP has been over-applied: interfaces with one method that are only ever used together. They are not separate capabilities — they are parameters of one capability. Collapse them back into a single interface whose method takes those parameters.
The right granularity is the caller cohort: a group of methods that are always consumed together by at least one client. If one client always wants both print and printDuplex, they belong on the same interface.
Variants and trade-offs#
A few useful refinements:
- Role interfaces vs header interfaces. Martin Fowler’s distinction. A role interface describes the role a collaborator plays from one caller’s perspective (
PrintertoCopyJob). A header interface mirrors a class’s public methods 1:1. ISP rewards role interfaces. - Default methods change the maths. Java 8’s default methods let a fat interface ship sensible no-op defaults — so
OldPrintercan implementIMachinewithout writing throw stubs. This softens the cost of fat interfaces but does not fix the dependency-graph problem. ISP still applies. - Interfaces are cheap; consumers are not. Adding a new interface costs almost nothing. The cost is borne when callers have to choose between intersections, or when wiring code has to enumerate every capability of every device. Pick the granularity the callers benefit from, not the granularity that maximises decomposition.
| Granularity | When it fits | Risk |
|---|---|---|
| One fat interface | A truly homogeneous capability where every method is used by every caller | Becomes the multi-function-printer trap as variants arrive |
| Sliced by caller cohort | The usual right answer | Requires understanding the call sites, not just the implementer |
| One method per interface | When each method is genuinely an independent capability of a plugin SPI | Interface dust; constructor parameter lists explode |
When this is asked in interviews#
Three moves carry ISP discussion in an LLD round:
- Lead with the implementer’s stubs. When asked “what’s wrong with this interface?”, look for the
UnsupportedOperationExceptionthrows or the dummy-return methods. That is the visible footprint of an ISP violation, and it is what to call out first. - Slice along caller cohorts, not method counts. It is tempting to slice “until each interface is small.” Slice instead “until each interface is consumed as a unit by some real caller.” That framing avoids interface dust.
- Pair ISP with DIP explicitly. Many candidates discuss ISP and DIP as separate principles. They are the same idea at two levels: ISP says the abstraction should be small; DIP says depend on the abstraction. Linking the two is the senior signal.
The principle’s one durable test, after the meeting: does any implementer of this interface have a method body that says “doesn’t apply to me”? If yes, the interface is fatter than its callers warrant.
Related concepts#
- OOP Fundamentals — The Four Pillars — ISP is abstraction said with discipline; the pillars supply the vocabulary.
- Abstraction — the pillar ISP sharpens; an abstraction is honest only when its consumers use all of it.
- Single Responsibility Principle (SRP) — SRP for classes, ISP for interfaces; both come from the same instinct.
- Dependency Inversion Principle (DIP) — DIP says depend on abstractions; ISP says make those abstractions small enough to be worth depending on.
- Strategy Pattern — strategy interfaces are the most common consumers of ISP discipline; a fat strategy interface is the same smell wearing a different hat.