Interface Segregation Principle: No Fat Interfaces
The Fat Remote Control Problem
Imagine a TV remote with 50 buttons.
π― System Scenario & Requirements
System Goal
Imagine a TV remote with 50 buttons.
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- Imagine a TV remote with 50 buttons.
- You only ever use 5: power, volume, channel up/down, and input select.
- ISP says: don't give every device the full remote .
Constraints & Assumptions
- ISP is closely related to: Rule of thumb: If you ask "when would a class mock or stub this method just to satisfy the interface?", that method doesn't belong in that interface.
- When Fat Interfaces Are Acceptable π ISP Decision Flow: When to Split an Interface The flowchart captures the single question that drives every ISP refactoring decision: do all clients use all the methods?
β‘ Execution Stages & Decision Trace
Stage 1 β The Classic Violation: The Printer Interface
SimplePrinter can only print β but the Machine interface forces it to "implement" 4 other operations it can't do.
Any code that calls simplePrinter.fax() fails at runtime rather than at compile time.
Deep Dive
Scenario context
Imagine a TV remote with 50 buttons. You only ever use 5: power, volume, channel up/down, and input select. ISP says: don't give every device the full remote. Give each device the buttons it actually needs.
In software, a fat interface forces every implementing class to provide methods it doesn't need, creating:
- Stub implementations that throw
UnsupportedOperationException. - Tight coupling β changes toβ¦
This walkthrough focuses on The Fat Remote Control Problem.
Before making a design choice, clarify:
- Imagine a TV remote with 50 buttons.
- You only ever use 5: power, volume, channel up/down, and input select.
- ISP says: don't give every device the full remote .
Concepts in play: design-principles-solid, jvm-internals-and-garbage-collection
Stage 1 β The Classic Violation: The Printer Interface
- SimplePrinter can only print β but the Machine interface forces it to "implement" 4 other operations it can't do.
- Any code that calls simplePrinter.fax() fails at runtime rather than at compile time.
- π Fat Interface Violation: SimplePrinter Forced to Stub Four Methods Both SimplePrinter and OfficePrinter implement the fat Machine interface, but the similarity ends there.
- OfficePrinter can legitimately fulfil all five methods, while SimplePrinter is forced to throw UnsupportedOperationException in scan() , fax() , copy() , and staple() β four methods it cannot meaningfully support.
public interface Machine {
void print(Document d);
void scan(Document d);
void fax(Document d);
void copy(Document d);
void staple(Document d);
}
public class SimplePrinter implements Machine {
public void print(Document d) { /* works */ }
public void scan(Document d) { throw new UnsupportedOperationException(); }
public void fax(Document d) { throw new UnsupportedOperationException(); }
public void copy(Document d) { throw new UnsupportedOperationException(); }
public void staple(Document d){ throw new UnsupportedOperationException(); }
}
classDiagram
class Machine {
<>
+print(d)
+scan(d)
+fax(d)
+copy(d)
+staple(d)
}
class SimplePrinter {
+print(d)
+scan(d) throws
+fax(d) throws
+copy(d) throws
+staple(d) throws
}
class OfficePrinter {
+print(d)
+scan(d)
+fax(d)
+copy(d)
+staple(d)
}
Machine <|.. SimplePrinter
Machine <|.. OfficePrinter
Observable outcome: OfficePrinter can legitimately fulfil all five methods, while SimplePrinter is forced to throw UnsupportedOperationException in scan() , fax() , copy() , and staple() β four methods it cannot meaningfully support.
Stage 2 β Applying ISP: Split by Role
- After the split, SimplePrinter implements only Printable β the single capability it genuinely supports β while OfficePrinter implements Printable , Scannable , and Faxable to reflect its real hardware capabilities.
- Any code holding a Printable reference can safely accept either class, and no class is ever forced to provide a method it cannot meaningfully execute.
- Now: - SimplePrinter compiles and works with zero stubs.
- OfficePrinter implements everything it genuinely supports.
// Segregated interfaces β each is a single capability
public interface Printable { void print(Document d); }
public interface Scannable { void scan(Document d); }
public interface Faxable { void fax(Document d); }
public interface Copyable { void copy(Document d); }
public interface Stapleable { void staple(Document d); }
// Simple printer: only what it can do
public class SimplePrinter implements Printable {
public void print(Document d) { /* full implementation */ }
}
// All-in-one office printer: combines all
public class OfficePrinter implements Printable, Scannable, Faxable, Copyable, Stapleable {
public void print(Document d) { /* ... */ }
public void scan(Document d) { /* ... */ }
public void fax(Document d) { /* ... */ }
public void copy(Document d) { /* ... */ }
public void staple(Document d) { /* ... */ }
}
classDiagram
class Printable {+print(d)}
class Scannable {+scan(d)}
class Faxable {+fax(d)}
SimplePrinter ..|> Printable
OfficePrinter ..|> Printable
OfficePrinter ..|> Scannable
OfficePrinter ..|> Faxable
Observable outcome: - OfficePrinter implements everything it genuinely supports.
Stage 3 β Deep Dive: Python Protocol-Based ISP
- Python uses structural subtyping via Protocol (PEP 544) β no explicit implements required: SimplePrinter never declared it implements Printable .
- The type checker confirms structural compatibility at analysis time without runtime overhead.
- Notice that OfficePrinter satisfies both Printable and Scannable β Python's Protocol lets you express the same ISP split as Java's explicit interfaces, but with zero inheritance boilerplate.
from typing import Protocol
class Printable(Protocol):
def print(self, doc: str) -> None: ...
class Scannable(Protocol):
def scan(self) -> str: ...
class SimplePrinter:
def print(self, doc: str) -> None:
print(f"Printing: {doc}")
class OfficePrinter:
def print(self, doc: str) -> None:
print(f"Printing: {doc}")
def scan(self) -> str:
return "scanned content"
def send_to_printer(device: Printable, doc: str) -> None:
device.print(doc)
def run_scanner(device: Scannable) -> str:
return device.scan()
send_to_printer(SimplePrinter(), "Invoice.pdf") # OK β only print needed
send_to_printer(OfficePrinter(), "Report.pdf") # OK β also satisfies Printable
run_scanner(OfficePrinter()) # OK β satisfies Scannable
# run_scanner(SimplePrinter()) # Type error β SimplePrinter has no scan()
Observable outcome: Notice that OfficePrinter satisfies both Printable and Scannable β Python's Protocol lets you express the same ISP split as Java's explicit interfaces, but with zero inheritance boilerplate.
Stage 4 β ISP Interface Segregation Flow
- The transformation from a fat interface to segregated role-based interfaces β and how implementations choose only what they need.
- The left-to-right flow traces the refactoring decision for the Machine interface.
- OfficePrinter answers "YES" to supporting all capabilities and takes the direct path β it implements Printable , Scannable , and Faxable because it genuinely provides all three.
- SimplePrinter answers "NO" and routes through the split, arriving at Printable alone, with no stubs and no forced coupling to operations its hardware cannot perform.
flowchart LR
A[Machine fat interface] --> B{Does class support all?}
B -- YES --> C[OfficePrinter implements all 5]
B -- NO --> D[Split into role interfaces]
D --> E[Printable]
D --> F[Scannable]
D --> G[Faxable]
E --> H[SimplePrinter implements Printable only]
C --> E
C --> F
C --> G
Observable outcome: SimplePrinter answers "NO" and routes through the split, arriving at Printable alone, with no stubs and no forced coupling to operations its hardware cannot perform.
Stage 5 β Real-World Application: ISP in Production Systems
- ISP applies everywhere interfaces define contracts between components.
- Kubernetes analogy: A Pod spec in Kubernetes follows ISP β you only declare the capabilities you need (volumes, network policies, security contexts).
- Each concern is a separate, composable specification.
- You don't get a monolithic "configure everything" spec that forces you to set all 200 fields.
Observable outcome: You don't get a monolithic "configure everything" spec that forces you to set all 200 fields.
Stage 6 β Hands-On: Segregate a Fat Interface
- Here is a fat IUserService interface.
- Problem: A read-only admin dashboard must implement createUser , deleteUser , sendWelcomeEmail .
- A background job that only resets passwords must implement findById , listAll , sendWelcomeEmail , and getAuditLog .
- ISP-compliant split: Key benefit: AdminDashboardService cannot accidentally call deleteUser β the interface doesn't include it.
// π Fat interface β forces all implementers to support everything
public interface IUserService {
User findById(String id);
List<User> searchByName(String name);
List<User> listAll();
void createUser(User user);
void updateUser(User user);
void deleteUser(String id);
void resetPassword(String userId, String newPassword);
void sendWelcomeEmail(String userId);
List<String> getAuditLog(String userId);
}
public interface IUserReader {
User findById(String id);
List<User> searchByName(String name);
List<User> listAll();
}
public interface IUserWriter {
void createUser(User user);
void updateUser(User user);
void deleteUser(String id);
}
public interface IUserPasswordManager {
void resetPassword(String userId, String newPassword);
}
public interface IUserNotifier {
void sendWelcomeEmail(String userId);
}
public interface IUserAuditor {
List<String> getAuditLog(String userId);
}
// Admin dashboard only needs reads
public class AdminDashboardService implements IUserReader { ... }
// Full user management service implements all
public class UserManagementService implements IUserReader, IUserWriter,
IUserPasswordManager, IUserNotifier, IUserAuditor { ... }
Observable outcome: ISP-compliant split: Key benefit: AdminDashboardService cannot accidentally call deleteUser β the interface doesn't include it.
End-to-end outcome
After working through the stages, you should be able to explain:
- UnsupportedOperationException is the ISP alarm.
- Any time you write throw new UnsupportedOperationException() in an interface implementation, that method doesn't belong in that interface.
Alternatives considered
Decision Guide
Decision Guide
Failure and degraded modes
ISP is closely related to:
- Detection: Rule of thumb: If you ask "when would a class mock or stub this method just to satisfy the interface?", that method doesn't belong in that interface.
- User or operator impact: Rule of thumb: If you ask "when would a class mock or stub this method just to satisfy the interface?", that method doesn't belong in that interface.
When Fat Interfaces Are Acceptable
- Detection: π ISP Decision Flow: When to Split an Interface The flowchart captures the single question that drives every ISP refactoring decision: do all clients use all the methods?
- User or operator impact: π ISP Decision Flow: When to Split an Interface The flowchart captures the single question that drives every ISP refactoring decision: do all clients use all the methods?
