The Interface Segregation Principle says: no class should be forced to implement methods it doesn’t use.
In simple language, don’t create one giant interface that tries to cover everything. Break it into smaller, focused interfaces so each class only implements what it actually needs.
Think of it like a restaurant menu. A vegetarian shouldn’t have to flip through 10 pages of meat dishes to find their options. Give them a separate vegetarian menu.
The Problem: Fat Interfaces
Let’s say we’re designing a system with different types of workers:
from abc import ABC, abstractmethod
# BAD -- one fat interface for everything
class Worker(ABC):
@abstractmethod
def work(self):
pass
@abstractmethod
def eat(self):
pass
@abstractmethod
def sleep(self):
pass
class HumanWorker(Worker):
def work(self):
print("Writing code...")
def eat(self):
print("Eating lunch...")
def sleep(self):
print("Sleeping...")
class RobotWorker(Worker):
def work(self):
print("Assembling parts...")
def eat(self):
pass # Robots don't eat! But forced to implement this.
def sleep(self):
pass # Robots don't sleep! But forced to implement this.
// BAD -- one fat interface (enforced by convention in JS)
class Worker {
work() {
throw new Error("Must implement");
}
eat() {
throw new Error("Must implement");
}
sleep() {
throw new Error("Must implement");
}
}
class HumanWorker extends Worker {
work() {
console.log("Writing code...");
}
eat() {
console.log("Eating lunch...");
}
sleep() {
console.log("Sleeping...");
}
}
class RobotWorker extends Worker {
work() {
console.log("Assembling parts...");
}
eat() {
// Robots don't eat... but we're forced to have this
}
sleep() {
// Robots don't sleep... but we're forced to have this
}
}
// BAD -- one fat interface
interface Worker {
void work();
void eat();
void sleep();
}
class HumanWorker implements Worker {
public void work() { System.out.println("Writing code..."); }
public void eat() { System.out.println("Eating lunch..."); }
public void sleep() { System.out.println("Sleeping..."); }
}
class RobotWorker implements Worker {
public void work() { System.out.println("Assembling parts..."); }
public void eat() { /* do nothing -- robots don't eat */ }
public void sleep() { /* do nothing -- robots don't sleep */ }
}
RobotWorker is forced to implement eat() and sleep() even though those make zero sense for a robot. Those empty methods are a code smell. They confuse anyone reading the code: “Wait, why is this empty? Is it a bug?”
The Fix: Segregated Interfaces
Split the fat interface into focused ones:
from abc import ABC, abstractmethod
# GOOD -- small, focused interfaces
class Workable(ABC):
@abstractmethod
def work(self):
pass
class Eatable(ABC):
@abstractmethod
def eat(self):
pass
class Sleepable(ABC):
@abstractmethod
def sleep(self):
pass
class HumanWorker(Workable, Eatable, Sleepable):
def work(self):
print("Writing code...")
def eat(self):
print("Eating lunch...")
def sleep(self):
print("Sleeping...")
class RobotWorker(Workable): # only implements what it needs!
def work(self):
print("Assembling parts...")
# Clean. No empty methods. No confusion.
// GOOD -- in JS, we just don't force unnecessary methods
// With TypeScript interfaces, this would be:
// interface Workable { work(): void; }
// interface Eatable { eat(): void; }
// interface Sleepable { sleep(): void; }
class HumanWorker {
work() {
console.log("Writing code...");
}
eat() {
console.log("Eating lunch...");
}
sleep() {
console.log("Sleeping...");
}
}
class RobotWorker {
// only has what it needs -- work()
work() {
console.log("Assembling parts...");
}
}
// GOOD -- small, focused interfaces
interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Sleepable {
void sleep();
}
class HumanWorker implements Workable, Eatable, Sleepable {
public void work() { System.out.println("Writing code..."); }
public void eat() { System.out.println("Eating lunch..."); }
public void sleep() { System.out.println("Sleeping..."); }
}
class RobotWorker implements Workable { // only what it needs!
public void work() { System.out.println("Assembling parts..."); }
}
Now RobotWorker only implements Workable. No empty methods. No confusion. If we later add a ChargingRobot, it implements Workable and maybe a new Chargeable interface. Everything stays clean.
Real-World Example: Printer Interface
from abc import ABC, abstractmethod
# BAD -- fat interface
class Machine(ABC):
@abstractmethod
def print_doc(self):
pass
@abstractmethod
def scan(self):
pass
@abstractmethod
def fax(self):
pass
# A simple printer is forced to implement scan() and fax()
# Even if it can't do those things!
# GOOD -- segregated
class Printer(ABC):
@abstractmethod
def print_doc(self):
pass
class Scanner(ABC):
@abstractmethod
def scan(self):
pass
class Faxer(ABC):
@abstractmethod
def fax(self):
pass
class SimplePrinter(Printer):
def print_doc(self):
print("Printing...")
class AllInOnePrinter(Printer, Scanner, Faxer):
def print_doc(self):
print("Printing...")
def scan(self):
print("Scanning...")
def fax(self):
print("Faxing...")
// GOOD -- each capability is its own thing
class SimplePrinter {
printDoc() {
console.log("Printing...");
}
}
class AllInOnePrinter {
printDoc() {
console.log("Printing...");
}
scan() {
console.log("Scanning...");
}
fax() {
console.log("Faxing...");
}
}
// SimplePrinter doesn't pretend it can scan or fax
interface Printer { void printDoc(); }
interface Scanner { void scan(); }
interface Faxer { void fax(); }
class SimplePrinter implements Printer {
public void printDoc() { System.out.println("Printing..."); }
}
class AllInOnePrinter implements Printer, Scanner, Faxer {
public void printDoc() { System.out.println("Printing..."); }
public void scan() { System.out.println("Scanning..."); }
public void fax() { System.out.println("Faxing..."); }
}
How to Spot ISP Violations
- Empty method implementations — a class implements a method but the body is empty or throws “not supported”
- “I only use 2 out of 10 methods” — if a class implementing an interface only needs a few methods, the interface is too fat
- Changes to one method affect unrelated classes — adding a new method to the interface forces ALL implementers to change
The Simple Rule
Keep interfaces small and cohesive. If an interface has methods that don’t all make sense together, split it up. It’s better to have 5 small interfaces than 1 giant one.
In LLD interviews, this shows up when we design things like vehicles (not every vehicle can fly), employees (not every employee gets a parking spot), or payment methods (not every method supports refunds). Segregated interfaces keep our design honest.