Interface Segregation Principle
Intermediate · 10 min read · ▶ live playground · ✦ checkpoint
A shipping desk replaces its all-in-one machine with a small label printer. The new class prints perfectly, yet the shared OfficeMachine interface also demands scanning and faxing, so two methods exist only to throw errors.
The Interface Segregation Principle (ISP) says many small, client-specific interfaces are better than one fat interface. A client is code that calls an interface, and a fat interface is a contract that makes clients or implementers depend on operations they do not need.
The principle is not “every interface gets one method.” It is “no client should be forced to depend on an irrelevant promise.”
Before: one contract pretends every machine does everything
OfficeMachine was designed around the largest device in the office. That makes the all-in-one implementation comfortable, but a print-only device must lie about capabilities it does not have.
class diagram
Before: one fat machine interface
- 1OfficeMachine bundles printing, scanning, and faxing into one promise.
- 2BasicPrinter must claim scan() and fax() even though it supports neither operation.
- 3AllInOnePrinter fits the contract, which hides how poorly the contract fits smaller devices.
- 4PrintDesk needs only print(), yet its dependency exposes three unrelated capabilities.
- 5The forced methods compile, then turn a promised operation into a runtime failure.
The class satisfies TypeScript's method-shape check. It does not satisfy the meaning a caller reads from the interface.
interface OfficeMachine {
print(document: string): string;
scan(document: string): string;
fax(document: string): string;
}
class BasicPrinter implements OfficeMachine {
public print(document: string): string {
return `Printed: ${document}`;
}
// smell: the fat interface forces unsupported operations into this class.
public scan(document: string): string {
throw new Error("not supported");
}
public fax(document: string): string {
throw new Error("not supported");
}
}
class AllInOnePrinter implements OfficeMachine {
public print(document: string): string {
return `Printed: ${document}`;
}
public scan(document: string): string {
return `Scanned: ${document}`;
}
public fax(document: string): string {
return `Faxed: ${document}`;
}
}
class PrintDesk {
private machine: OfficeMachine;
public constructor(machine: OfficeMachine) {
this.machine = machine;
}
public printLabel(text: string): string {
return this.machine.print(text);
}
}
const desk = new PrintDesk(new BasicPrinter());
console.log(desk.printLabel("BOX-42"));Printed: BOX-42
The happy path works, which makes this smell easy to miss. Any code holding the same OfficeMachine reference is allowed by the type to call scan(). With BasicPrinter, that legal call becomes Error: not supported.
This is also an LSP warning: an implementation cannot stand in for the contract wherever scanning is expected. ISP fixes the contract before more classes accumulate stub methods, empty bodies, or feature flags.
After: split the contract by client role
A role interface is a contract shaped around one capability a client uses. Printing, scanning, and faxing can vary independently, so they become separate roles. A device implements the combination it truly supports.
class diagram
After: machines implement only supported roles
- 1Three role interfaces separate capabilities that clients can request independently.
- 2BasicPrinter implements Printable and makes no scanning or faxing promise.
- 3AllInOnePrinter still promises printing through the same focused contract.
- 4It opts into Scannable because this concrete device can honor scan().
- 5It independently opts into Faxable instead of forcing that role on every printer.
- 6PrintDesk now depends on exactly the capability it uses: Printable.
Every dashed hollow-triangle arrow is an honest implements promise. The absence of arrows is meaningful too: no caller can mistake BasicPrinter for something scannable or faxable.
interface Printable {
print(document: string): string;
}
interface Scannable {
scan(document: string): string;
}
interface Faxable {
fax(document: string): string;
}
class BasicPrinter implements Printable {
public print(document: string): string {
return `Printed: ${document}`;
}
}
class AllInOnePrinter implements Printable, Scannable, Faxable {
public print(document: string): string {
return `Printed: ${document}`;
}
public scan(document: string): string {
return `Scanned: ${document}`;
}
public fax(document: string): string {
return `Faxed: ${document}`;
}
}
class PrintDesk {
private printer: Printable;
public constructor(printer: Printable) {
this.printer = printer;
}
public printLabel(text: string): string {
return this.printer.print(text);
}
}
const labelDesk = new PrintDesk(new BasicPrinter());
const frontOffice = new PrintDesk(new AllInOnePrinter());
console.log(labelDesk.printLabel("BOX-42"));
console.log(frontOffice.printLabel("VISITOR-7"));Printed: BOX-42 Printed: VISITOR-7
PrintDesk can accept either device because both keep the small promise it needs. Code that scans can ask for Scannable; code that faxes can ask for Faxable. Changing the fax contract no longer forces a print-only class or client to change because of an irrelevant method.
When not to over-apply ISP
Small means focused, not automatically single-method. If a document reader must open(), read(), and close() as one lifecycle, and the same clients need all three, keeping those operations together can be cohesive. Splitting them into Openable, Readable, and Closable may let callers assemble combinations that make no sense.
Over-fragmentation creates more names, imports, and combinations to understand. It can also hide the main concept: a client that genuinely needs the complete document lifecycle should be able to name that contract once.
Split an interface when implementations are forced to stub methods, clients use disjoint subsets, or groups of methods change independently. Keep methods together when they form one promise and clients use them as a unit. ISP optimizes dependencies around real roles, not the number of lines in an interface.
Checkpoint
Answer all three to mark this lesson complete
You can now shape contracts around the clients that use them and the implementers that can keep them. Next, Dependency Inversion changes which side of a collaboration gets to define that contract.