Chapter 02 · Article 06 of 55
Interface Segregation Principle (ISP)
"No client should be forced to depend on methods it does not use." - Robert C. Martin
Article outline14 sections on this page+
"No client should be forced to depend on methods it does not use." - Robert C. Martin
Overview
The Interface Segregation Principle (ISP) is the fourth principle in the SOLID acronym. It states that clients should not be forced to implement interfaces they do not use. Instead of one large, monolithic interface, prefer many small, focused interfaces tailored to specific client needs.
ISP originated from Robert C. Martin's consulting work at Xerox in the early 1990s. The printer system had a single Job class used by multiple subsystems - stapling, printing, faxing - each forced to recompile when any part of the interface changed. The solution was to segregate the interface into role-specific contracts.
Core idea: Design interfaces from the client's perspective, not the implementor's. Each interface should represent a single role or capability that a client actually needs.
Formal definition: When a class implements an interface, every method in that interface should be meaningful to that class. If a method is irrelevant, the interface is too broad and should be split.
Problem It Solves
| Problem | Description |
|---|---|
| Fat interfaces | A single interface accumulates methods for multiple unrelated responsibilities, forcing all implementors to provide every method |
| Unnecessary dependencies | Clients depend on methods they never call, creating coupling that triggers recompilation and redeployment on unrelated changes |
| Forced implementations | Classes must implement methods that have no logical meaning for them, leading to throw new NotImplementedException() or empty method bodies |
| Fragile systems | A change to one method in a fat interface ripples across all implementors, even those unaffected by the change |
| Testing burden | Mocking a fat interface requires stubbing dozens of irrelevant methods just to test one behavior |
When to Use / When NOT to Use
| When to Use | When NOT to Use |
|---|---|
| Interface has methods that some implementors leave empty or throw exceptions | Interface is genuinely cohesive - all methods relate to one role |
| Different clients use different subsets of the interface | You have only one or two implementors and they use all methods |
| You need to mock/stub interfaces in tests and the fat interface makes it painful | Splitting would create single-method interfaces that always appear together |
| Adding a new method to an interface would break unrelated implementors | The system is small, stable, and unlikely to grow new implementors |
| Multiple teams own different implementors of the same interface | Premature abstraction - you don't yet know the variation points |
| Plugin/extension systems where third parties implement your contracts | Performance-critical code where vtable indirection matters (rare) |
Key Concepts & Theory
Interface Pollution
Interface pollution occurs when methods are added to an existing interface for convenience rather than cohesion. Over time, the interface becomes a dumping ground. Signs include:
- Implementors with
// not applicablecomments - Methods that only 1 out of N implementors actually uses
- Parameters ignored by most implementations
Role Interfaces vs Header Interfaces
| Role Interface | Header Interface |
|---|---|
| Defined by what the client needs | Defined by what the class can do |
| Small, focused, client-specific | Large, mirrors the full class API |
| Promotes decoupling | Creates tight coupling |
Example: IReadableRepository, IWritableRepository | Example: IRepository with 20 methods |
Role interfaces are the ISP-compliant approach. Each interface represents a role the implementing class plays in a specific collaboration.
Cohesion in Interfaces
An interface is cohesive when all its methods are used together by at least one client. The test: if you can identify a client that uses methods A and B but never C, then C likely belongs in a separate interface.
Heuristic: Group methods by which clients call them, not by which class implements them.
ASCII Class Diagram
Violation - Fat Interface
┌─────────────────────────┐
│ <<interface>> │
│ IWorker │
├─────────────────────────┤
│ + work(): void │
│ + eat(): void │
│ + sleep(): void │
└─────────┬───────────────┘
│ implements
┌─────┴──────┐
│ │
┌───▼───┐ ┌────▼────┐
│ Human │ │ Robot │
├───────┤ ├─────────┤
│work() │ │work() │
│eat() │ │eat() X│ ← throws / no-op
│sleep()│ │sleep() X│ ← throws / no-op
└───────┘ └─────────┘
Robot is forced to implement eat() and sleep() - methods that have no meaning for it.
Segregated Interfaces
┌───────────────┐ ┌───────────────┐ ┌────────────────┐
│ <<interface>> │ │ <<interface>> │ │ <<interface>> │
│ IWorkable │ │ IFeedable │ │ ISleepable │
├───────────────┤ ├───────────────┤ ├────────────────┤
│ + work() │ │ + eat() │ │ + sleep() │
└──────┬────────┘ └──────┬────────┘ └───────┬────────┘
│ │ │
│ ┌────────────┼────────────────────┘
│ │ │
┌────▼─────▼────┐ ┌───▼────┐
│ Human │ │ Robot │
│ implements: │ │ impl: │
│ IWorkable │ │IWorkable│
│ IFeedable │ └────────┘
│ ISleepable │
└───────────────┘
Robot implements only IWorkable. Human implements all three. Each class depends only on what it needs.
Pseudocode Implementation
BAD - Fat Interface
interface IMultiFunctionDevice {
print(doc: Document): void
scan(doc: Document): Image
fax(doc: Document, number: String): void
staple(doc: Document): void
photocopy(doc: Document, copies: int): void
}
class SimplePrinter implements IMultiFunctionDevice {
print(doc) { /* actual printing logic */ }
scan(doc) { throw NotSupportedException("Cannot scan") }
fax(doc, number) { throw NotSupportedException("Cannot fax") }
staple(doc) { throw NotSupportedException("Cannot staple") }
photocopy(doc, copies) { throw NotSupportedException("Cannot copy") }
}
class OldFaxMachine implements IMultiFunctionDevice {
print(doc) { throw NotSupportedException("Cannot print") }
scan(doc) { throw NotSupportedException("Cannot scan") }
fax(doc, number) { /* actual fax logic */ }
staple(doc) { throw NotSupportedException("Cannot staple") }
photocopy(doc, copies) { throw NotSupportedException("Cannot copy") }
}
Problems: SimplePrinter is forced to implement 4 irrelevant methods. Adding emailDoc() to the interface breaks every implementor.
GOOD - Segregated Interfaces
interface IPrinter {
print(doc: Document): void
}
interface IScanner {
scan(doc: Document): Image
}
interface IFax {
fax(doc: Document, number: String): void
}
interface IStapler {
staple(doc: Document): void
}
// Classes implement ONLY what they support
class SimplePrinter implements IPrinter {
print(doc) { /* printing logic */ }
}
class OldFaxMachine implements IFax {
fax(doc, number) { /* fax logic */ }
}
class MultiFunctionPrinter implements IPrinter, IScanner, IFax, IStapler {
print(doc) { /* printing logic */ }
scan(doc) { /* scanning logic */ return image }
fax(doc, number) { /* fax logic */ }
staple(doc) { /* staple logic */ }
}
// Client depends only on what it needs
class PrintService {
constructor(private printer: IPrinter) {} // doesn't know about scan/fax
printReport(report) {
this.printer.print(report)
}
}
Benefits: SimplePrinter has no dead methods. PrintService depends only on IPrinter - easy to test, easy to swap implementations.
Real-World Examples
1. Repository Interfaces
// Instead of one IRepository<T> with 15 methods:
interface IReadRepository<T> {
findById(id): T
findAll(): List<T>
exists(id): boolean
}
interface IWriteRepository<T> {
save(entity: T): void
delete(id): void
}
// Read-only reporting service depends only on IReadRepository
class ReportingService {
constructor(private repo: IReadRepository<Order>) {}
}
2. Event Handlers
// BAD: IApplicationLifecycle with onStart, onStop, onPause, onResume, onCrash
// GOOD:
interface IStartable { onStart(): void }
interface IStoppable { onStop(): void }
// A background job only cares about start/stop
class BackgroundJob implements IStartable, IStoppable { ... }
3. Plugin Systems
// Plugin host exposes fine-grained extension points
interface IToolbarPlugin { addToolbarItems(): List<Item> }
interface IMenuPlugin { addMenuItems(): List<Item> }
interface IThemePlugin { getTheme(): Theme }
// A plugin implements only the hooks it needs
class DarkModePlugin implements IThemePlugin {
getTheme() { return darkTheme }
}
Comparison
ISP vs SRP
| Aspect | SRP | ISP |
|---|---|---|
| Scope | Class level | Interface/contract level |
| Focus | A class should have one reason to change | A client should not depend on unused methods |
| Violation symptom | God class doing too much | Implementors throwing NotSupported |
| Fix | Split the class | Split the interface |
| Relationship | ISP often enables SRP - segregated interfaces lead to focused classes |
ISP vs Interface Adapter Pattern
| Aspect | ISP | Adapter Pattern |
|---|---|---|
| When applied | Design time - proactive | After the fact - reactive |
| Approach | Design small interfaces from the start | Wrap a fat interface with a narrow adapter |
| Trade-off | Requires upfront thought about client needs | Adds indirection but works with legacy code |
| Use together | When you own the interface, apply ISP directly. When you consume a third-party fat interface, use Adapter to present a narrow view to your code |
Advantages & Disadvantages
| Advantages | Disadvantages |
|---|---|
| Reduced coupling - clients depend only on what they use | Interface explosion - too many tiny interfaces can overwhelm developers |
| Easier testing - mock only relevant methods | Discoverability - harder to find all capabilities of a class |
| Safer refactoring - changes to one interface don't ripple to unrelated clients | Upfront design effort - requires understanding client needs early |
| Better reusability - small interfaces compose flexibly | Potential over-engineering for simple systems |
| Clearer contracts - each interface communicates a specific role | Composition overhead in languages without multiple inheritance |
| Parallel development - teams work on separate interfaces independently | May require facade/aggregate interfaces for convenience |
Constraints & Edge Cases
Interface Explosion
Splitting too aggressively creates dozens of single-method interfaces that always travel together. If every client that uses IReader also uses IWriter, they should probably be one interface.
Guideline: Merge interfaces that are always consumed together by every client. Split interfaces that have at least one client using a subset.
Finding the Right Granularity
- Too coarse: Implementors have dead methods → violates ISP
- Too fine: Every method is its own interface → cognitive overload, no practical benefit
- Just right: Each interface maps to a distinct client role
Language Constraints
- Java/C#: Multiple interface implementation is natural; ISP maps cleanly
- Go: Implicit interfaces make ISP almost automatic - define interfaces at the consumer site
- Python/JS: Duck typing means ISP is about documentation and discipline, not compiler enforcement
- C++: Multiple inheritance of abstract classes works but watch for diamond problems
Versioning
Adding a method to a published interface is a breaking change. ISP minimizes this risk because each interface is small and stable. When extension is needed, create a new interface (e.g., IPrinterV2 extends IPrinter) rather than modifying the original.
Interview Follow-ups
Q1: How does ISP relate to the Dependency Inversion Principle?
Model Answer: DIP says depend on abstractions, not concretions. ISP refines which abstractions to depend on. Without ISP, you might depend on an abstraction (satisfying DIP) that is still too broad, coupling you to irrelevant methods. ISP ensures the abstraction is right-sized for the client. Together: DIP tells you to use interfaces; ISP tells you to make them narrow. A high-level module should depend on a role interface tailored to its needs, not a fat abstraction that happens to be an interface.
Q2: You inherit a legacy system with a 30-method interface implemented by 12 classes. How do you refactor toward ISP without breaking everything?
Model Answer:
- Identify client clusters - group the 30 methods by which clients actually call them. Tools like dependency analysis or IDE "find usages" help.
- Extract role interfaces - create new small interfaces for each cluster. Have the fat interface extend them all (
IFatInterface extends IPrinter, IScanner, ...). - Migrate clients gradually - change client code to depend on the narrow interface instead of the fat one. This is non-breaking because the fat interface still exists.
- Deprecate the fat interface - once all clients use narrow interfaces, the fat interface becomes an empty aggregation and can be removed.
This is the "parallel change" or "expand-contract" refactoring pattern applied to interfaces.
Q3: Can ISP be violated even with small interfaces?
Model Answer: Yes. If you have a 3-method interface but one implementor never uses one of those methods, ISP is still violated. Size is a heuristic, not the rule. The principle is about relevance to the client, not method count. Conversely, a 10-method interface used entirely by every client and every implementor does not violate ISP.
Q4: How would you apply ISP in a microservices architecture?
Hints:
- Think of API contracts (REST/gRPC) as interfaces between services
- A service exposing one massive API endpoint with 20 fields forces consumers to parse irrelevant data
- Consider BFF (Backend for Frontend) pattern - tailored APIs per client type
- GraphQL as a mechanism that lets clients self-segregate by querying only needed fields
Q5: What is the relationship between ISP and the concept of "interface pollution" in Go?
Hints:
- Go idiom: "Accept interfaces, return structs"
- Define interfaces at the consumer site, not the provider site
io.Reader(1 method) vs a hypotheticalio.Everything- Go stdlib is ISP by design- Implicit satisfaction means you never force an implementor to declare unused methods
- Contrast with Java where the provider defines the interface upfront
Counter Questions to Ask Interviewer
Use these to demonstrate depth and steer the conversation:
-
"Are we discussing ISP in the context of a library/SDK consumed by external teams, or internal application code? The cost of getting interface granularity wrong is much higher for published APIs."
-
"Does the system have multiple deployment units? ISP matters more when a fat interface causes unnecessary recompilation or redeployment across module boundaries."
-
"What language and type system are we working with? In Go, ISP is nearly free due to structural typing. In Java, it requires explicit design. In dynamic languages, it's a convention."
-
"Are there existing consumers of this interface we need to maintain backward compatibility with, or is this greenfield design?"
-
"How frequently do new implementors get added? If the interface is closed to new implementations, the cost of a fat interface is lower."
References & Whitepapers
-
Martin, R.C. - Agile Software Development: Principles, Patterns, and Practices (2002), Chapter 12: The Interface Segregation Principle. The canonical source with the Xerox printer case study.
-
Martin, R.C. - "The Interface Segregation Principle" (1996), C++ Report. Original paper describing the Xerox consulting engagement where ISP was formalized.
-
Martin, R.C. - Clean Architecture (2017), Chapter 10. Updated discussion of ISP in modern contexts including microservices.
-
Xerox Printer Case Study - The original motivating example: a
Jobclass used by print, staple, and fax subsystems. Changes to stapling forced recompilation of print code. Solution: segregate intoIPrintJob,IStapleJob,IFaxJob. -
Gamma, E. et al. - Design Patterns: Elements of Reusable Object-Oriented Software (1994). The Adapter and Facade patterns are complementary techniques when ISP cannot be applied directly.
-
Bloch, J. - Effective Java (3rd ed., 2018), Item 20: "Prefer interfaces to abstract classes." Discusses interface design principles aligned with ISP.
Related Topics
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Dependency Inversion Principle
- Adapter Pattern
- Facade Pattern
- CQRS Pattern
Word count: ~3000 | Last updated: 2026-05-31