Interface Segregation Principle (ISP) Explained

Updated 2026-03-11

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.

Prerequisites: Understanding of interfaces/abstract base classes in Python, basic inheritance concepts, familiarity with the Single Responsibility Principle, and knowledge of polymorphism.

After this topic: Identify when interfaces are too broad and violate ISP, design multiple focused interfaces instead of monolithic ones, refactor existing code to follow ISP, and explain the benefits of interface segregation in system design interviews.

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:

  • Workable interface with work()
  • Eatable interface with eat()
  • Sleepable interface with sleep()

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 NotImplementedError or return None
  • 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.