Interface Segregation Principle
Don't Force What You Don't Need
LinkedIn Hook
Here is a question interviewers ask that separates developers who know SOLID from developers who understand it:
"Your interface has 10 methods. One class uses 3 of them. The other 9 classes use only 1 or 2 each. What's wrong here?"
Most candidates say "it violates ISP." Few can explain what the real damage is.
The damage is this: every class that implements a fat interface must provide code for methods it does not use. When those unused methods change, every implementing class recompiles, retests, and reships — even if the change had nothing to do with them.
You didn't break their logic. You broke their build schedule.
ISP is not about being tidy. It's about reducing the blast radius of change. A small, focused interface means changes to one behavior affect only the clients of that behavior. Nothing else moves.
In this lesson you'll see the fat interface problem with a concrete TypeScript before/after, learn why role interfaces are the solution, and walk away with an answer that holds up under follow-up questions.
Read the full lesson → [link]
#OOP #TypeScript #SOLID #SoftwareEngineering #InterviewPrep
What You'll Learn
- What the Interface Segregation Principle is and why it exists
- What a "fat interface" is and why it creates hidden coupling
- How to split a fat interface into focused role interfaces in TypeScript
- How ISP connects to the Single Responsibility Principle and to Liskov Substitution
- How to spot ISP violations during code review and in interview questions
[INTERNAL-LINK: SOLID overview → Chapter 7 introduction, content.md]
The Analogy — A Swiss Army Knife vs. Dedicated Tools
Imagine handing a surgeon a Swiss Army knife and saying "use the scalpel." The knife has a scalpel blade, yes. It also has a corkscrew, a toothpick, and a tiny pair of scissors the surgeon will never touch. The surgeon must hold, sterilize, and account for the entire tool, even though 90% of it is irrelevant.
Now give the surgeon a dedicated scalpel. Clean. Focused. Nothing extraneous attached to it.
That's the difference between a fat interface and a role interface.
A fat interface forces every implementing class to carry tools it will never use. A role interface gives each class exactly what it needs and nothing it doesn't. The surgeon's job doesn't change. The tool they hold becomes appropriate for it.
What Is the Interface Segregation Principle?
ISP is the fourth of the five SOLID principles, introduced by Robert C. Martin (also the author of Clean Code) in his 1996 paper "The Interface Segregation Principle." The definition is precise: no client should be forced to depend on methods it does not use.
The word "client" here means any class or module that implements or uses the interface. If a client implements an interface, it must provide code for every method on that interface, including ones it has no use for. Those unused method implementations are dead weight. They add noise, invite confusion, and quietly create coupling between components that should be independent.
The fix is not to write a single monolithic interface that covers every behavior a domain might need. The fix is to define many small, focused interfaces, one per coherent role. Clients then implement only the interfaces that match their responsibilities.
Citation Capsule: Robert C. Martin defined the Interface Segregation Principle in 1996 as the principle that "clients should not be forced to depend upon interfaces that they do not use" (Martin, "The Interface Segregation Principle," C++ Report, 1996). His original motivation was reducing recompilation cascades in large C++ systems. The principle applies equally to TypeScript and Java interfaces today.
[INTERNAL-LINK: Single Responsibility Principle → Lesson 7.1: SRP]
What Is a Fat Interface?
A fat interface is an interface that groups too many unrelated methods into a single contract. It tries to be everything for everyone. As a result, every class that implements it must stub out, throw, or silently ignore the methods it does not need.
That stubbing is the red flag. When you see throw new Error("Not implemented") inside an interface method, a fat interface is almost certainly the cause.
Before — The Fat Interface in TypeScript
// FAT INTERFACE: one interface trying to describe every possible worker behavior
// Problem: not all workers eat, sleep, or need charging
interface IWorker {
work(): void;
eat(): void; // robots don't eat
sleep(): void; // robots don't sleep
charge(): void; // humans don't charge
repair(): void; // humans don't repair themselves
}
// HumanWorker must implement charge() and repair() even though they make no sense
class HumanWorker implements IWorker {
work(): void {
console.log("Human working...");
}
eat(): void {
console.log("Human eating lunch...");
}
sleep(): void {
console.log("Human sleeping...");
}
// Forced to implement methods that don't apply
charge(): void {
throw new Error("Not implemented: humans don't charge"); // ISP violation signal
}
repair(): void {
throw new Error("Not implemented: humans don't self-repair"); // ISP violation signal
}
}
// RobotWorker must implement eat() and sleep() even though they make no sense
class RobotWorker implements IWorker {
work(): void {
console.log("Robot working...");
}
// Forced to implement methods that don't apply
eat(): void {
throw new Error("Not implemented: robots don't eat"); // ISP violation signal
}
sleep(): void {
throw new Error("Not implemented: robots don't sleep"); // ISP violation signal
}
charge(): void {
console.log("Robot charging battery...");
}
repair(): void {
console.log("Robot running self-diagnostic repair...");
}
}
// Any caller that uses IWorker could accidentally call eat() on a RobotWorker
// and get a runtime error instead of a compile-time guarantee
function startWorkShift(worker: IWorker): void {
worker.work();
worker.eat(); // compiles fine -- but blows up at runtime if worker is a RobotWorker
}
const robot = new RobotWorker();
startWorkShift(robot); // Runtime Error: Not implemented: robots don't eat
The throw new Error("Not implemented") pattern is the loudest ISP violation signal in any codebase. It means a class was forced into a contract it cannot fulfill.
[UNIQUE INSIGHT] The real cost of a fat interface is not the stub methods themselves. It is the false compiler guarantee. TypeScript tells you worker.eat() is safe to call because IWorker declares eat(). But at runtime, calling eat() on a RobotWorker throws. The type system has been tricked into lying. ISP violations break the contract between compile-time safety and runtime behavior.
After — Segregated Role Interfaces in TypeScript
The solution is to split the fat interface into small, focused interfaces that each describe one coherent role. A class then implements only the roles that apply to it.
// SEGREGATED INTERFACES: each describes exactly one role
// A class implements only the interfaces that match its nature
interface IWorkable {
work(): void;
}
interface IEatable {
eat(): void;
sleep(): void;
}
interface IChargeable {
charge(): void;
repair(): void;
}
// HumanWorker implements IWorkable and IEatable -- nothing about charging
class HumanWorker implements IWorkable, IEatable {
work(): void {
console.log("Human working...");
}
eat(): void {
console.log("Human eating lunch...");
}
sleep(): void {
console.log("Human sleeping...");
}
// No charge(), no repair() -- and TypeScript does not ask for them
}
// RobotWorker implements IWorkable and IChargeable -- nothing about eating
class RobotWorker implements IWorkable, IChargeable {
work(): void {
console.log("Robot working...");
}
charge(): void {
console.log("Robot charging battery...");
}
repair(): void {
console.log("Robot running self-diagnostic repair...");
}
// No eat(), no sleep() -- and TypeScript does not ask for them
}
// Callers receive only the interface they actually need
// startWorkShift only needs work() -- it accepts any IWorkable
function startWorkShift(worker: IWorkable): void {
worker.work();
// worker.eat() is not available here -- TypeScript prevents the accidental call
}
// breakTime only applies to workers that eat -- TypeScript enforces this
function takeBreak(worker: IEatable): void {
worker.eat();
worker.sleep();
}
// maintenanceCrew only deals with chargeable workers
function performMaintenance(worker: IChargeable): void {
worker.repair();
worker.charge();
}
const human = new HumanWorker();
const robot = new RobotWorker();
startWorkShift(human); // Human working...
startWorkShift(robot); // Robot working...
takeBreak(human); // Human eating lunch... Human sleeping...
// takeBreak(robot); // Compile error: RobotWorker does not implement IEatable
performMaintenance(robot); // Robot running self-diagnostic repair... Robot charging battery...
// performMaintenance(human); // Compile error: HumanWorker does not implement IChargeable
The compiler now catches the mistake that previously caused a runtime crash. takeBreak(robot) fails at compile time with a clear message. No runtime surprises. No throw new Error("Not implemented"). The type system's guarantee matches reality.
A Second Example — UI Component Interfaces
The fat interface problem appears in frontend code just as often as in backend domain logic. Consider a component interface for a design system.
// FAT INTERFACE: one interface for all possible UI component behaviors
// Problem: not every component is clickable, draggable, or resizable
interface IUIComponent {
render(): string;
onClick(handler: () => void): void; // static images are not clickable
onDrag(handler: () => void): void; // labels are not draggable
resize(width: number, height: number): void; // icons have fixed sizes
focus(): void; // decorative images cannot be focused
}
// StaticImage is forced to implement interactive methods it doesn't need
class StaticImage implements IUIComponent {
render(): string {
return "<img src='photo.jpg' />";
}
onClick(): void {
throw new Error("StaticImage is not clickable"); // ISP violation
}
onDrag(): void {
throw new Error("StaticImage is not draggable"); // ISP violation
}
resize(width: number, height: number): void {
throw new Error("StaticImage has a fixed size"); // ISP violation
}
focus(): void {
throw new Error("StaticImage cannot be focused"); // ISP violation
}
}
// SEGREGATED: split by interaction role
interface IRenderable {
render(): string;
}
interface IClickable {
onClick(handler: () => void): void;
}
interface IDraggable {
onDrag(handler: () => void): void;
}
interface IResizable {
resize(width: number, height: number): void;
}
interface IFocusable {
focus(): void;
}
// StaticImage only renders -- no interaction methods forced onto it
class StaticImage implements IRenderable {
render(): string {
return "<img src='photo.jpg' />";
}
}
// A full interactive button gets all the roles it actually needs
class Button implements IRenderable, IClickable, IFocusable {
render(): string {
return "<button>Click me</button>";
}
onClick(handler: () => void): void {
handler();
}
focus(): void {
console.log("Button focused");
}
// No drag, no resize -- TypeScript does not ask for them
}
// A draggable modal panel
class DraggablePanel implements IRenderable, IDraggable, IResizable {
render(): string {
return "<div class='panel'>...</div>";
}
onDrag(handler: () => void): void {
handler();
}
resize(width: number, height: number): void {
console.log(`Panel resized to ${width}x${height}`);
}
}
// Caller only receives the interface it needs -- no accidental calls to wrong methods
function renderAll(components: IRenderable[]): void {
components.forEach(c => console.log(c.render()));
}
renderAll([new StaticImage(), new Button(), new DraggablePanel()]);
[PERSONAL EXPERIENCE] In production design systems, fat component interfaces are one of the most common sources of runtime exceptions in UI layers. A render() function receives an IUIComponent, calls onClick() to attach a handler, and throws on a decorative image that was never meant to be interactive. Segregating by interaction role eliminates that entire class of bug at the type level.
How ISP Connects to the Other SOLID Principles
ISP does not exist in isolation. It works alongside the rest of the SOLID set.
ISP and SRP. A fat interface usually signals that a class is trying to serve too many responsibilities. When you split the interface, you often find the class itself should also be split. The two principles push in the same direction: smaller, more focused units.
ISP and LSP. A class that throws "Not implemented" in required interface methods violates the Liskov Substitution Principle directly. The Liskov principle says a subtype must be substitutable for its base type without breaking behavior. A class that throws on eat() cannot substitute for IWorker anywhere eat() is expected. ISP violations frequently cause LSP violations downstream.
ISP and DIP. The Dependency Inversion Principle says high-level modules should depend on abstractions, not concretions. When you segregate interfaces into focused roles, high-level modules can depend on only the narrow abstraction they actually need. A scheduler depends on IWorkable, not on the full IWorker blob. The dependency is minimal and precise.
[INTERNAL-LINK: Liskov Substitution Principle → Lesson 7.3: LSP] [INTERNAL-LINK: Dependency Inversion Principle → Lesson 7.5: DIP]
Common Mistakes
Mistake 1: Confusing ISP With "One Method Per Interface"
ISP does not require every interface to have exactly one method. It requires that methods in an interface belong together because the same clients need all of them. IEatable has both eat() and sleep() in the example above because every client that eats also sleeps. They belong together. The principle is about cohesion among the methods, not minimizing method count.
Mistake 2: Over-Segregating Into Single-Method Interfaces Everywhere
Splitting every method into its own interface creates a different problem: callers must implement or accept dozens of tiny interfaces, and the relationship between related behaviors becomes invisible. The goal is role-level cohesion. Group methods that always travel together. Split methods that are independent.
Mistake 3: Only Applying ISP to Interfaces and Missing Abstract Classes
Abstract classes carry the same problem. An abstract base class with 8 abstract methods forces every subclass to provide implementations for all 8, even if only 2 are relevant. ISP thinking applies equally to abstract class design.
Mistake 4: Treating "Not Implemented" Errors as Acceptable
Some codebases treat throw new Error("Not implemented") as a legitimate pattern. It is not. It is the symptom of a design that forced a class into a contract that doesn't fit. The right fix is interface segregation, not runtime error handling around stub methods.
Interview Questions
Q: What is the Interface Segregation Principle? ISP states that no client should be forced to depend on methods it does not use. When an interface grows too large and contains methods that only some implementing classes need, it should be split into smaller, focused interfaces. Each client then implements only the interfaces that match its actual responsibilities, eliminating forced stub methods and false compiler guarantees.
Q: What is a fat interface and why is it a problem? A fat interface is an interface that groups too many unrelated methods into one contract. The problem is that every implementing class must provide code for all methods, including ones it does not use. This causes three harms: dead stub code that throws at runtime, a false compile-time guarantee that a method is safe to call, and tight coupling between unrelated behaviors so that changing one method forces all implementing classes to recompile and retest.
Q: How does ISP relate to the Liskov Substitution Principle?
They are closely linked. An ISP violation often produces an LSP violation. When a class is forced to implement interface methods it cannot fulfill and throws "Not implemented", it cannot substitute for the base interface in any context where those methods are called. The program compiles but crashes at runtime. Segregating the interface so the class only implements what it can actually fulfill restores both principles simultaneously.
Q: Can you have too many interfaces — is it possible to over-apply ISP? Yes. Splitting every method into its own single-method interface creates noise and hides the relationship between behaviors that naturally belong together. The target is role-level cohesion: methods that every client of that role will always need belong in the same interface. Methods that only some clients need belong in a separate interface. The test is: "Does every class that needs method A also need method B?" If yes, they belong together.
Q: How do you detect an ISP violation during code review?
Three signals: (1) a class has throw new Error("Not implemented") in one or more interface method implementations, (2) an interface has methods that only one or two of its many implementing classes actually use, (3) a change to one method on a large interface triggers recompilation or retesting of classes that have nothing to do with that method. Any of these points directly to a fat interface that should be split.
Quick Reference — Cheat Sheet
INTERFACE SEGREGATION PRINCIPLE
---------------------------------------------------------------------------
Definition No client should be forced to depend on methods
it does not use.
(Robert C. Martin, 1996)
Root Cause Fat interfaces: one interface trying to serve
all clients. Each client ends up implementing
methods that don't apply to it.
ISP Violation - throw new Error("Not implemented") in interface methods
Signals - Interface methods used by only 1-2 of many implementors
- Unrelated changes force full recompile/retest of classes
- Type system allows a call that throws at runtime
The Fix Split the fat interface into role interfaces.
Each role interface groups only the methods that
always travel together for the same client.
Role Interface A focused interface that describes one coherent
behavior role. Clients implement multiple role
interfaces as needed.
---------------------------------------------------------------------------
BEFORE vs AFTER AT A GLANCE
---------------------------------------------------------------------------
BEFORE (Fat) interface IWorker {
work(): void;
eat(): void; // robots don't eat
sleep(): void; // robots don't sleep
charge(): void; // humans don't charge
repair(): void; // humans don't repair
}
AFTER (Segregated) interface IWorkable { work(): void; }
interface IEatable { eat(): void; sleep(): void; }
interface IChargeable{ charge(): void; repair(): void; }
HumanWorker implements IWorkable, IEatable
RobotWorker implements IWorkable, IChargeable
---------------------------------------------------------------------------
ISP AND THE REST OF SOLID
---------------------------------------------------------------------------
ISP + SRP Fat interface often maps to a class doing too much.
Split the interface, consider splitting the class too.
ISP + LSP ISP violations cause LSP violations.
"Not implemented" stubs break substitutability.
ISP + DIP Segregated interfaces create precise, minimal
abstractions for high-level modules to depend on.
---------------------------------------------------------------------------
COMMON MISTAKE One method per interface is NOT the goal.
Group by role cohesion, not by method count.
Methods that always travel together belong together.
---------------------------------------------------------------------------
Previous: Lesson 7.3 → Next: Lesson 7.5 →
This is Lesson 7.4 of the OOP Interview Prep Course — 8 chapters, 41 lessons.