So far, we've explored three SOLID principles:
- Single Responsibility Principle (SRP) — keep responsibilities together.
- Open/Closed Principle (OCP) — make software easier to extend.
- Liskov Substitution Principle (LSP) — objects of a superclass should be replaceable with objects of its subclasses
Now let's look at the fourth principle.
Interface Segregation Principle (ISP).
The formal definition says:
Clients should not be forced to depend on interfaces they do not use.
Sounds reasonable.
But what does that actually mean?
Let's start with a problem.
The Problem
Imagine we're building an application for managing employees.
Different employees can perform different actions.
Some employees can:
- Work
- Attend meetings
- Take breaks
Some can also:
- Approve expenses
- Manage teams
- Conduct interviews
So we create an interface:
interface Employee {
work(): void;
attendMeeting(): void;
takeBreak(): void;
approveExpenses(): void;
conductInterview(): void;
}
Looks convenient.
Everything related to an employee is in one place.
Now let's create a regular developer.
class Developer implements Employee {
work() {
console.log("Writing code...");
}
attendMeeting() {
console.log("Attending meeting...");
}
takeBreak() {
console.log("Taking a break...");
}
approveExpenses() {
// Not applicable
}
conductInterview() {
// Not applicable
}
}
We have a problem.
The developer is being forced to implement methods that don't make sense for them.
The "Not Applicable" Problem
We might try to solve this by throwing an error.
approveExpenses() {
throw new Error("Developers cannot approve expenses.");
}
Or perhaps we leave the method empty.
approveExpenses() {}
Neither solution feels right.
Why should a Developer be required to implement something that isn't part of its responsibility?
The problem isn't with the developer.
The problem is with our interface.
We've created an interface that is trying to represent too many things at once.
The Idea Behind ISP
Instead of one large interface, we can split it into smaller, focused interfaces.
interface Worker {
work(): void;
}
interface MeetingParticipant {
attendMeeting(): void;
}
interface BreakTaker {
takeBreak(): void;
}
interface ExpenseApprover {
approveExpenses(): void;
}
interface Interviewer {
conductInterview(): void;
}
Now a developer can implement only what they actually need.
class Developer implements Worker, MeetingParticipant, BreakTaker {
work() {
console.log("Writing code...");
}
attendMeeting() {
console.log("Attending meeting...");
}
takeBreak() {
console.log("Taking a break...");
}
}
A manager might implement more capabilities.
class Manager
implements Worker, MeetingParticipant, BreakTaker, ExpenseApprover, Interviewer {
work() {}
attendMeeting() {}
takeBreak() {}
approveExpenses() {}
conductInterview() {}
}
Now our design represents reality much better.
Why Does This Matter?
At first, this might seem like a small improvement.
But imagine the application becomes much larger.
Suppose dozens of classes implement our original Employee interface.
Now we decide to add a new method:
interface Employee {
...
generatePayrollReport(): void;
}
Suddenly every class implementing Employee has to deal with this new method.
Even classes that have absolutely nothing to do with payroll.
This is one of the hidden costs of large interfaces.
A change in one part of an interface can force unrelated classes to change.
That's exactly the kind of coupling ISP tries to reduce.
ISP Isn't About Creating Tiny Interfaces Everywhere
This is important.
After learning ISP, it's easy to go too far.
You might end up creating interfaces like:
interface CanCreate {}
interface CanUpdate {}
interface CanDelete {}
interface CanRead {}
And suddenly the codebase has hundreds of interfaces.
That's not the goal.
The goal isn't:
Make every interface as small as possible.
The goal is:
Don't force a client to depend on behaviour it doesn't need.
Sometimes a larger interface genuinely represents one cohesive concept.
There's nothing wrong with that.
The problem begins when unrelated responsibilities are bundled together.
A Simple Way to Think About ISP
Whenever you're designing an interface, ask:
Does every implementation actually need all of these methods?
If the answer is repeatedly "no", that's a signal that the interface may be doing too much.
Another useful question is:
If I add a new method to this interface, how many unrelated classes will I have to change?
If the answer is "a lot", you may have a fat interface.
ISP and Real-World APIs
This principle isn't limited to classes.
The same thinking applies to APIs and modules.
Imagine a service exposes:
userService.createUser()
userService.deleteUser()
userService.generateReport()
userService.sendMarketingEmail()
userService.exportFinancialData()
A component that only needs to create users shouldn't necessarily depend on everything the service can do.
Smaller, focused contracts make dependencies clearer.
This becomes especially important when we start discussing Dependency Inversion and Dependency Injection.
The Key Takeaway
The Interface Segregation Principle is essentially about avoiding unnecessary dependencies.
Instead of creating one large interface that tries to describe everything, create focused contracts around meaningful capabilities.
That way:
- Classes implement only what they need.
- Changes affect fewer parts of the system.
- Dependencies become clearer.
- Interfaces become easier to understand and maintain.
The important thing isn't the number of methods in an interface.
It's whether those methods belong together from the perspective of the clients using them.
What's Next?
We've now covered four of the five SOLID principles:
- SRP — Keep responsibilities focused.
- OCP — Extend behaviour without constantly modifying existing code.
- LSP — Subtypes should genuinely be replaceable for their parent types.
- ISP — Don't force clients to depend on things they don't need.
One principle remains.
And arguably, it's the one that connects many of the ideas we've discussed so far.
Dependency Inversion Principle (DIP).
We'll explore why high-level business logic shouldn't be tightly coupled to low-level implementation details—and how this idea eventually leads us to Dependency Injection.

Top comments (0)