SOLID

Interface Segregation Principle

No client should be forced to depend on methods it does not use — prefer many small, focused interfaces over one large, general-purpose one.

Small, role-based interfaces.

The Interface Segregation Principle (ISP) says don't force a class to implement methods it doesn't need. Instead of one fat interface with everything, split it into small ones so each client depends only on what it actually uses.

Example: a Machine interface with print(), scan(), fax() forces a simple printer to implement scan and fax. Split into Printer, Scanner, Fax instead.

bad.py
# Problem: fat interface — clients depend on methods they don't use.
class Worker:
    def work(self): ...
    def eat(self): ...
    def sleep(self): ...

class Robot(Worker):
    def work(self): print("weld")
    def eat(self): raise NotImplementedError   # robots don't eat
    def sleep(self): raise NotImplementedError
# Callers of work() are still coupled to eat/sleep existing.
good.py
# Fix: segregate into role interfaces clients actually need.
from abc import ABC, abstractmethod

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

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

class Human(Workable, Eatable):
    def work(self): print("code")
    def eat(self): print("lunch")

class Robot(Workable):            # only the role it can fulfill
    def work(self): print("weld")

def run_shift(worker: Workable):
    worker.work()                 # no forced eat()/sleep()

Fat interfaces couple clients.

A "fat" interface couples all its clients together: when you add or change a method, every implementer must change — even those that don't use it. This forces empty/throwing implementations and needless recompilation. Segregated, role-based interfaces keep clients decoupled.

Rule of thumb Design interfaces around client needs (roles), not around what a class happens to provide.

Role interfaces and cohesion.

  • Role interfaces — define small interfaces per consuming role; a class implements several.
  • ISP ≈ SRP for interfaces — a fat interface usually serves multiple actors, echoing an SRP violation.
  • Coupling to unused code — depending on a fat interface drags in transitive dependencies you don't use.
  • Adapters — when stuck with a fat third-party interface, wrap it in the narrow interface your client needs.

Interview Questions

Filter

No client should be forced to depend on methods it does not use. Prefer several small, specific interfaces over one large, general-purpose ("fat") interface.

An interface that declares many methods spanning multiple responsibilities, so implementers are forced to provide methods they don't need and clients depend on more than they use.

A single IMultiFunctionDevice interface with print(), scan(), and fax(). A basic printer implementing it must supply scan/fax (often as empty or throwing methods), which it doesn't support.

Split it into smaller, role-based interfaces (e.g. Printer, Scanner, Fax). Each class implements only the interfaces it truly supports, and each client depends only on the interface it needs.

Classes implementing interface methods with empty bodies or throwing "not supported" exceptions, because the interface forces methods they don't need.

Yes — that's the intent. A multifunction device can implement Printer, Scanner, and Fax all at once, while a simple printer implements only Printer. Clients depend on whichever interface(s) they need.

It creates unnecessary coupling: the client is tied to a contract whose changes (new/changed methods it never uses) can force it to recompile or adapt. It obscures what the client actually needs, makes testing harder (you must mock more), and can drag in transitive dependencies. Narrow interfaces make dependencies explicit and minimal, so changes ripple less.

ISP is essentially SRP applied to interfaces. A fat interface typically bundles methods serving multiple responsibilities/actors, so it has more than one reason to change. Splitting it into cohesive, role-focused interfaces gives each a single responsibility. High-cohesion interfaces (ISP) and single-responsibility classes (SRP) are the same idea at different levels — group by what changes together and for whom.

An interface defined from the perspective of a client's need (a role it plays in an interaction) rather than from the full capability of the implementing class. For example, an OrderProcessor might depend on a narrow PaymentCharger role interface rather than a whole PaymentGateway. Role interfaces are small and client-driven, which is exactly how ISP suggests designing interfaces.

Small interfaces are trivial to mock/stub: a test only needs to fake the one or two methods the code under test uses, rather than implementing a large interface's entire surface (with dummy methods). This reduces test setup, makes intent clear (the mock shows exactly what the code depends on), and avoids brittle tests that break when unrelated methods on a fat interface change.

Wrap it with an Adapter that exposes only the narrow interface your client needs, delegating to the fat interface internally. Your code depends on the small role interface you define; the adapter absorbs the third-party surface. This restores ISP for your clients (they see only relevant methods), isolates the third-party dependency in one place, and makes testing easy since you mock your narrow interface, not the sprawling external one.

No. ISP calls for interfaces that are cohesive and focused on a single client role — that may be one method or several closely-related methods that always go together. The goal is that clients aren't forced to depend on methods they don't use, not maximal fragmentation. Splitting a cohesive interface into single-method pieces can add needless complexity; group methods that belong to the same role and change together.

Robert Martin formulated ISP while consulting for Xerox on a printer system. A single fat Job class/interface was used by many clients (print jobs, staple jobs, etc.), so any change to Job forced recompilation and redeployment of all clients — builds took an hour and even unrelated changes rippled everywhere. The fix was to introduce client-specific interfaces (e.g. PrintJob, StapleJob) that the Job class implemented, so each client depended only on the operations it used. This decoupled clients from irrelevant changes, dramatically reducing rebuild scope. The lesson: fat interfaces create hidden coupling and build/deploy pain, especially in statically-compiled systems.

ISP and DIP work together: DIP says high-level modules depend on abstractions; ISP says those abstractions should be narrow and client-specific. Ideally the client declares the minimal interface it needs (owned in its layer), and lower-level modules implement it — this is the "role interface owned by the consumer" pattern. That keeps dependencies pointing toward small, stable abstractions and prevents a high-level module from being coupled to a broad service contract full of methods it doesn't use. Combined, they minimize coupling: each module depends only on the exact behavior it requires, expressed as an abstraction it controls, which is the essence of clean/hexagonal "ports."

Fragmenting interfaces too aggressively yields an explosion of tiny interfaces that are hard to discover and navigate, more wiring/boilerplate, and cognitive overhead understanding how many pieces compose into a capability. Classes may implement a long list of interfaces; assembling them and reasoning about the whole becomes harder. There's also a risk of premature segregation guessing client roles that never diverge. The balance: segregate along real client-role boundaries and cohesion, not mechanically. Like SRP, ISP is about "one reason to change / one client concern," not maximum splitting — over-segmentation trades a fat-interface problem for a scattered-interface problem.

The same principle scales: a client should depend only on the operations it needs. REST/GraphQL endpoints can be designed as client-specific (e.g. Backend-for-Frontend, GraphQL letting clients request only needed fields) rather than a monolithic API forcing every consumer to know the whole surface. gRPC services should be cohesive rather than one god-service with dozens of unrelated RPCs. At the service level, a bloated shared contract couples many consumers and makes evolution risky (a change impacts everyone), mirroring the Xerox rebuild problem. ISP thus informs API/versioning design: expose focused, consumer-oriented contracts so services and clients can evolve independently, reducing cross-team coupling in distributed systems.

Under ISP/DIP, the consumer ideally owns (declares) the interface expressing what it needs, and providers implement it — because the interface exists to serve the client's requirements, not to advertise a provider's full capabilities. This "consumer-driven" ownership (seen in hexagonal ports and consumer-driven contracts) keeps interfaces minimal and client-focused, and inverts the dependency so providers depend on the consumer's abstraction. In practice, shared/stable domain abstractions may live in a neutral module both depend on. The anti-pattern is letting a provider export one huge interface reflecting everything it can do; that pushes provider concerns onto clients and reintroduces fat-interface coupling. So ownership should follow need, favoring the client's perspective.

Language features like Java default methods, traits, or mixins let interfaces carry default behavior, which can tempt "just add a method with a default" to a fat interface — implementers aren't forced to write it, so the empty-implementation smell hides. But this doesn't fix the coupling: clients still see methods they don't use, and defaults can mask LSP issues (a default that no-ops or throws). Used well, though, small interfaces with sensible defaults can compose cleanly (mixins for orthogonal capabilities), supporting ISP by letting a class opt into narrow capability interfaces. The guidance: keep interfaces role-focused regardless of defaults; don't use defaults as an excuse to keep interfaces fat, and be wary of default implementations that only exist to satisfy implementers that shouldn't have the method at all.

Clients shouldn't be forced to depend on things they don't use — keep interfaces small and focused on a client's actual needs.

The "I" — Interface Segregation Principle, the fourth of the five SOLID principles.

Yes, conceptually. Even without explicit interface declarations, a function that expects an object to have certain methods defines an implicit interface. Keeping those expectations minimal (only the methods actually used) is ISP applied to duck typing, and it improves testability and decoupling just as much.

A "fat" or "polluted" interface — a large general-purpose interface that many unrelated clients depend on, forcing them to know about (and be coupled to) methods they never call.

In statically-compiled systems, a client that depends on a fat interface must be recompiled/redeployed whenever that interface changes — even for methods it doesn't use. With small, client-specific interfaces, a change only affects the clients of that particular interface, so unrelated clients are insulated. This was the original driver at Xerox, where fat interfaces caused system-wide rebuilds for localized changes.

Yes. A single class can implement several small interfaces; clients then depend only on the relevant interface(s). You don't necessarily have to break up the implementing class — you segregate the interface that different clients see. For example one Document class could implement both Readable and Writable, letting a viewer depend only on Readable. ISP is about the client-facing contracts, which can be narrowed independently of how the implementation is organized.

By depending only on the narrow interface it needs, a client isn't dragged into unrelated concerns and doesn't change when those unrelated methods change. This keeps the client focused and its dependencies aligned with its own single responsibility. Fat interfaces, conversely, blur responsibilities by coupling a client to capabilities outside its concern, giving it extra (indirect) reasons to change.

Read-only clients are forced to depend on mutation methods they never use (and vice versa), coupling them to changes in behavior they don't care about and enabling misuse. Segregating into, say, a read interface and a write interface (aligned with CQRS-style thinking) lets each client depend only on what it needs, prevents accidental mutation through a read-only reference, and lets read and write concerns evolve independently. It also makes intent explicit at the type level.

Martin Fowler distinguishes header interfaces (an interface that mirrors all public methods of a class, typically 1:1, often auto-extracted) from role interfaces (small interfaces defined by what a particular collaboration/role needs). Header interfaces tend to be fat and provider-shaped — they violate ISP because every client sees the whole class surface. Role interfaces embody ISP: they're client-driven and minimal, so each consumer depends only on its slice. Favoring role interfaces yields many small, meaningful contracts and better decoupling, whereas reflexively extracting a header interface per class adds indirection without the ISP benefit. The distinction guides you to design interfaces from the consumer's need, not the implementer's capabilities.

Small, cohesive interfaces are easier to evolve compatibly: you can introduce a new narrow interface for new capabilities rather than growing an existing one and forcing all implementers to change. Fat shared interfaces are brittle — any addition ripples to every implementer and consumer, and removals break many clients. ISP encourages composing capabilities from many focused interfaces so new features arrive as new interfaces (which only interested parties implement/consume), enabling additive, backward-compatible evolution. This mirrors good API versioning: prefer new focused contracts over mutating a broad one, minimizing the blast radius of change across a library's many dependents.

Fat interfaces often force a class to implement methods it can't honor, so it throws "not supported" — which is simultaneously an ISP problem (the interface bundles methods this implementer/its clients don't need) and an LSP problem (the implementation violates the interface's behavioral contract by refusing an operation). ISP prevents the situation structurally: if interfaces are segregated by capability, a class simply doesn't implement the interface for a capability it lacks, so there's no method to stub out and no contract to break. LSP then ensures the implementations it does provide are fully substitutable. Together they eliminate the throw-on-unsupported anti-pattern: ISP removes the forced method, LSP guarantees the remaining ones behave.

When the methods form a genuinely cohesive unit that clients always use together, over-splitting hurts: it scatters a single concept across many interfaces, forces clients to depend on and compose several pieces to do one logical thing, and adds naming/wiring overhead without reducing real coupling. ISP is about not forcing clients to depend on unused methods — if all methods are used together by the relevant clients, they belong in one interface. So the judgment is driven by actual client usage patterns and cohesion, not a rule to minimize methods per interface. A well-sized interface matches a real role; both a bloated interface (unrelated methods) and a shattered one (a cohesive role split apart) are ISP-related design smells.