Interface Segregation Principle (ISP)

7 min read

Reading Progress0%
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5

Interface Segregation Principle (ISP)

1. What Is It?

The Principle states that clients should not be forced to depend on interfaces they do not use. Robert C. Martin's formulation: "many client-specific interfaces are better than one general-purpose ."

ISP addresses the problem of "fat interfaces" — interfaces so large that implementing classes are forced to provide stub implementations for methods they don't support. When a Robot class implements a Worker that includes eat_lunch(), the robot must either implement a nonsensical method or throw an exception — both are wrong. More critically, any client that imports Worker to call eat_lunch() now has an invisible dependency on Robot, even though Robot never meaningfully provides that behavior.

ISP keeps interfaces focused and cohesive: each interface captures a single role or capability that clients actually need.


QUICK CHECK

A Printable interface in a document management system defines three methods: print(), scan(), and fax(). A BasicPrinter class that only supports printing is forced to implement scan() and fax() by throwing UnsupportedOperationException. Which problem does this design most directly illustrate?

Choose one answer

2. How It Works

The core mechanic: split large interfaces by client — who calls these methods, not who implements them. If the HRDepartment module only calls work() and the Cafeteria module only calls eat(), those are two different clients that need two different interfaces.

Mermaid Class Diagram

Python Example

from abc import ABC, abstractmethod


# BEFORE — ISP violation: one fat interface forces Robot to implement eat()

class WorkerBad(ABC):
    @abstractmethod
    def work(self) -> None: ...

    @abstractmethod
    def eat(self) -> None: ...  # Robot can't eat!


class HumanWorkerBad(WorkerBad):
    def work(self) -> None:
        print("Human is working")

    def eat(self) -> None:
        print("Human is eating lunch")


class RobotWorkerBad(WorkerBad):
    def work(self) -> None:
        print("Robot is working")

    def eat(self) -> None:
        # Forced to implement a meaningless method
        raise NotImplementedError("Robots don't eat!")  # LSP violation too!


# AFTER — ISP applied: segregated interfaces by client need

class Workable(ABC):
    @abstractmethod
    def work(self) -> None: ...


class Eatable(ABC):
    @abstractmethod
    def eat(self) -> None: ...


class Chargeable(ABC):
    @abstractmethod
    def charge(self) -> None: ...


class HumanWorker(Workable, Eatable):
    """Human implements work + eat. No dummy methods."""

    def work(self) -> None:
        print("Human is working")

    def eat(self) -> None:
        print("Human is eating lunch")


class RobotWorker(Workable, Chargeable):
    """Robot implements work + charge. Never asked about eating."""

    def work(self) -> None:
        print("Robot is working")

    def charge(self) -> None:
        print("Robot is charging")


# Clients depend only on what they use

class HRDepartment:
    def assign_task(self, worker: Workable) -> None:
        worker.work()  # Only cares about work()


class Cafeteria:
    def serve_lunch(self, eater: Eatable) -> None:
        eater.eat()  # Only cares about eat()


# Usage — clients are completely decoupled from each other's concerns
hr = HRDepartment()
cafeteria = Cafeteria()
human = HumanWorker()
robot = RobotWorker()

hr.assign_task(human)   # Works
hr.assign_task(robot)   # Works — robot is Workable
cafeteria.serve_lunch(human)  # Works
# cafeteria.serve_lunch(robot)  # Type error — robot is not Eatable (caught at design time)

Java Example

// BEFORE — ISP violation: fat interface
public interface WorkerBad {
    void work();
    void eat(); // Robots can't eat!
}

public class RobotWorkerBad implements WorkerBad {
    @Override
    public void work() { System.out.println("Robot working"); }

    @Override
    public void eat() {
        throw new UnsupportedOperationException("Robots don't eat!"); // LSP violation
    }
}


// AFTER — ISP applied: segregated interfaces

public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Chargeable {
    void charge();
}

public class HumanWorker implements Workable, Eatable {
    @Override
    public void work() { System.out.println("Human working"); }

    @Override
    public void eat() { System.out.println("Human eating"); }
}

public class RobotWorker implements Workable, Chargeable {
    @Override
    public void work() { System.out.println("Robot working"); }

    @Override
    public void charge() { System.out.println("Robot charging"); }
}

// HR only depends on Workable
public class HRDepartment {
    public void assignTask(Workable worker) {
        worker.work();
    }
}

// Cafeteria only depends on Eatable
public class Cafeteria {
    public void serveLunch(Eatable eater) {
        eater.eat();
    }
}

QUICK CHECK

A system has a NotificationService interface with three methods: sendEmail(), sendSMS(), and sendPushNotification(). The BillingModule only ever calls sendEmail(), while the MobileApp module only ever calls sendPushNotification(). According to the Interface Segregation Principle, what is the primary criterion for deciding how to split this interface?

Choose one answer

3. Variants & Comparisons

ApproachHow It WorksProsConsBest For
Role interfacesSplit by client role (what callers need)True ISP; minimal couplingRequires more interfacesMost production code
Mixin/Protocol (Python)Structural typing — if it has the method, it qualifiesNo explicit interface declarationContract is implicitPythonic duck-typed code
Header interfacesOne interface per concrete classSimple to createNot really ISP; just a wrapperLegacy code modernization
Default methods (Java 8+)Interface with default implementations for optional methodsReduces forced stub implementationsHides violations; methods exist but do nothingFramework extension points

QUICK CHECK

A Java framework team wants to add optional lifecycle hooks to an existing interface (e.g., onBeforeRequest() and onAfterRequest()). Most implementors won't need these hooks, but the team wants to avoid forcing every implementor to write empty stub methods. Which interface design approach best addresses this without violating the spirit of ISP?

Choose one answer

4. When to Use It (and When NOT To)

Use ISP when:

  • You have implementing classes that leave methods as pass, raise NotImplementedError, or return None for methods they don't support
  • A new class implements an but only needs 2 of its 8 methods
  • Changes to one client's requirements force recompilation/retesting of unrelated clients
  • You're designing a public API or library where clients will implement your interfaces

Anti-patterns:

  • One-method interfaces everywhere: Splitting every single method into its own interface creates an explosion of types and makes unwieldy. ISP is about removing forced dependencies, not maximizing interface count.
  • Ignoring ISP in internal code: ISP matters most at module/package boundaries. Over-applying it to private internal classes adds complexity without benefit.
  • Using default methods to paper over fat interfaces: Java's default methods let you add methods to interfaces without breaking implementors — but if nobody asked for those methods, you've just hidden the fat interface problem.

Decision triggers:

  • "If you see raise NotImplementedError or return None in an interface implementation — reach for ISP."
  • "If a class implements an interface but 3 of its 7 methods are no-ops — the interface is too fat."

QUICK CHECK

A team is building a plugin system where third-party developers implement a StorageBackend interface. The interface has 10 methods, but a developer implementing a read-only S3 backend finds that 4 of the write-related methods have no meaningful implementation — so they leave them as raise NotImplementedError. A colleague suggests using default method implementations (e.g., Java's default keyword) so those 4 methods silently do nothing. Why is this suggestion problematic?

Choose one answer

5. Real-World Usage

Java's java.util collection interfaces: Java splits collection behavior into Iterable, Collection, List, Set, Queue, Deque. Clients that only need to iterate take Iterable<T>. Clients that need ordered access take List<T>. No single "God Collection" forces everyone to depend on every operation. This is textbook ISP.

Python's collections.abc module: Python segregates abstract base classes into Iterable, Iterator, Sized, Container, Mapping, MutableMapping, etc. A class that only needs to be counted implements Sized — no need to implement __iter__ or __contains__.

Spring's ApplicationContext vs BeanFactory: Spring segregates its container interfaces. BeanFactory is the minimal for dependency lookup. ApplicationContext adds event publishing, internationalization, and more. Clients that only do DI take BeanFactory; those needing the full container take ApplicationContext. Clients aren't forced to depend on features they don't use.


QUICK CHECK

A service only needs to retrieve beans by name from a Spring container — it does not require event publishing or internationalization. According to the Interface Segregation Principle, which dependency type should this service accept?

Choose one answer

6. Interview Cheat Sheet

Key sentences to say:

  1. "ISP says split interfaces by the clients that use them, not the classes that implement them. The client's perspective is what drives the split."
  2. "A fat forces implementing classes to stub out methods they don't support — and those stubs are silent bugs waiting to happen."
  3. "In Java, I look for UnsupportedOperationException in implementations — that's almost always an ISP violation."
  4. "ISP and LSP are closely linked: fat interfaces that force meaningless implementations almost always violate LSP too."

Common follow-up questions:

"How is ISP different from SRP?"

SRP is about classes having one reason to change. ISP is about interfaces not forcing clients to depend on methods they don't use. SRP is about the server side (who owns the class); ISP is about the client side (who uses the interface).

"When should you NOT split an interface?"

When all clients need all methods. If every consumer of Shape uses both area() and perimeter(), keeping them in one interface is fine. Split only when you identify a client that needs only a subset.

"Does Python even need ISP given duck typing?"

Python's structural typing means ISP violations are runtime errors (AttributeError) instead of compile-time errors. ISP still matters — it just surfaces differently. Python's Protocol (PEP 544) brings explicit structural typing that makes ISP violations visible early.

Connections to other concepts:

  • ISP reduces the blast radius of SRP violations — a fat interface couples all clients together; segregated interfaces isolate change
  • ISP enables — thin, focused interfaces are easier to mock and invert than fat ones
  • ISP violations and LSP violations often co-occur — a class that inherits a fat interface frequently violates LSP by stubbing unsupported methods
  • In Python, Protocols (PEP 544) and ABCs (collections.abc) are the direct mechanism for applying ISP
Glossary History

Click dotted jargon to save explanations here.

Glossary History

Click dotted jargon to save explanations here.