Interface Segregation Principle (ISP)
The Problem It Solves
One interface to rule the domain:
interface Worker {
void work(); void eat(); void sleep();
}
class Robot implements Worker {
public void work() { assemble(); }
public void eat() { throw new UnsupportedOperationException(); } // ← lie
public void sleep(){ throw new UnsupportedOperationException(); } // ← lie
}
Every Worker-typed client drags dependencies on eating and sleeping it never uses. Every implementer that can’t eat must fake it. The fat interface forces lies into the codebase, and lies surface as runtime exceptions. ISP (Robert C. Martin): clients should not be forced to depend on methods they do not use — split interfaces by client role.
The Split
FAT INTERFACE SEGREGATED BY ROLE
┌─────────────────┐ ┌──────────────┐ ┌──────────────┐
│ Worker │ │ Workable │ │ Feedable │
│ work() eat() │ │ + work() │ │ + eat() │
│ sleep() │ └──────△───────┘ └──────△───────┘
└───△────△────△───┘ │ │
│ │ │ ┌──────┴────┐ ┌─────┴─────┐
Robot Human Manager │ Robot │ │ Human │
(fakes 2) (fine)(never sleeps?)│ implements│ │implements │
│ Workable │ │ both │
└───────────┘ └───────────┘
Robot implements only what it truthfully does. Clients depending on Workable never see eat() at all — their dependency shrank with the interface.
Mechanics
interface Workable { void work(); }
interface Feedable { void eat(); }
class Robot implements Workable {
public void work() { assemble(); }
}
class Human implements Workable, Feedable {
public void work() { think(); }
public void eat() { lunch(); }
}
Multiple small interfaces compose; Java’s unlimited implements makes segregation free of the single-inheritance constraint.
Where Fat Interfaces Hide
- Service interfaces grown by accretion:
UserServicewith 25 methods — every mock in every test implements 25 stubs. - Framework callbacks: listeners demanding ten no-op overrides → fixed by adapter defaults or split listener types (
MouseListenervsMouseMotionListenerin AWT). - Repository interfaces:
findByName,countByStatus,exportCsvused by different callers who each need one-tenth.
Trade-offs
| Fat interface | Segregated | |
|---|---|---|
| Interface count | Few | Many small ones |
| Implementer honesty | Forced stubs | Only real capabilities |
| Client dependency surface | Everything | Exactly what’s used |
| Evolution risk | Any change touches all | Change isolated per role |
| Over-segregation cost | — | Churn re-bucketing tiny interfaces |
Detection Signals
- Empty/throwing override bodies.
- Interfaces named after things (
Manager) rather than capabilities (Workable). - Test mocks with dozens of unused stubbed methods.
- Documentation comments saying “not applicable for X implementations.”
Interview Framing
- The robot example is the canonical opener; recognizing it instantly is expected.
- Senior nuance: ISP pairs with OCP — segregated interfaces are also the stable extension seams; fat interfaces freeze wrongly because every addition touches all implementers.
The Craft Behind ISP
Segregation says split; lean design decides how small, named after what, and how it evolves. A lean interface contains exactly the methods one client role needs, named for the capability, with a documented contract — and nothing else. Every extra method is a permanent tax on all implementers and all readers.
Role-Based Discovery
Interfaces are discovered from client needs, not provider capability:
WRONG question RIGHT question
"What can PaymentGateway do?" "What does RefundService need?"
→ list 20 gateway methods → refund(Money, PaymentRef) only
→ interface mirrors vendor SDK → one-method role interface,
→ every client sees everything vendor adapter maps to it
The right-hand process yields interfaces matching domain vocabulary instead of third-party API shapes — vendors become swappable details.
Minimality Audit
| Check | Question |
|---|---|
| Per method | Which concrete client calls this? Name it or cut it |
| Per parameter | Is this data the client legitimately knows? |
| Cohesion | Would removing any method break the role’s story? |
| Naming | Does the name state capability (Workable) not implementation (WorkerManager)? |
| Return types | Does any return leak internal types (entities, builders)? |
Contract Documentation
A lean interface still needs its semantics written:
/**
* Atomically reserves seats; hold expires after ttlMinutes if unconfirmed.
* Thread-safe. Throws SeatsUnavailableException without side effects.
*/
interface SeatInventory {
Hold reserve(ShowId show, List<SeatId> seats);
}
Atomicity, expiry, thread-safety, failure atomicity — clients code against these words. Undocumented interfaces force implementer-reading, defeating segregation’s decoupling purpose.
Default Methods as Evolution Tool
Interfaces must evolve without breaking implementers — Java 8’s answer:
interface SeatInventory {
Hold reserve(ShowId show, List<SeatId> seats);
/** @since 2.0 bulk variant; default delegates per-seat for old impls */
default List<Hold> reserveAll(ShowId show, List<SeatId> seats) {
return seats.stream().map(s -> reserve(show, List.of(s))).toList();
}
}
Old implementations keep compiling via the default; performance-critical ones override. This is how Collection gained streams without breaking the ecosystem.
Trade-offs
| Pressure | Lean answer |
|---|---|
| ”Just add it to the interface” (convenience) | New narrow interface or default method — never accretion |
| Two roles share 80% of methods | Share the overlapping interface; keep role-specific ones separate |
| Vendor SDK shapes leaking in | Adapter mapping vendor → domain role interfaces |
Interview Framing
- Reviewing a candidate-drawn interface and asking “who calls each of these?” is a standard senior probe — designing interfaces that survive it wins.
- Citing
ComparatorvsComparableas pre-built segregation (external strategy vs internal identity ordering) shows the pattern recognized in the wild.
Premium Content
Unlock Interface Segregation Principle and all premium lessons with a subscription.
From ₹199.99/year — See plans