The idea
What it is
The one sentence to remember
No client should be forced to depend on methods it doesn't use — prefer many small, focused interfaces over one fat one.
The Interface Segregation Principle is the I in SOLID. It says something simple: don't make a class promise to do things it can't actually do. If an interface lists ten methods and a class only really needs three of them, that class is being forced to depend on seven methods it doesn't use. ISP tells you to break the big interface into small ones, so every class signs up for only what it truly does.
Think of a restaurant menu. A vegetarian sitting down shouldn't be handed a menu where the only option is a giant combo platter of steak, fish, and a salad — they just want the salad. A good restaurant offers separate sections so each diner picks only what they want. A fat interface is the combo-only menu: it forces everyone to take the whole thing. ISP is the menu with sections — small, focused choices.
Another way to picture it: a universal remote with 50 buttons when all you do is turn the TV on, change the channel, and adjust the volume. The other 47 buttons aren't just clutter — every one is a thing you now depend on that can break, confuse you, or change under you. A small remote with the 3 buttons you use is the segregated version.
Mechanics
How it works
The smell: a fat interface
A fat interface is one big contract that bundles together methods that don't all belong to every implementer. The classic textbook example is a Worker interface with work(), eat(), and sleep(). That's fine for a human worker — but the day you add a RobotWorker, it's suddenly forced to implement eat() and sleep(), which make no sense for a robot. The robot has to provide something, so it ends up with empty or throwing methods.
The most famous version is the multifunction device. Imagine one interface, IMultiFunctionDevice, with print(), scan(), and fax(). A fancy office PhotoCopier can do all three — great. But a humble SimplePrinter can only print. Because it implements the fat interface, it is forced to provide scan() and fax() anyway — methods it has no hardware for. All it can do is throw:
class SimplePrinter implements IMultiFunctionDevice {
public void print() { /* real work */ }
// forced on us by the fat interface — we can't actually do these:
public void scan() { throw new UnsupportedOperationException(); }
public void fax() { throw new UnsupportedOperationException(); }
}Empty or throwing stubs are the tell
When you find yourself writing a method body that is empty, returns null, or just throw new UnsupportedOperationException() — stop. That stub is a sign the interface is fatter than the class. The class is being forced to depend on a method it doesn't use, which is exactly what ISP forbids.
Why a forced stub hurts
- It pollutes every implementer. Every class that implements the fat interface must write all the methods, even the ones it can't do — so the same throwing stubs get copy-pasted everywhere.
- It lies to callers. Code that holds an
IMultiFunctionDevicereference thinks it can callscan(). At runtime, on aSimplePrinter, that call blows up. The interface promised something the object can't keep. - It blocks change. Add a
staple()method to the fat interface and every device — even ones with no stapler — must be touched. Small interfaces only ripple to the classes that genuinely care.
The fix: split into role interfaces
Break the fat interface into small role interfaces — one per capability. Printer has just print(). Scanner has just scan(). Fax has just fax(). Now each class implements only the roles it can truly fulfil. A SimplePrinter implements Printer. A multifunction PhotoCopier implements all three — Printer, Scanner, and Fax — because it genuinely does all three. No class is ever forced; no stub ever throws.
IMultiFunctionDevice forces both SimplePrinter and PhotoCopier to implement all three methods — so SimplePrinter is stuck with two forced stubs that throw on scan() and fax(). After: the contract is split into Printer, Scanner, and Fax; SimplePrinter implements only Printer, PhotoCopier implements all three, and nobody is forced — the stub count is 0 everywhere.Links to LSP and to cohesion
ISP doesn't live alone. Those throwing stubs also break the Liskov Substitution Principle (LSP): a caller holding an IMultiFunctionDevice expects every method to work, but substituting a SimplePrinter makes scan() blow up — the subtype isn't a safe stand-in for the type. Splitting the interface fixes both problems at once.
ISP is high cohesion, at the interface level
A small role interface groups methods that belong together and that the same clients use together — that's high cohesion. A fat interface is the opposite: a grab-bag of unrelated capabilities. So you can read ISP as: keep each interface cohesive, and let classes implement exactly the cohesive roles they fulfil.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
A device shop with a mode switch. In Fat interface mode, one big IMultiFunctionDevice { print(); scan(); fax(); } forces every device — SimplePrinter, Scanner, PhotoCopier — to implement all three methods. Click a method: green means the device can really do it, red means it was forced to declare it and can only throw UnsupportedOperationException. A running tally counts the forced stubs. Flip to Segregated mode and the fat contract splits into Printer / Scanner / Fax role interfaces — each device now exposes only the methods it genuinely supports, nothing throws, and the forced-stub tally drops to 0. One fixed panel narrates every click; nothing scrolls away.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Count the forced stubs
Open the prototype in its default Fat interface mode. Read the big tally number at the bottom — those are the forced stubs across all three devices. Now click the red scan() button on SimplePrinter: watch the panel report throw UnsupportedOperationException. That red button is a method the printer was forced to declare but can't actually do.
Find the only device that's happy
Still in Fat mode, click print(), scan(), and fax() on the PhotoCopier. Every one is green / supported — because a multifunction copier genuinely does all three. Compare that to SimplePrinter and Scanner, which each light up red on the methods they don't have. The fat interface only fits the device that happens to do everything.
Segregate and watch the tally hit 0
Flip the toggle to Segregated mode. The fat interface splits into Printer / Scanner / Fax, and each device card now shows only the methods it truly supports — the impossible ones simply disappear. Click around: nothing throws anymore, and the forced-stub tally drops to 0. That zero is the Interface Segregation Principle, made visible.
In practice
When to use it — and what trips people up
When to split an interface
Reach for ISP whenever you notice the tell-tale signs: a class implementing an interface with empty bodies, null returns, or throw UnsupportedOperationException; an interface that keeps growing as new, unrelated features arrive; or different clients that each use only a small, different slice of the same big interface. In each case, carve the fat interface into role interfaces grouped by who-uses-what, and let each class implement only the roles it can honestly keep.
Note that “interface” here is broader than the interface keyword. It also means a class's public surface — the set of public methods other code can call — and a role an object plays for a particular client. ISP applies to all three: a class with 30 public methods, where each caller uses only a handful, is just as fat as a 30-method interface, and can be split the same way.
Don't over-segregate into one method per interface
ISP is about cohesion, not maximum fragmentation. If you reflexively give every single method its own interface, you get a confetti of IPrintable, IScannable, IFaxable, IStapleable… that's just as hard to use as the fat one — now the pain is spread across dozens of tiny files. Group methods that genuinely belong together and that the same clients use together. Split when a client is forced to depend on something it doesn't use; don't split when the methods naturally travel as a set.
What it gives you
- No forced stubs — every class implements only methods it can truly perform, so empty/throwing bodies disappear.
- Changes stay local: adding a method to one role interface only touches the classes that play that role, not every implementer.
- Clients depend on exactly the small contract they need, which lowers coupling and makes test doubles tiny and easy to write.
- Reinforces LSP and high cohesion — subtypes become safe substitutes and each interface describes one coherent capability.
Common mistakes
- More interfaces (and files) to name, navigate, and keep track of — extra ceremony for small or stable designs.
- Over-applied, it fragments into one-method interfaces, which is its own kind of clutter and confusion.
- A class that genuinely does many things (a true multifunction device) must now declare it implements several interfaces, lengthening its header.
- Deciding where the seams go takes design judgement; splitting along the wrong axis just relocates the coupling.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
// ❌ FAT INTERFACE — one contract bundles three capabilities.
interface IMultiFunctionDevice {
print(): void;
scan(): void;
fax(): void;
}
// A simple printer is FORCED to implement scan() and fax() it can't do.
class SimplePrinter implements IMultiFunctionDevice {
print(): void { /* real work */ }
scan(): void { throw new Error("UnsupportedOperationException"); } // forced stub
fax(): void { throw new Error("UnsupportedOperationException"); } // forced stub
}
// ✅ SEGREGATED — one small role interface per capability.
interface Printer { print(): void; }
interface Scanner { scan(): void; }
interface Fax { fax(): void; }
// Each class implements ONLY the roles it can truly fulfil.
class SimplePrinter2 implements Printer {
print(): void { /* real work — nothing forced, nothing throws */ }
}
class PhotoCopier implements Printer, Scanner, Fax {
print(): void { /* ... */ }
scan(): void { /* ... */ }
fax(): void { /* ... */ }
}References & further reading
5 sources- Articleen.wikipedia.org
Interface segregation principle — Wikipedia
The concise definition plus the origin story: ISP came out of Robert C. Martin's work for Xerox, whose fat Job class was the original multifunction-device problem.
- Articleblog.cleancoder.com
The Interface Segregation Principle — Robert C. Martin (cleancoder)
Uncle Bob, who coined the principle, on the SOLID set it belongs to. The original ISP paper is also linked from his archive at objectmentor.
- Docsbaeldung.com
Interface Segregation Principle in Java — Baeldung
A hands-on, code-first walkthrough that refactors a fat interface into role interfaces in Java — the closest companion to this lesson's examples.
- Docsrefactoring.guru
Refactoring: Extract Interface — Refactoring.Guru
The mechanical refactoring you apply to do ISP: pull a cohesive subset of methods into a new interface. Pairs with their SOLID overview.
- Talkyoutube.com
SOLID Principles: Interface Segregation — video walkthrough
A clear visual explainer of ISP with the printer/scanner example, good for seeing the before-and-after refactor narrated step by step.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 05
What does the Interface Segregation Principle state?
question 02 / 05
A SimplePrinter implements IMultiFunctionDevice { print(); scan(); fax(); } and its scan() body is just throw new UnsupportedOperationException(). What is this a sign of?
question 03 / 05
How does ISP fix the multifunction-device problem?
question 04 / 05
Which other SOLID principle do throwing stubs also violate, and why?
question 05 / 05
What is the danger of over-applying ISP?
0/5 answered