Tier 1 · Foundations

Core Concepts

OOP Pillars

SOLID Principles

Creational Patterns

Structural Patterns

Behavioral Patterns

Tier 2

Tier 3

Tier 4

Tier 5

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();
    }
}

Each client calls only the capability it needs

HRDepartment uses Workable for humans and robots; Cafeteria uses Eatable for humans. The doc’s Python calls run without dummy methods.

HRDepartment and Cafeteria are two clients that need different interfaces.

Text version
  1. HRDepartment and Cafeteria are two clients that need different interfaces.
  2. The human implements Workable and Eatable because it can work and eat.
  3. The robot implements Workable and Chargeable because it can work and charge.
  4. HRDepartment calls the human through Workable, which requires only the work method.
  5. The same HRDepartment call works for the robot because it also implements Workable.
  6. Cafeteria uses Eatable to serve the human, so the robot needs no eating method.
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
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

Role interfaces remove forced dependencies

Role interfaces split by callers’ needs. Per-class headers mirror public class APIs, while default methods can hide fat interfaces.

Role interfaces follow callers, header interfaces follow concrete classes, and default methods provide inherited implementations.

Text version
  1. Role interfaces follow callers, header interfaces follow concrete classes, and default methods provide inherited implementations.
  2. Role interfaces add more types, header interfaces are not really ISP, and default methods can hide violations.
  3. Header interfaces suit legacy modernization, and default methods suit framework extension points.
  4. For most production code, prefer role interfaces, because they keep coupling minimal.
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."

Unsupported methods are a signal to split

When a class needs only two of eight interface methods, inspect client needs and split by role. Avoid creating one-method interfaces everywhere.

Needing only two of eight methods is a warning, especially when unused methods have dummy bodies.

Text version
  1. Needing only two of eight methods is a warning, especially when unused methods have dummy bodies.
  2. Inspect what each client calls, because caller needs should guide the split.
  3. Create focused role interfaces so clients depend only on capabilities they use.
  4. Yes, split by client role to remove forced dependencies, but do not give every method its own interface.
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

Reading

On this page

My notes

Sign in to keep highlights and notes.

Glossary history

Click dotted jargon to save explanations here.