Interface Segregation Principle (ISP) Explained
TL;DR
The Interface Segregation Principle (ISP) states that clients should not be forced to depend on interfaces they don’t use. Instead of one large interface, create multiple smaller, focused interfaces so classes only implement what they actually need. This reduces coupling and makes code more maintainable.
Core Concept
What is the Interface Segregation Principle?
The Interface Segregation Principle (ISP) is the “I” in SOLID principles. It states: “No client should be forced to depend on methods it does not use.” When an interface becomes too large and contains methods that not all implementers need, you’ve created a fat interface that violates ISP.
Why ISP Matters
Imagine you have a Worker interface with methods work(), eat(), and sleep(). A Robot class implementing this interface would be forced to implement eat() and sleep() even though robots don’t eat or sleep. This creates:
- Unnecessary dependencies: Classes depend on methods they never call
- Rigid design: Changes to unused methods still affect your class
- Confusing APIs: Implementers must provide meaningless implementations
- Testing complexity: You must mock or test methods you don’t use
The Solution: Segregate Interfaces
Instead of one large interface, create multiple role-specific interfaces. Each interface should represent a cohesive set of behaviors. Classes then implement only the interfaces relevant to them.
For the worker example, segregate into:
Workableinterface withwork()Eatableinterface witheat()Sleepableinterface withsleep()
Now HumanWorker implements all three, while Robot implements only Workable.
ISP vs SRP
While related, these principles focus on different aspects:
- SRP: A class should have one reason to change (about class responsibilities)
- ISP: A client shouldn’t depend on unused methods (about interface design)
ISP is about the contract between classes, while SRP is about the internal cohesion of a single class.
Visual Guide
Violating ISP: Fat Interface
classDiagram
class Worker {
<<interface>>
+work()
+eat()
+sleep()
}
class HumanWorker {
+work()
+eat()
+sleep()
}
class Robot {
+work()
+eat() ❌ forced to implement
+sleep() ❌ forced to implement
}
Worker <|.. HumanWorker
Worker <|.. Robot
note for Robot "Robot must implement eat() and sleep()\neven though it doesn't need them"
A fat interface forces Robot to implement irrelevant methods, violating ISP.
Following ISP: Segregated Interfaces
classDiagram
class Workable {
<<interface>>
+work()
}
class Eatable {
<<interface>>
+eat()
}
class Sleepable {
<<interface>>
+sleep()
}
class HumanWorker {
+work()
+eat()
+sleep()
}
class Robot {
+work()
}
Workable <|.. HumanWorker
Eatable <|.. HumanWorker
Sleepable <|.. HumanWorker
Workable <|.. Robot
note for Robot "Robot only implements\nwhat it needs"
Segregated interfaces allow each class to implement only relevant behaviors.
Examples
Example 1: Document Processor (Violating ISP)
from abc import ABC, abstractmethod
# BAD: Fat interface forces all implementers to support all operations
class Document(ABC):
@abstractmethod
def open(self):
pass
@abstractmethod
def save(self):
pass
@abstractmethod
def print(self):
pass
@abstractmethod
def fax(self):
pass
class ModernDocument(Document):
def open(self):
print("Opening document")
def save(self):
print("Saving document")
def print(self):
print("Printing document")
def fax(self):
# Modern systems don't fax, but forced to implement
raise NotImplementedError("Faxing not supported")
class ReadOnlyDocument(Document):
def open(self):
print("Opening read-only document")
def save(self):
# Read-only shouldn't save, but forced to implement
raise NotImplementedError("Cannot save read-only document")
def print(self):
print("Printing document")
def fax(self):
raise NotImplementedError("Faxing not supported")
# Usage shows the problem
doc = ReadOnlyDocument()
doc.open() # Works
try:
doc.save() # Runtime error! Interface promised this would work
except NotImplementedError as e:
print(f"Error: {e}")
Output:
Opening read-only document
Error: Cannot save read-only document
Problem: The interface promises methods that some implementations can’t fulfill. Clients can’t trust the interface contract.
Example 2: Document Processor (Following ISP)
from abc import ABC, abstractmethod
# GOOD: Segregated interfaces for different capabilities
class Openable(ABC):
@abstractmethod
def open(self):
pass
class Saveable(ABC):
@abstractmethod
def save(self):
pass
class Printable(ABC):
@abstractmethod
def print(self):
pass
class Faxable(ABC):
@abstractmethod
def fax(self):
pass
# Modern document implements what it needs
class ModernDocument(Openable, Saveable, Printable):
def open(self):
print("Opening document")
def save(self):
print("Saving document")
def print(self):
print("Printing document")
# Read-only document only implements relevant interfaces
class ReadOnlyDocument(Openable, Printable):
def open(self):
print("Opening read-only document")
def print(self):
print("Printing document")
# Legacy document can support faxing
class LegacyDocument(Openable, Saveable, Printable, Faxable):
def open(self):
print("Opening legacy document")
def save(self):
print("Saving legacy document")
def print(self):
print("Printing document")
def fax(self):
print("Faxing document")
# Type-safe functions that work with specific capabilities
def process_saveable(doc: Saveable):
doc.save()
def process_printable(doc: Printable):
doc.print()
# Usage
modern = ModernDocument()
readonly = ReadOnlyDocument()
legacy = LegacyDocument()
process_printable(modern) # Works
process_printable(readonly) # Works
process_saveable(modern) # Works
# process_saveable(readonly) # Type checker catches this at compile time!
Output:
Printing document
Printing document
Saving document
Benefits: Each class implements only what it needs. Type system prevents calling unsupported operations. No runtime surprises.
Try it yourself: Add a Encryptable interface with an encrypt() method. Create a SecureDocument class that implements Openable, Saveable, and Encryptable.
Example 3: Payment Processing System
from abc import ABC, abstractmethod
from typing import Dict
# GOOD: Segregated payment interfaces
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float) -> bool:
pass
class RefundablePayment(ABC):
@abstractmethod
def refund(self, transaction_id: str, amount: float) -> bool:
pass
class RecurringPayment(ABC):
@abstractmethod
def setup_subscription(self, amount: float, interval: str) -> str:
pass
@abstractmethod
def cancel_subscription(self, subscription_id: str) -> bool:
pass
# Credit card supports all features
class CreditCardProcessor(PaymentProcessor, RefundablePayment, RecurringPayment):
def process_payment(self, amount: float) -> bool:
print(f"Processing ${amount} via credit card")
return True
def refund(self, transaction_id: str, amount: float) -> bool:
print(f"Refunding ${amount} for transaction {transaction_id}")
return True
def setup_subscription(self, amount: float, interval: str) -> str:
sub_id = f"SUB-{amount}-{interval}"
print(f"Setting up ${amount} {interval} subscription: {sub_id}")
return sub_id
def cancel_subscription(self, subscription_id: str) -> bool:
print(f"Cancelling subscription {subscription_id}")
return True
# Cash payments are simple - no refunds or subscriptions
class CashProcessor(PaymentProcessor):
def process_payment(self, amount: float) -> bool:
print(f"Accepting ${amount} cash payment")
return True
# Gift cards support payments and refunds, but not subscriptions
class GiftCardProcessor(PaymentProcessor, RefundablePayment):
def process_payment(self, amount: float) -> bool:
print(f"Processing ${amount} via gift card")
return True
def refund(self, transaction_id: str, amount: float) -> bool:
print(f"Refunding ${amount} to gift card for {transaction_id}")
return True
# Client code works with specific capabilities
def process_order(processor: PaymentProcessor, amount: float):
return processor.process_payment(amount)
def handle_return(processor: RefundablePayment, transaction_id: str, amount: float):
return processor.refund(transaction_id, amount)
def setup_monthly_billing(processor: RecurringPayment, amount: float):
return processor.setup_subscription(amount, "monthly")
# Usage
cc = CreditCardProcessor()
cash = CashProcessor()
gift = GiftCardProcessor()
process_order(cc, 100.00) # Works
process_order(cash, 50.00) # Works
process_order(gift, 75.00) # Works
handle_return(cc, "TXN-123", 100.00) # Works
handle_return(gift, "TXN-456", 75.00) # Works
# handle_return(cash, "TXN-789", 50.00) # Type error - cash doesn't support refunds!
setup_monthly_billing(cc, 29.99) # Works
# setup_monthly_billing(gift, 29.99) # Type error - gift cards don't support subscriptions!
Output:
Processing $100.0 via credit card
Accepting $50.0 cash payment
Processing $75.0 via gift card
Refunding $100.0 for transaction TXN-123
Refunding $75.0 to gift card for TXN-456
Setting up $29.99 monthly subscription: SUB-29.99-monthly
Key insight: Client functions declare exactly what capabilities they need. The type system prevents passing incompatible payment processors.
Try it yourself: Add a FraudCheckable interface with a verify_transaction() method. Implement it for CreditCardProcessor but not for CashProcessor. Create a function that requires both PaymentProcessor and FraudCheckable.
Java/C++ Note: Java uses the interface keyword explicitly. C++ uses pure virtual functions (abstract classes). Python uses ABC and abstractmethod. The principle applies identically across all languages.
Common Mistakes
1. Creating Too Many Tiny Interfaces
Mistake: Over-segregating interfaces to the point where you have one method per interface.
# TOO GRANULAR
class Openable(ABC):
@abstractmethod
def open(self): pass
class Closeable(ABC):
@abstractmethod
def close(self): pass
class Readable(ABC):
@abstractmethod
def read(self): pass
class Writable(ABC):
@abstractmethod
def write(self): pass
# Now you have interface explosion
class File(Openable, Closeable, Readable, Writable):
# Implementing 4 interfaces for basic file operations
pass
Why it’s wrong: This creates maintenance overhead and makes the codebase harder to navigate. Group cohesive operations together.
Better approach: Group related operations that naturally go together.
# BETTER: Cohesive grouping
class FileOperations(ABC):
@abstractmethod
def open(self): pass
@abstractmethod
def close(self): pass
class Readable(ABC):
@abstractmethod
def read(self): pass
class Writable(ABC):
@abstractmethod
def write(self): pass
2. Using NotImplementedError Instead of Segregating
Mistake: Implementing a fat interface and raising exceptions for unsupported methods.
class Bird(ABC):
@abstractmethod
def fly(self): pass
@abstractmethod
def swim(self): pass
class Penguin(Bird):
def fly(self):
raise NotImplementedError("Penguins can't fly")
def swim(self):
print("Swimming")
Why it’s wrong: This violates the interface contract. Clients expect all methods to work. Runtime errors break the Liskov Substitution Principle.
Fix: Segregate the interface so Penguin only implements what it can do.
class Flyable(ABC):
@abstractmethod
def fly(self): pass
class Swimmable(ABC):
@abstractmethod
def swim(self): pass
class Penguin(Swimmable): # Only implements what it can do
def swim(self):
print("Swimming")
3. Confusing ISP with Single Responsibility Principle
Mistake: Thinking ISP means a class should only implement one interface.
# WRONG THINKING: "ISP means one interface per class"
class Document(Openable): # Only one interface
def open(self): pass
# But what about save, print, etc.?
Why it’s wrong: ISP is about interface design, not limiting implementations. A class can implement multiple interfaces if it genuinely needs all those capabilities.
Correct understanding: ISP means don’t force classes to implement methods they don’t need. A class implementing multiple focused interfaces is perfectly fine.
4. Not Considering Client Needs
Mistake: Designing interfaces based on implementation details rather than client requirements.
# WRONG: Designed around implementation
class DatabaseOperations(ABC):
@abstractmethod
def connect(self): pass
@abstractmethod
def execute_query(self): pass
@abstractmethod
def close_connection(self): pass
@abstractmethod
def optimize_indexes(self): pass # Not all clients need this!
Why it’s wrong: Clients that just want to query data are forced to know about index optimization.
Fix: Segregate based on client use cases.
class QueryExecutor(ABC):
@abstractmethod
def execute_query(self): pass
class DatabaseAdmin(ABC):
@abstractmethod
def optimize_indexes(self): pass
5. Ignoring Language Features
Mistake: In Python, not using Protocol or duck typing when appropriate, forcing unnecessary inheritance.
# OVERLY RIGID in Python
from abc import ABC, abstractmethod
class Drawable(ABC):
@abstractmethod
def draw(self): pass
def render(obj: Drawable): # Forces inheritance
obj.draw()
Better in Python: Use Protocol for structural subtyping when you don’t need explicit inheritance.
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
def render(obj: Drawable): # Works with any object that has draw()
obj.draw()
# No inheritance needed
class Circle:
def draw(self):
print("Drawing circle")
render(Circle()) # Works!
Interview Tips
How to Discuss ISP in Interviews
1. Start with the Problem, Not the Solution
When asked about ISP, don’t immediately jump to “many small interfaces.” Start by explaining the problem:
“The Interface Segregation Principle addresses the issue of fat interfaces — when an interface contains methods that not all implementers need. This forces classes to implement methods they don’t use, creating unnecessary coupling and confusing contracts.”
Then present ISP as the solution.
2. Use a Concrete Example
Interviewers love concrete examples. Have a go-to example ready:
“Consider a printer interface with print(), scan(), and fax() methods. A simple printer that only prints would be forced to implement scan() and fax(), probably throwing NotImplementedError. This violates ISP. Instead, we’d create separate Printable, Scannable, and Faxable interfaces. A simple printer implements only Printable, while a multifunction device implements all three.”
3. Connect to Other SOLID Principles
Interviewers often ask how principles relate:
- ISP vs SRP: “SRP is about class cohesion — one reason to change. ISP is about interface contracts — clients shouldn’t depend on unused methods. A class can follow SRP but violate ISP if it implements a fat interface.”
- ISP enables LSP: “ISP helps ensure Liskov Substitution. If an interface has methods a subclass can’t implement, you’ll violate LSP by throwing exceptions.”
- ISP reduces OCP violations: “Fat interfaces make systems rigid. When you add a method to a fat interface, all implementers must change, violating Open/Closed.”
4. Recognize ISP Violations in Code Reviews
Interviewers may show you code and ask for improvements. Red flags for ISP violations:
- Methods that throw
NotImplementedErroror returnNone - Empty method implementations or stub comments like
# TODO: implement later - Conditional logic checking type before calling interface methods
- Comments like “This class doesn’t need this method but…”
5. Know When NOT to Apply ISP
Senior-level interviews test judgment:
“ISP shouldn’t be applied prematurely. If you have only 2-3 implementations and they all need all methods, don’t over-engineer. Apply ISP when you have concrete evidence that different clients need different subsets of functionality. Premature interface segregation creates unnecessary complexity.”
6. Discuss Trade-offs
Show mature thinking:
“ISP increases the number of interfaces, which can make the codebase harder to navigate initially. The benefit is flexibility and maintainability. In a stable domain with few implementations, a larger interface might be acceptable. In a plugin architecture or framework where many third parties implement interfaces, ISP is critical.”
7. Language-Specific Considerations
- Python: Mention Protocol for structural subtyping vs ABC for nominal typing
- Java: Discuss default methods in interfaces (Java 8+) as a way to add methods without breaking implementations
- C++: Talk about pure virtual functions and multiple inheritance challenges
8. Real-World Application
Be ready to discuss where you’ve applied ISP:
“In our payment system, we had a Payment interface with process(), refund(), and setupRecurring(). When we added cash payments, we couldn’t support refunds or recurring. Instead of throwing exceptions, we segregated into PaymentProcessor, RefundablePayment, and RecurringPayment interfaces. This made the type system enforce correct usage at compile time.”
Common Interview Questions:
- “How do you decide when to split an interface?” → When different clients need different subsets of methods
- “Can you have too many interfaces?” → Yes, over-segregation creates maintenance overhead
- “How does ISP relate to dependency injection?” → DI containers work better with focused interfaces; easier to mock and test
- “What’s the difference between ISP and facade pattern?” → Facade simplifies a complex subsystem; ISP is about not forcing unnecessary dependencies
Key Takeaways
-
ISP Core Principle: Clients should not be forced to depend on interfaces they don’t use. Create focused, role-specific interfaces instead of monolithic ones.
-
Red Flag for Violations: If you see
NotImplementedError, empty implementations, or methods that don’t make sense for a class, the interface is too broad and violates ISP. -
Design Strategy: Segregate interfaces based on client needs, not implementation details. Ask “What does this client actually need?” not “What can this class do?”
-
Balance is Key: Don’t over-segregate. Group cohesive operations together. One method per interface is usually too granular; aim for interfaces that represent meaningful capabilities.
-
Type Safety Benefit: Properly segregated interfaces let the type system catch errors at compile time instead of runtime, making code safer and more maintainable.