Interface Segregation Principle
Why bundling unrelated capabilities into one large interface forces implementers to fake support for things they can't do, and how splitting by capability fixes it.
Learning objectives
- State the Interface Segregation Principle and explain what 'forced to depend on' means
- Recognize UnsupportedOperationException-style stub methods as a common ISP warning sign
- Split a bloated interface into small, capability-focused interfaces
- Distinguish ISP (about interface shape) from SRP (about a class's job)
◆ Story
A job posting for "Office Cleaner" that also requires a forklift license, a food-handler's certificate, and fluency in three languages is asking candidates to prove capabilities that job will never actually need. Every qualified cleaner is forced to either fake those unrelated skills or awkwardly leave them blank — the job description itself is badly scoped, bundling unrelated requirements into one posting.
The Interface Segregation Principle (ISP) says exactly this about interfaces: clients shouldn't be forced to depend on methods they don't actually use.
The worked example for this topic is a model of office machines: a simple printer that only prints, and a multi-function machine that prints, scans, and copies. Both are, in some sense, "machines" — the question is what contract they should actually share.
Since every device in this domain does at least one of print, scan, or copy, the obvious first instinct is one shared interface covering all three. This works cleanly for MultiPurposeMachine, which genuinely does all three.
The problem shows up the moment a device that doesn't need all three has to implement the same interface anyway. A SimplePrinter genuinely supports print(), but has no scanner hardware and no copier hardware — and yet Machine requires it to provide scan() and copy() methods too. The only honest thing those methods can do is throw.
💻 Code example
interface Machine { void print(Document doc); void scan(Document doc); void copy(Document doc); } class MultiPurposeMachine implements Machine { public void print(Document doc) { System.out.println("Printing document"); } public void scan(Document doc) { System.out.println("Scanning document"); } public void copy(Document doc) { System.out.println("Copying document"); } } class SimplePrinter implements Machine { public void print(Document doc) { System.out.println("Printing document"); } // genuinely supported public void scan(Document doc) { throw new UnsupportedOperationException("This printer cannot scan"); // no scanner hardware exists } public void copy(Document doc) { throw new UnsupportedOperationException("This printer cannot copy"); } }
SimplePrinter is forced to implement scan() and copy() just to satisfy Machine's contract, even though neither operation makes sense for it. Throwing UnsupportedOperationException is a real, common warning sign of an ISP violation — it means the interface promised a capability this specific implementer genuinely can't provide, and any caller holding a plain Machine reference has no way to know in advance which methods are actually safe to call, short of either reading the concrete class's source or wrapping every call in a try/catch defensively.
The mistake, one step upstream of SimplePrinter's exceptions: Machine bundled three independent capabilities into one contract, on the assumption that "office machine" is a single coherent concept. It isn't — printing, scanning, and copying are three separable capabilities that happen to co-occur in some devices and not others. A device should only promise what it can actually do; a class implementing an interface is a promise to callers, and a promise that sometimes throws isn't a promise at all.
Split the bundle — one interface per capability, so a class only opts into exactly the ones it genuinely supports. SimplePrinter now implements only Printer, with no scan() or copy() anywhere, fake or otherwise. MultiPurposeMachine implements all three interfaces, since it genuinely needs all three — nothing is lost for the implementer that needs everything.
A device that scans and copies but doesn't print — a dedicated scanner-copier — can now be written as implements Scanner, Copier, with no Printer in sight and no fake, throwing print() method anywhere. Every implementer's list of interfaces is now an honest, compiler-checked description of what it can actually do — and calling simplePrinter.scan(doc) on a Printer-typed reference is now a compile error, not a runtime surprise.
💻 Code example
interface Printer { void print(Document doc); } interface Scanner { void scan(Document doc); } interface Copier { void copy(Document doc); } class SimplePrinter implements Printer { // no scan(), no copy(), no fake methods at all public void print(Document doc) { System.out.println("Printing document"); } } class MultiPurposeMachine implements Printer, Scanner, Copier { // genuinely needs all three public void print(Document doc) { System.out.println("Printing document"); } public void scan(Document doc) { System.out.println("Scanning document"); } public void copy(Document doc) { System.out.println("Copying document"); } } // usage Printer simple = new SimplePrinter(); simple.print(new Document()); // the ONLY thing a Printer reference can even offer to call MultiPurposeMachine mfp = new MultiPurposeMachine(); mfp.print(new Document()); mfp.scan(new Document()); mfp.copy(new Document()); // simple.scan(doc); // COMPILE ERROR — Printer has no scan() at all, no exception needed at runtime
▲ Edge case — splitting too finely
Going the other direction — one interface per method, for every conceivable capability, regardless of whether they ever vary independently — creates its own drag: a class implementing eight tiny single-method interfaces just to do one coherent job is harder to read than one interface with eight related methods. Split along capabilities that genuinely vary independently across implementers (some machines print but can't scan), not simply along "each method is technically separable."
▲ Edge case — implementing every small interface still isn't the same as one big interface
MultiPurposeMachine implements Printer, Scanner, Copier can do everything the original Machine-based version could — nothing is lost for the implementer that genuinely needs everything. What's gained is only visible from the caller's side: code that only needs printing can now depend on Printer alone, and never has to know Scanner or Copier even exist, which shrinks that code's dependency surface and makes it easier to test in isolation.
A genuinely common point of confusion worth resolving here: SRP (covered earlier in this course) is about a class having one reason to change; ISP is about an interface not forcing implementers to depend on methods they don't need. You can violate ISP while every individual implementing class still perfectly satisfies SRP — SimplePrinter in the original attempt had a perfectly single, focused responsibility (printing); the bloated Machine interface was the actual problem, not anything about how focused SimplePrinter's own job was.
Java's own standard library corrected exactly this kind of violation over time — early Java I/O had large, monolithic interfaces, and modern Java favors small, single-capability interfaces like Closeable, Comparable, and the functional interfaces from Java 8 (Function, Predicate, Supplier) — each one deliberately narrow, so implementers only ever commit to exactly one capability.
Iterable and Iterator are another clean example: a class only needs to implement Iterable to support a for-each loop, without being forced into unrelated collection behavior like add() or remove(), which live on the separate, larger Collection interface instead. Spring's @Repository layer relies on the same instinct — Spring Data lets you extend narrow interfaces like CrudRepository or compose your own with just the query methods a particular repository actually needs, rather than every application forcibly implementing one giant data-access contract.
Q: What's a common, real warning sign that an interface is violating ISP?
A: An implementing class throwing UnsupportedOperationException (or similar) from a method it's forced to implement but genuinely can't support.
Q: How does ISP differ from SRP?
A: SRP is about a class having one reason to change; ISP is about an interface not forcing implementers to depend on methods they don't actually need — a class can violate one without violating the other.
Q: Does splitting Machine into Printer, Scanner, and Copier lose any capability for MultiPurposeMachine?
A: No — it implements all three interfaces and does exactly what it did before. The benefit shows up on the caller's side: code needing only printing can depend on Printer alone.
Q: What's the risk of over-applying ISP?
A: Splitting into too many tiny, single-method interfaces for capabilities that never actually vary independently adds indirection without a real benefit — the split should track genuine, observed differences between implementers.
Q: Why is a compile error (from a missing method) a better outcome than a runtime UnsupportedOperationException?
A: A compile error is caught before the code ever ships; a runtime exception is only discovered when that exact code path executes in production, potentially far from where the mistake was actually made.
Want a visual for this concept?
Generate a diagram tailored to “Interface Segregation Principle” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →