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

ProblemDescription
Fat interfacesA single interface accumulates methods for multiple unrelated responsibilities, forcing all implementors to provide every method
Unnecessary dependenciesClients depend on methods they never call, creating coupling that triggers recompilation and redeployment on unrelated changes
Forced implementationsClasses must implement methods that have no logical meaning for them, leading to throw new NotImplementedException() or empty method bodies
Fragile systemsA change to one method in a fat interface ripples across all implementors, even those unaffected by the change
Testing burdenMocking a fat interface requires stubbing dozens of irrelevant methods just to test one behavior

When to Use / When NOT to Use

When to UseWhen NOT to Use
Interface has methods that some implementors leave empty or throw exceptionsInterface is genuinely cohesive - all methods relate to one role
Different clients use different subsets of the interfaceYou 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 painfulSplitting would create single-method interfaces that always appear together
Adding a new method to an interface would break unrelated implementorsThe system is small, stable, and unlikely to grow new implementors
Multiple teams own different implementors of the same interfacePremature abstraction - you don't yet know the variation points
Plugin/extension systems where third parties implement your contractsPerformance-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 applicable comments
  • Methods that only 1 out of N implementors actually uses
  • Parameters ignored by most implementations

Role Interfaces vs Header Interfaces

Role InterfaceHeader Interface
Defined by what the client needsDefined by what the class can do
Small, focused, client-specificLarge, mirrors the full class API
Promotes decouplingCreates tight coupling
Example: IReadableRepository, IWritableRepositoryExample: 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

AspectSRPISP
ScopeClass levelInterface/contract level
FocusA class should have one reason to changeA client should not depend on unused methods
Violation symptomGod class doing too muchImplementors throwing NotSupported
FixSplit the classSplit the interface
RelationshipISP often enables SRP - segregated interfaces lead to focused classes

ISP vs Interface Adapter Pattern

AspectISPAdapter Pattern
When appliedDesign time - proactiveAfter the fact - reactive
ApproachDesign small interfaces from the startWrap a fat interface with a narrow adapter
Trade-offRequires upfront thought about client needsAdds indirection but works with legacy code
Use togetherWhen 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

AdvantagesDisadvantages
Reduced coupling - clients depend only on what they useInterface explosion - too many tiny interfaces can overwhelm developers
Easier testing - mock only relevant methodsDiscoverability - harder to find all capabilities of a class
Safer refactoring - changes to one interface don't ripple to unrelated clientsUpfront design effort - requires understanding client needs early
Better reusability - small interfaces compose flexiblyPotential over-engineering for simple systems
Clearer contracts - each interface communicates a specific roleComposition overhead in languages without multiple inheritance
Parallel development - teams work on separate interfaces independentlyMay 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:

  1. Identify client clusters - group the 30 methods by which clients actually call them. Tools like dependency analysis or IDE "find usages" help.
  2. Extract role interfaces - create new small interfaces for each cluster. Have the fat interface extend them all (IFatInterface extends IPrinter, IScanner, ...).
  3. 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.
  4. 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 hypothetical io.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:

  1. "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."

  2. "Does the system have multiple deployment units? ISP matters more when a fat interface causes unnecessary recompilation or redeployment across module boundaries."

  3. "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."

  4. "Are there existing consumers of this interface we need to maintain backward compatibility with, or is this greenfield design?"

  5. "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

  1. 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.

  2. Martin, R.C. - "The Interface Segregation Principle" (1996), C++ Report. Original paper describing the Xerox consulting engagement where ISP was formalized.

  3. Martin, R.C. - Clean Architecture (2017), Chapter 10. Updated discussion of ISP in modern contexts including microservices.

  4. Xerox Printer Case Study - The original motivating example: a Job class used by print, staple, and fax subsystems. Changes to stapling forced recompilation of print code. Solution: segregate into IPrintJob, IStapleJob, IFaxJob.

  5. 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.

  6. Bloch, J. - Effective Java (3rd ed., 2018), Item 20: "Prefer interfaces to abstract classes." Discusses interface design principles aligned with ISP.



Word count: ~3000 | Last updated: 2026-05-31