Skip to content

Interface Segregation

TL;DR: No class should be forced to implement methods it doesn't use. If your Worker interface has work(), eat(), and sleep(), you're forcing a Robot to fake eating and sleeping. Split the interface so each class only commits to what it can actually do.

Interface Segregation Principle

Why This Matters in Interviews

ISP violations surface constantly in LLD interviews because candidates tend to build one big interface that covers every possible behavior. When the interviewer says "now add a robot worker," the cracks show immediately: the Robot has to implement eat() and sleep() with empty bodies or throw exceptions. That's the same kind of contract violation you saw with LSP, but the root cause is different. LSP says "don't break promises." ISP says "don't make promises you can't keep in the first place."

Interviewers look for whether you can design interfaces that are specific enough that every implementor genuinely supports every method.

The Problem: One Fat Interface

// BAD: Worker forces every implementor to support all three methods
interface Worker {
    void work();
    void eat();
    void sleep();
}

class HumanWorker implements Worker {
    @Override
    public void work() {
        System.out.println("Writing code");
    }

    @Override
    public void eat() {
        System.out.println("Eating lunch");
    }

    @Override
    public void sleep() {
        System.out.println("Sleeping 8 hours");
    }
}

class Robot implements Worker {
    @Override
    public void work() {
        System.out.println("Assembling parts");
    }

    @Override
    public void eat() {
        // Robots don't eat... but we're forced to implement this
    }

    @Override
    public void sleep() {
        // Robots don't sleep... but we're forced to implement this
    }
}
// BAD: Same problem in C++
class Worker {
public:
    virtual ~Worker() = default;
    virtual void work() = 0;
    virtual void eat() = 0;
    virtual void sleep() = 0;
};

class HumanWorker : public Worker {
public:
    void work() override { std::cout << "Writing code" << std::endl; }
    void eat() override { std::cout << "Eating lunch" << std::endl; }
    void sleep() override { std::cout << "Sleeping 8 hours" << std::endl; }
};

class Robot : public Worker {
public:
    void work() override { std::cout << "Assembling parts" << std::endl; }
    void eat() override { /* Robots don't eat */ }
    void sleep() override { /* Robots don't sleep */ }
};
# BAD: Same problem in Python
from abc import ABC, abstractmethod

class Worker(ABC):
    @abstractmethod
    def work(self) -> None:
        pass

    @abstractmethod
    def eat(self) -> None:
        pass

    @abstractmethod
    def sleep(self) -> None:
        pass


class HumanWorker(Worker):
    def work(self) -> None:
        print("Writing code")

    def eat(self) -> None:
        print("Eating lunch")

    def sleep(self) -> None:
        print("Sleeping 8 hours")


class Robot(Worker):
    def work(self) -> None:
        print("Assembling parts")

    def eat(self) -> None:
        pass  # Robots don't eat

    def sleep(self) -> None:
        pass  # Robots don't sleep

What's Wrong Here?

The Robot class is lying. It claims to support eat() and sleep() because it implements the Worker interface, but those methods do nothing. Any code that calls eat() on a Worker is silently broken when the worker is a Robot. This leads to two problems:

  1. Silent failures — calling robot.eat() doesn't crash, but it also doesn't do what the caller expects. This is worse than crashing because it hides bugs.
  2. Coupling to irrelevant behavior — if you add a takeVitamins() method to Worker, you have to update Robot too, even though vitamins have nothing to do with robots.

Interview tip: Empty method implementations are a design smell. When you see a class implementing a method with an empty body or a comment like "not applicable," the interface is too broad. That's your cue to split it.

The Fix: Split Into Focused Interfaces

// GOOD: Each interface represents one capability

interface Workable {
    void work();
}

interface Feedable {
    void eat();
}

interface Restable {
    void sleep();
}

class HumanWorker implements Workable, Feedable, Restable {
    @Override
    public void work() {
        System.out.println("Writing code");
    }

    @Override
    public void eat() {
        System.out.println("Eating lunch");
    }

    @Override
    public void sleep() {
        System.out.println("Sleeping 8 hours");
    }
}

class Robot implements Workable {
    @Override
    public void work() {
        System.out.println("Assembling parts");
    }
    // No eat(). No sleep(). No empty stubs. No lies.
}
// GOOD: Same fix in C++

class Workable {
public:
    virtual ~Workable() = default;
    virtual void work() = 0;
};

class Feedable {
public:
    virtual ~Feedable() = default;
    virtual void eat() = 0;
};

class Restable {
public:
    virtual ~Restable() = default;
    virtual void sleep() = 0;
};

class HumanWorker : public Workable, public Feedable, public Restable {
public:
    void work() override { std::cout << "Writing code" << std::endl; }
    void eat() override { std::cout << "Eating lunch" << std::endl; }
    void sleep() override { std::cout << "Sleeping 8 hours" << std::endl; }
};

class Robot : public Workable {
public:
    void work() override { std::cout << "Assembling parts" << std::endl; }
};
# GOOD: Same fix in Python

from abc import ABC, abstractmethod

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


class Feedable(ABC):
    @abstractmethod
    def eat(self) -> None:
        pass


class Restable(ABC):
    @abstractmethod
    def sleep(self) -> None:
        pass


class HumanWorker(Workable, Feedable, Restable):
    def work(self) -> None:
        print("Writing code")

    def eat(self) -> None:
        print("Eating lunch")

    def sleep(self) -> None:
        print("Sleeping 8 hours")


class Robot(Workable):
    def work(self) -> None:
        print("Assembling parts")

Now Robot only implements Workable. It makes no promises about eating or sleeping. Code that needs a Feedable will never accidentally receive a Robot — the type system prevents it.

// Type safety: this method can only receive things that actually eat
void scheduleLunch(List<Feedable> workers) {
    for (Feedable worker : workers) {
        worker.eat();  // Guaranteed to work — Robot can't appear here
    }
}

How ISP and LSP Work Together

ISP and LSP attack the same family of problems from different angles:

Principle What it prevents How
LSP Subclass breaks the base class contract Fix the hierarchy after the violation
ISP Class is forced to implement irrelevant methods Prevent the violation by splitting the interface upfront

If you apply ISP correctly, you avoid most LSP violations before they happen. The Robot never gets a fly() or eat() method it can't support because it never implements an interface that promises those.

Interview tip: When designing interfaces, ask: "Would every implementor of this interface genuinely support every method?" If the answer is no, the interface needs splitting. It's much cheaper to split an interface during design than to fix LSP violations after you've built a class hierarchy on top of it.

Quick Recap

Concept What it means Why it matters
No forced implementations Classes should only implement methods they actually use No empty stubs, no silent no-ops
Focused interfaces Each interface represents one cohesive capability Implementors commit only to what they can deliver
Prevents LSP violations If a class never promises behavior it can't fulfill, substitutability is preserved Catches design mistakes at the interface level, not the class level
Composition of interfaces Classes implement multiple small interfaces as needed HumanWorker is Workable + Feedable + Restable; Robot is just Workable