Interface Segregation Principle: Stop Forcing Clients to Care

The Interface Segregation Principle (ISP) is the “I” in SOLID, and it boils down to a simple idea:

A client shouldn’t be forced to depend on methods it doesn’t use.

In other words, your interfaces shouldn’t make callers—and implementers—care about behaviors that are irrelevant to them. When interfaces stay focused, implementations get simpler, and the rest of the system stops paying for features it never asked for.

Why ISP Matters

Big, generic interfaces feel powerful at first. Then they quietly start to hurt.

When an interface tries to do too much, you get:

Unused methods everywhere. Implementations end up with empty method bodies or UnsupportedOperationException just to satisfy the type system.

Unnecessary coupling. Clients depend on methods they don’t actually need, making their code harder to change, test, and reason about.

Change ripples. A small tweak to a “fat interface” forces edits across every implementation—even those that should be unaffected.

ISP pushes you toward smaller, client‑shaped interfaces, so each piece of code only knows about what it truly cares about.

The Core Ideas Behind ISP

You can think of ISP as SRP for interfaces:

Small, focused interfaces. Each interface should reflect a clear responsibility or tight cluster of behavior, not a grab bag of everything a type might be able to do.

Client‑specific contracts. Design interfaces based on how they’re actually used, not how you imagine they might be used one day.

Avoid “fat interfaces.” If an interface tends to force a lot of “not implemented” behavior, it’s trying to cover too many roles and should be split.

ISP in Code: Printers That Do Too Much

Let’s start with a classic example.

The Problem: One Interface to Rule Them All

interface Printer {
    void print(Document doc);
    void scan(Document doc);
    void fax(Document doc);
}

class BasicPrinter implements Printer {
    public void print(Document doc) {
        System.out.println("Printing document...");
    }

    public void scan(Document doc) {
        throw new UnsupportedOperationException("Scan not supported");
    }

    public void fax(Document doc) {
        throw new UnsupportedOperationException("Fax not supported");
    }
}

BasicPrinter only knows how to print, but the interface forces it to pretend it can scan and fax too. The type system is satisfied; your users are not.

Every new method added to Printer becomes another obligation for all implementers, whether it makes sense or not.

The Fix: Split by Capability

Instead, we express capabilities as separate, focused interfaces.

interface Printable {
    void print(Document doc);
}

interface Scannable {
    void scan(Document doc);
}

interface Faxable {
    void fax(Document doc);
}

class BasicPrinter implements Printable {
    public void print(Document doc) {
        System.out.println("Printing document...");
    }
}

class MultiFunctionPrinter implements Printable, Scannable, Faxable {
    public void print(Document doc) {
        System.out.println("Printing document...");
    }

    public void scan(Document doc) {
        System.out.println("Scanning document...");
    }

    public void fax(Document doc) {
        System.out.println("Faxing document...");
    }
}

Now each device implements only what it truly supports. Clients that just need printing depend on Printable, not on some monolithic Printer that implies features they never use.

When You Can’t Change the Big Interface: Adapters

Sometimes you’re stuck with a large, legacy API you don’t control. ISP still helps—you just enforce it at your own boundaries.

class AdvancedPrinter {
    public void print(Document doc) { /* ... */ }
    public void scan(Document doc) { /* ... */ }
    public void fax(Document doc) { /* ... */ }
    public void email(Document doc) { /* ... */ }
}

interface Printable {
    void print(Document doc);
}

class PrintAdapter implements Printable {
    private final AdvancedPrinter advancedPrinter;

    public PrintAdapter(AdvancedPrinter advancedPrinter) {
        this.advancedPrinter = advancedPrinter;
    }

    public void print(Document doc) {
        advancedPrinter.print(doc);
    }
}

Your code now talks to a slim Printable interface, even though the underlying dependency is a sprawling AdvancedPrinter. That keeps your own design clean and insulated from future changes in the external API.

Benefits and Trade‑Offs

What You Gain

Modularity. Responsibilities are naturally separated. Reading and changing code gets easier when each interface has a clear purpose.

Flexibility. Clients depend only on what they use, so swapping implementations or wiring different combinations together is straightforward.

Testability. Smaller interfaces are easier to fake or mock. Tests can focus on one behavior at a time.

What It Costs

More interfaces. ISP tends to increase the number of interface types in your system. Without good naming and organization, that can feel noisy.

Design overhead. Over‑segmentation in tiny projects can backfire, making the code look more complex than the problem it solves.

Consistency discipline. You need to be intentional about where you draw boundaries so the interface landscape stays coherent.

Applying ISP in Practice

You don’t need to perfectly foresee every client, but a few habits help:

Start from the caller’s perspective. Ask: “What does this client actually need from this dependency?” Model your interfaces around that.

Prefer composition over inheritance. Composing behaviors usually leads to smaller, sharper interfaces than inheriting from a bloated base type.

Refactor fat interfaces over time. When you see empty implementations or frequent UnsupportedOperationException, treat that as a signal to split things up.

The Bottom Line

The Interface Segregation Principle is about respect—for your callers, and for your own future sanity.

When each client depends only on the methods it truly uses, your codebase becomes easier to change, easier to test, and less fragile. Interfaces turn from vague promises into precise contracts.

You may end up with more interfaces, but if they’re well‑named and focused, the trade‑off usually pays for itself in clarity and confidence.

0%