Interface Segregation Principle
No client should be forced to depend on interfaces it does not use.
Interface Segregation Principle (ISP): A Complete Guide with Examples in 11 Programming Languages
The Interface Segregation Principle states that no client should be forced to depend on methods it does not use. Instead of one large, general-purpose interface, prefer many small, focused interfaces tailored to specific client needs. A robot forced to implement eat() and sleep() simply because it shares a Worker interface with humans is a classic ISP violation — and every empty stub or thrown exception it produces is the consequence. ISP is the discipline of keeping contracts minimal, honest, and aligned with what clients actually need.
In this comprehensive guide, we'll explore ISP with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common violations, and clear guidance on when and how to apply this principle.
Table of Contents
- What is the Interface Segregation Principle?
- Why Follow ISP?
- ISP Comparison
- ISP Explained
- Class Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is the Interface Segregation Principle?
ISP is the fourth of the five SOLID principles, introduced by Robert C. Martin. Its statement is direct: clients should not be forced to depend on interfaces they do not use.
The classic violation is the "fat interface" — a single interface with methods for every imaginable operation on a concept. A Worker interface with work(), eat(), sleep(), and sign_documents() works fine for human employees. But a RobotWorker must also implement eat() and sleep() even though robots do neither — forcing it to either provide empty method bodies or throw NotImplementedError, both of which are wrong. The interface is lying: it promises capabilities that some implementors simply don't have.
The deeper problem is coupling. A client that depends on a fat interface is coupled to every method in it, even the ones it never calls. If an unrelated method changes, the client is still affected — it must be recompiled, reviewed, and retested for a change that has nothing to do with it. The build breaks, the tests run, the diff is reviewed — all for a method the client doesn't use.
ISP's solution is decomposition: split the fat interface into focused, role-specific sub-interfaces. Each client implements only what it actually needs. A Workable interface for work scheduling. A Eatable interface for break management. A DocumentSigner interface for legal workflows. Classes that can do all of it implement all of them. Classes that can only work implement only Workable — with no stubs, no exceptions, and no lies.
Why Follow ISP?
ISP provides several critical benefits:
- Reduced Coupling: Clients depend only on the methods they actually call. A change to an unrelated method in a fat interface no longer ripples through clients that never use it.
- Honest Contracts: Small interfaces make only the promises they can keep. No implementation needs to provide an empty stub or throw
NotImplementedErrorfor a method it doesn't support. - Smaller Mocks in Tests: A test that needs only a
Readabledoesn't have to stubsave(),update(),delete(), andsearch(). Mock setup shrinks to only what the test exercises. - Clearer Communication: Small, focused interfaces communicate precise requirements — "this component needs to read" vs. "this component needs everything the store can do."
- Easier Incremental Extension: Adding a method to a small interface affects only the classes that implement that interface. Adding a method to a fat interface forces every implementing class to be updated.
- Natural Ally of LSP: When interfaces are the right size, implementations don't need to violate LSP with no-op or exception-throwing methods. ISP and LSP are deeply complementary.
ISP Comparison
Let's compare the fat interface approach with the segregated approach:
| Dimension | Fat Interface | Segregated Interfaces |
|---|---|---|
| Implementation burden | Every implementor must provide all methods | Each implementor provides only what it genuinely does |
| Forced stubs | Common — unsupported methods throw or silently no-op | Absent — every method on every interface is meaningful |
| Test complexity | Mocks must stub all methods, even unused ones | Mocks are minimal — only the interface under test |
| Change impact | Adding a method forces updates across every implementor | Adding a method affects only the implementors of that sub-interface |
| Interface naming | Broad names: "Worker", "Repository", "Service" | Precise names: "Workable", "Readable", "Printable" |
| Coupling | High — all clients depend on everything | Low — each client depends on only what it uses |
| LSP compliance | At risk — empty/throwing stubs break LSP | Natural — there is nothing to stub |
Key Distinctions:
- ISP vs. SRP: SRP is about class responsibilities; ISP is about interface scope. A class with a single responsibility still naturally leads to a focused interface — but ISP asks the same question from the client's perspective: does this client need everything this interface offers?
- ISP vs. LSP: ISP prevents the LSP violations that fat interfaces cause. When an interface demands methods an implementor can't meaningfully provide, LSP breaks. Segregating the interface removes the demand entirely.
- ISP vs. Interface Explosion: ISP does not mean one method per interface. The goal is cohesion — each interface groups methods that serve a single client need.
Readablecan haveread(),readAll(), andexists()and still be a single, focused interface. The question is always: do these methods serve the same type of client?
ISP Explained
What Makes an Interface "Fat"?
A fat interface has methods that serve multiple distinct types of clients. The practical warning signs are:
- Forced empty implementations: Some implementors write do-nothing bodies for methods that don't apply to them.
- Exception-throwing placeholders: Implementations throw
UnsupportedOperationException,NotImplementedError,fatalError, or similar for methods they can't support. - Interface name requires "And":
ReadableAndWritable,SearchableAndSortable,WorkerAndSigner— the name already reveals multiple concerns. - Most callers use only a subset: If 80% of the callers of an 8-method interface only call 2 of those methods, the interface is oversized for those callers.
How to Segregate a Fat Interface
- List what the fat interface currently offers — write out every method.
- Identify distinct client types — which clients call which methods? Group the callers.
- Define one interface per distinct client need — name each after the role it plays, not the class that implements it.
- Let classes implement multiple interfaces — a
FullRepositorycan implementReadable,Writable, andSearchablesimultaneously. The interfaces are small; the class is complete. - Update callers to depend on the narrowest interface they need — a read-only service should accept
Readable, notRepository.
Role Interfaces vs. Header Interfaces
Robert Martin distinguishes between two kinds of interfaces:
- Header Interface: A one-to-one mapping of a class's public methods into an interface. The interface mirrors the class. This is not ISP — it's just extracting what already exists.
- Role Interface: A minimal interface expressing what a specific client needs from its collaborators. Different clients define different role interfaces from the same class. This is genuine ISP.
The shift is from thinking "what does this class offer?" to "what does this client need?" ISP is client-driven interface design.
Class Diagram
Here's the UML class diagram showing a worker system refactored to comply with ISP:
Beginner-Friendly Example
Let's illustrate ISP with the classic Worker example. We start with the fat interface violation — one interface for everything — then show the correctly segregated design where each client gets only what it needs.
from __future__ import annotations
from abc import ABC, abstractmethod
# ── ❌ ISP VIOLATION: Fat Worker interface ─────────────────────────
# Every implementor must provide all four methods — including ones it can't support.
class Worker(ABC):
@abstractmethod
def work(self) -> None: ...
@abstractmethod
def eat(self) -> None: ...
@abstractmethod
def sleep(self) -> None: ...
@abstractmethod
def sign_documents(self) -> None: ...
class HumanWorker(Worker):
def work(self) -> None: print(" 👷 Human working")
def eat(self) -> None: print(" 🍔 Human eating")
def sleep(self) -> None: print(" 😴 Human sleeping")
def sign_documents(self) -> None: print(" ✍️ Human signing")
class RobotWorker(Worker):
def work(self) -> None: print(" 🤖 Robot working")
def eat(self) -> None: raise NotImplementedError("Robots don't eat!") # ISP violation
def sleep(self) -> None: raise NotImplementedError("Robots don't sleep!") # ISP violation
def sign_documents(self) -> None: raise NotImplementedError("Robots can't sign!") # ISP violation
# ── ✅ ISP COMPLIANT: Segregated, role-based interfaces ────────────
# Each interface serves one distinct client need
class Workable(ABC):
@abstractmethod
def work(self) -> None: ...
class Eatable(ABC):
@abstractmethod
def eat(self) -> None: ...
class Sleepable(ABC):
@abstractmethod
def sleep(self) -> None: ...
class DocumentSigner(ABC):
@abstractmethod
def sign_documents(self) -> None: ...
# Human implements every interface it's capable of
class HumanEmployee(Workable, Eatable, Sleepable, DocumentSigner):
def __init__(self, name: str) -> None:
self._name = name
def work(self) -> None: print(f" 👷 {self._name} working")
def eat(self) -> None: print(f" 🍔 {self._name} eating")
def sleep(self) -> None: print(f" 😴 {self._name} sleeping")
def sign_documents(self) -> None: print(f" ✍️ {self._name} signed documents")
# Robot implements ONLY what it can actually do — no stubs, no exceptions
class IndustrialRobot(Workable):
def __init__(self, model: str) -> None:
self._model = model
def work(self) -> None: print(f" 🤖 Robot {self._model} working")
# Each manager depends ONLY on the narrow interface it needs
class WorkScheduler:
"""Only needs workers to work — unaware of eating or signing."""
def run_shift(self, workers: list[Workable]) -> None:
print(" === Work Shift ===")
for worker in workers:
worker.work() # Correct for both humans and robots — no isinstance needed
class BreakScheduler:
"""Only needs workers that can eat — never sees robots."""
def lunch_break(self, workers: list[Eatable]) -> None:
print(" === Lunch Break ===")
for worker in workers:
worker.eat()
class LegalDepartment:
"""Only needs workers that can sign — focused contract."""
def collect_signatures(self, signers: list[DocumentSigner]) -> None:
print(" === Signature Collection ===")
for signer in signers:
signer.sign_documents()
# ── Client ─────────────────────────────────────────────────────────
if __name__ == "__main__":
alice = HumanEmployee("Alice")
bob = HumanEmployee("Bob")
robot = IndustrialRobot("R2-D2")
print("=== ISP Compliant Worker System ===\n")
# Robot and humans both work — WorkScheduler doesn't care which
WorkScheduler().run_shift([alice, bob, robot])
print()
# Only humans eat — robots never even appear here
BreakScheduler().lunch_break([alice, bob])
print()
# Only humans sign — a clean, honest contract
LegalDepartment().collect_signatures([alice, bob])
Production-Ready Example
Now let's build a document repository system — a common production scenario where ISP violations are widespread. A full-featured repository can read, write, search, and export. But many clients only need one of those capabilities. Without ISP, a read-only analytics service must depend on the full repository — including save(), delete(), and export() methods it never calls. With ISP, each client declares exactly what it needs.
🐍 Python
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import date
from typing import Optional
# ── Domain Model ───────────────────────────────────────────────────
@dataclass
class Document:
id: str
title: str
content: str
tags: list[str] = field(default_factory=list)
created_at: date = field(default_factory=date.today)
# ── ❌ ISP VIOLATION: One fat repository interface ─────────────────
class DocumentRepository(ABC):
"""Fat interface: every client depends on everything."""
@abstractmethod
def find_by_id(self, doc_id: str) -> Optional[Document]: ...
@abstractmethod
def find_all(self) -> list[Document]: ...
@abstractmethod
def save(self, document: Document) -> None: ...
@abstractmethod
def delete(self, doc_id: str) -> bool: ...
@abstractmethod
def search(self, query: str) -> list[Document]: ...
@abstractmethod
def export_csv(self) -> str: ...
# ReadOnlyAnalytics only needs to read — but is forced to implement save/delete/export:
class BadReadOnlyAnalytics(DocumentRepository):
def __init__(self, docs: list[Document]) -> None:
self._docs = {d.id: d for d in docs}
def find_by_id(self, doc_id: str) -> Optional[Document]:
return self._docs.get(doc_id)
def find_all(self) -> list[Document]:
return list(self._docs.values())
def search(self, query: str) -> list[Document]:
return [d for d in self._docs.values() if query.lower() in d.title.lower()]
# Forced to implement these even though analytics never writes or exports:
def save(self, document: Document) -> None:
raise NotImplementedError("Analytics is read-only!") # ISP violation
def delete(self, doc_id: str) -> bool:
raise NotImplementedError("Analytics is read-only!") # ISP violation
def export_csv(self) -> str:
raise NotImplementedError("Analytics doesn't export!") # ISP violation
# ── ✅ ISP COMPLIANT: Segregated role interfaces ────────────────────
# Role 1: Reading — used by analytics, viewers, search results
class DocumentReader(ABC):
@abstractmethod
def find_by_id(self, doc_id: str) -> Optional[Document]: ...
@abstractmethod
def find_all(self) -> list[Document]: ...
# Role 2: Writing — used by editors and import pipelines
class DocumentWriter(ABC):
@abstractmethod
def save(self, document: Document) -> None: ...
@abstractmethod
def delete(self, doc_id: str) -> bool: ...
# Role 3: Searching — used by search endpoints and autocomplete
class DocumentSearcher(ABC):
@abstractmethod
def search(self, query: str) -> list[Document]: ...
@abstractmethod
def find_by_tag(self, tag: str) -> list[Document]: ...
# Role 4: Exporting — used by reporting services
class DocumentExporter(ABC):
@abstractmethod
def export_csv(self) -> str: ...
@abstractmethod
def export_json(self) -> str: ...
# Full implementation — implements all roles for a complete repository
class InMemoryDocumentRepository(DocumentReader, DocumentWriter,
DocumentSearcher, DocumentExporter):
def __init__(self) -> None:
self._store: dict[str, Document] = {}
# ── DocumentReader ──
def find_by_id(self, doc_id: str) -> Optional[Document]:
return self._store.get(doc_id)
def find_all(self) -> list[Document]:
return list(self._store.values())
# ── DocumentWriter ──
def save(self, document: Document) -> None:
self._store[document.id] = document
print(f" [Repo] Saved: '{document.title}'")
def delete(self, doc_id: str) -> bool:
if doc_id in self._store:
print(f" [Repo] Deleted: {doc_id}")
del self._store[doc_id]
return True
return False
# ── DocumentSearcher ──
def search(self, query: str) -> list[Document]:
q = query.lower()
return [d for d in self._store.values()
if q in d.title.lower() or q in d.content.lower()]
def find_by_tag(self, tag: str) -> list[Document]:
return [d for d in self._store.values() if tag in d.tags]
# ── DocumentExporter ──
def export_csv(self) -> str:
lines = ["id,title,tags,created_at"]
for d in self._store.values():
lines.append(f"{d.id},{d.title},{';'.join(d.tags)},{d.created_at}")
return "\n".join(lines)
def export_json(self) -> str:
import json
return json.dumps(
[{"id": d.id, "title": d.title, "tags": d.tags} for d in self._store.values()],
indent=2
)
# ── Services — each depends only on the interface it needs ─────────
class AnalyticsService:
"""Only reads documents — depends solely on DocumentReader."""
def __init__(self, reader: DocumentReader) -> None:
self._reader = reader # Has no idea save/delete/export exist
def summarize(self) -> None:
docs = self._reader.find_all()
print(f"\n [Analytics] Total documents: {len(docs)}")
for doc in docs:
print(f" • [{doc.id}] {doc.title} ({len(doc.tags)} tags)")
class DocumentEditService:
"""Writes documents — depends solely on DocumentWriter."""
def __init__(self, writer: DocumentWriter) -> None:
self._writer = writer
def create(self, doc_id: str, title: str, content: str, tags: list[str]) -> Document:
doc = Document(id=doc_id, title=title, content=content, tags=tags)
self._writer.save(doc)
return doc
def remove(self, doc_id: str) -> bool:
return self._writer.delete(doc_id)
class SearchService:
"""Searches documents — depends solely on DocumentSearcher."""
def __init__(self, searcher: DocumentSearcher) -> None:
self._searcher = searcher
def search(self, query: str) -> None:
results = self._searcher.search(query)
print(f"\n [Search] '{query}' → {len(results)} result(s):")
for doc in results:
print(f" • {doc.title}")
def by_tag(self, tag: str) -> None:
results = self._searcher.find_by_tag(tag)
print(f"\n [Search] tag:'{tag}' → {len(results)} result(s):")
for doc in results:
print(f" • {doc.title}")
class ReportingService:
"""Exports documents — depends solely on DocumentExporter."""
def __init__(self, exporter: DocumentExporter) -> None:
self._exporter = exporter
def generate_csv_report(self) -> None:
csv = self._exporter.export_csv()
print(f"\n [Report] CSV export ({len(csv.splitlines())} rows):")
print(f" {csv.splitlines()[0]}") # Header only for brevity
# ── Client ─────────────────────────────────────────────────────────
if __name__ == "__main__":
repo = InMemoryDocumentRepository()
# The one concrete class satisfies all four role interfaces
editor = DocumentEditService(writer=repo)
analytics = AnalyticsService(reader=repo)
search = SearchService(searcher=repo)
reporting = ReportingService(exporter=repo)
print("=== Document Repository (ISP Compliant) ===")
editor.create("doc-1", "SOLID Principles", "A guide to SOLID.", tags=["architecture", "oop"])
editor.create("doc-2", "Design Patterns", "Creational and behavioral patterns.", tags=["patterns", "oop"])
editor.create("doc-3", "Clean Code", "Readable, maintainable code.", tags=["practices"])
analytics.summarize()
search.search("patterns")
search.by_tag("oop")
reporting.generate_csv_report()
Real-World Use Cases
ISP appears in well-designed systems wherever a concept has multiple clients with different needs:
Repository Pattern: Instead of one IRepository<T> with find, save, delete, search, and export, split into IReadRepository<T>, IWriteRepository<T>, and ISearchRepository<T>. A read-only dashboard service depends only on IReadRepository. An import pipeline depends only on IWriteRepository. Neither is burdened by the other's methods.
Authentication Providers: Instead of one IAuthProvider with login(), logout(), register(), resetPassword(), verify2FA(), and revokeToken(), create focused interfaces: ILoginProvider, IRegistrationProvider, IPasswordResetProvider. A login endpoint only depends on ILoginProvider. A registration flow only depends on IRegistrationProvider.
Notification Systems: Instead of one INotifier with sendEmail(), sendSms(), sendPush(), and sendSlack(), define a IEmailNotifier, ISmsNotifier, and IPushNotifier. Services declare only the channel they need — an order confirmation service only needs IEmailNotifier. A two-factor auth service only needs ISmsNotifier.
Media Players: Instead of one IMediaPlayer with play(), pause(), stop(), record(), stream(), exportFile(), create IPlayable, IRecordable, IStreamable. A playback UI component only depends on IPlayable. A recording UI depends on IRecordable. Neither knows the other exists.
File Storage: Instead of one IFileStorage with read(), write(), delete(), list(), compress(), encrypt(), and generateUrl(), define IReadableStorage, IWritableStorage, and IUrlGenerator. An archive reader only depends on IReadableStorage. A signed URL generator only depends on IUrlGenerator.
Language-Specific Mistakes and Anti-Patterns
❌ Mistake #1: Raising NotImplementedError for Unsupported Interface Methods
# BAD: ReadOnlyStore is forced to declare methods it can't support
class Store(ABC):
@abstractmethod
def read(self, key: str) -> str: ...
@abstractmethod
def write(self, key: str, value: str) -> None: ...
@abstractmethod
def delete(self, key: str) -> bool: ...
class ReadOnlyStore(Store):
def read(self, key: str) -> str:
return self._backend.get(key)
def write(self, key: str, value: str) -> None:
raise NotImplementedError("This store is read-only!") # ISP violation
def delete(self, key: str) -> bool:
raise NotImplementedError("This store is read-only!") # ISP violation
# GOOD: Split the interface; ReadOnlyStore is a complete implementation
class Readable(ABC):
@abstractmethod
def read(self, key: str) -> str: ...
class Writable(ABC):
@abstractmethod
def write(self, key: str, value: str) -> None: ...
@abstractmethod
def delete(self, key: str) -> bool: ...
class ReadOnlyStore(Readable):
def read(self, key: str) -> str:
return self._backend.get(key)
# No write/delete — the interface doesn't demand them
class FullStore(Readable, Writable):
def read(self, key: str) -> str: ...
def write(self, key: str, value: str) -> None: ...
def delete(self, key: str) -> bool: ...
❌ Mistake #2: Giant ABC with Many Abstractmethods Serving Different Callers
# BAD: One abstract base for an entire notification system
class Notifier(ABC):
@abstractmethod
def send_email(self, to: str, subject: str, body: str) -> None: ...
@abstractmethod
def send_sms(self, phone: str, message: str) -> None: ...
@abstractmethod
def send_push(self, device_id: str, title: str, body: str) -> None: ...
@abstractmethod
def send_slack(self, channel: str, message: str) -> None: ...
# Every concrete notifier must implement all four channels
# An EmailNotifier is forced to implement send_sms, send_push, send_slack
# GOOD: One interface per channel; compose them on the class that supports multiple
class EmailNotifier(ABC):
@abstractmethod
def send_email(self, to: str, subject: str, body: str) -> None: ...
class SmsNotifier(ABC):
@abstractmethod
def send_sms(self, phone: str, message: str) -> None: ...
class PushNotifier(ABC):
@abstractmethod
def send_push(self, device_id: str, title: str, body: str) -> None: ...
# Only the full multi-channel notifier implements all three
class MultiChannelNotifier(EmailNotifier, SmsNotifier, PushNotifier):
def send_email(self, to, subject, body): print(f"[EMAIL] {to}: {subject}")
def send_sms(self, phone, message): print(f"[SMS] {phone}: {message}")
def send_push(self, device_id, title, body): print(f"[PUSH] {device_id}: {title}")
Frequently Asked Questions
Q1: How many methods should an interface have?
There is no fixed number — the right question is cohesion. An interface should group methods that serve the same distinct client need. Readable can have read(), readAll(), and exists() because they all serve the same type of client (a reader). The moment you add write() to Readable, you've changed who the interface serves. The guiding test: would every client of this interface call all of these methods? If some clients never call a subset of the methods, those methods belong in a separate interface.
Q2: Doesn't ISP lead to an explosion of interfaces?
It can, if over-applied. The goal is not to maximize the number of interfaces but to draw boundaries where client needs genuinely diverge. If all clients call all methods of an interface, the interface is appropriately sized — don't split it. ISP becomes valuable when some clients never use some methods, forcing empty stubs or exception-throwing placeholders. That's the signal that segregation is warranted, not the raw method count.
Q3: How is ISP different from SRP at the interface level?
SRP is about a class having one reason to change. ISP is about an interface having one reason to be depended on. They're complementary views of the same cohesion principle — SRP from the implementation perspective, ISP from the client perspective. A class can satisfy SRP (one responsibility) while depending on a fat interface — ISP addresses the interface side of that coupling.
Q4: Does ISP apply to abstract classes too?
Yes. Abstract classes that define too many abstract methods force the same implementation burden as fat interfaces. A concrete subclass is forced to implement every abstract method, even those irrelevant to its purpose. The fix is the same: split the abstract class into focused base classes or interfaces and let concrete classes inherit or implement only the relevant ones.
Q5: How does Go's approach compare to ISP?
Go's approach is the purest expression of ISP: interfaces are defined at the consumer side, not the provider side. The standard library's single-method interfaces (io.Reader, io.Writer, io.Closer) are canonical ISP in practice. In Go, you ask "what does this function need from its collaborator?" — and the answer defines a minimal interface at the call site. No large provider-defined interface is needed. Each consumer defines its own focused dependency.
Q6: Can a class implement too many interfaces?
A class implementing many small interfaces is not itself an ISP violation — it means the class is capable of many things. HumanEmployee implementing Workable, Eatable, Sleepable, and DocumentSigner is fine — the class does all four things. The violation would be in the interfaces themselves (if they were fat) or in callers that depend on more interfaces than they need. The class implementing many focused interfaces is a sign of a rich, capable type, not a design problem.
Q7: Is ISP still relevant in dynamic languages without formal interfaces?
Yes. In dynamic languages like Python and JavaScript, ISP is expressed through class shape — what methods a class exposes. A class with a work() method but no eat() method is an ISP-compliant robot worker even without an explicit interface declaration. Callers that use duck typing and access only the specific methods they need are practicing ISP implicitly. The principle is about coupling, not about formal syntax — it applies regardless of whether interfaces are explicit.
Q8: How do I retrofit ISP onto a legacy fat interface?
Incremental extraction: identify the clients of the fat interface and group them by which methods they call. For each distinct group, define a new, focused interface containing only those methods. Have the existing concrete classes implement the new focused interfaces alongside the old fat one — this is backward compatible. Then migrate callers one by one to depend on the new narrow interfaces. Once all callers are migrated, deprecate or remove the fat interface. This can be done gradually alongside regular feature work.
Key Takeaways
🎯 Core Concept: The Interface Segregation Principle states that clients should not be forced to depend on methods they do not use. A fat interface with too many methods burdens every implementor — including those that can only support a subset — with forced stubs, silent no-ops, or exception-throwing placeholders. ISP demands decomposition: one focused interface per distinct client need, so every method in every interface is meaningful to every implementor.
🔑 Key Benefits:
- No forced stubs: Every method on every interface is meaningfully implemented — no
NotImplementedError, no silentreturn; - Minimal mocks in tests: Tests mock only the narrow interface they exercise — not a 10-method repository when the test only reads
- Reduced change impact: Adding a method to a focused interface only affects implementors of that interface — not the entire codebase
- Honest contracts: Small interfaces communicate precise capability — "this type can be read" vs. "this type can do everything"
- Natural LSP compliance: When interfaces are appropriately sized, LSP violations from empty stubs are structurally impossible
🌐 Language-Specific Highlights:
- Python: Split fat ABCs into focused abstract bases; avoid long
@abstractmethodlists that serve different callers; never useNotImplementedErroras a substitute for proper interface segregation - TypeScript: Define role interfaces at the consumer side (
UserReader,UserWriter) not the provider side (UserRepository); TypeScript's structural typing makes this natural - Java: Use multiple focused interfaces instead of one monolithic repository interface; leverage
defaultmethods only for genuine optional behavior, not to avoid ISP violations - JavaScript: Use class shape (duck typing) to express ISP — classes expose only what they genuinely do; filter by capability at call sites rather than implementing unused methods
- C#: Use
IQueryService/ICommandServicesplits in CQRS; inject the narrowest interface needed into each controller method - PHP: Type-hint method parameters with focused interfaces (
CacheReadernotCache); use PHP 8.1readonlyconstructor promotion to keep data-focused interfaces clean - Go: Define interfaces at the consumer, not the provider — this is Go's idiomatic style; single-method interfaces are idiomatic and encouraged
- Rust: Bound generic parameters only on the traits actually used inside the function; avoid supertrait bounds that force broader capabilities than needed
- Dart: Use
abstract interface classfor each focused role; avoid abstract classes with many abstract methods serving different clients - Swift: Avoid protocol default implementations that silently mask ISP violations; prefer multiple focused protocols over one big protocol with defaults
- Kotlin: Avoid default method bodies that paper over ISP violations; use separate interfaces and let classes opt into each one they genuinely support
📋 When to Apply ISP:
- An implementation is forced to provide
throw new NotImplementedError()for methods it can't support - Tests for one feature must mock unrelated methods to satisfy a fat interface
- Multiple callers use only different subsets of a single interface's methods
- Adding a new method to an interface forces changes across many implementing classes
- The interface name requires "And" to describe what it does
⚠️ When NOT to Over-Apply ISP:
- All clients of an interface use all of its methods — don't split for the sake of splitting
- The additional interfaces would be so narrow (single-method) that they add more complexity than they remove coupling
- The two "roles" always change together and always appear together — they're one cohesive concern
- Splitting would create an explosion of tiny types that are harder to navigate than a slightly broad interface
🛠️ Best Practices Across Languages:
- Define interfaces at the consumer side: Ask "what does this client need?" rather than "what does this class offer?" — the client's need defines the right interface
- Name interfaces after roles, not implementations:
Readable,Writable,Searchablecommunicate the role;DocumentRepositorycommunicates the implementation - Use multiple interface implementation freely: A class implementing five focused interfaces is a sign of a capable, honest design — not a design smell
- Watch for
NotImplementedErrorand silent no-ops: These are the symptoms that reveal the ISP violation; fix the interface, not the stub - Segregate test doubles: Your mock/stub classes should implement the narrowest possible interface; if your mock needs 8 methods, the interface is too broad
💡 Common Pitfalls:
- The "default implementation" escape hatch: Using protocol/interface default methods to avoid implementing unsupported methods — this hides the violation instead of fixing it
- Provider-side interface design: Defining the interface as "everything the class can do" rather than "everything the client needs to call" — the opposite of ISP
- The
implements Everythingclass: A class forced to implement a 12-method interface while only 3 methods are meaningful is a fat interface victim - Not revisiting interfaces as clients evolve: An interface that was right for two clients may become fat when a third client that needs only a subset is added — ISP applies continuously, not just at design time
The Interface Segregation Principle keeps contracts honest and dependencies minimal. In a codebase that follows ISP, every interface is a precise declaration of a client's needs, every implementation is a complete and truthful fulfillment of a contract, and every test mock is as small as the feature it tests.
Want to dive deeper into SOLID principles? Check out our comprehensive guides on:
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Dependency Inversion Principle (DIP) Each guide includes examples in all 11 programming languages with language-specific best practices.