- Low Level Design
- /
- Interface Segregation Principle
Course content
The Interface Segregation Principle says that no client should be forced to depend on methods it does not use. If a class implements an interface with ten methods but only really supports four of them, the other six are dead weight. Either the implementer has to provide stub bodies that throw or do nothing, or callers have to know which methods are real for which subclass.
The fix is to split the fat interface into smaller, cohesive interfaces. Each interface declares one focused capability. Each implementer claims only the capabilities it actually has. Callers depend on the small interface that matches the operation they need, not on a wide one that bundles unrelated operations.
In this module's running example, the checkout backend of an online store (the full tour), the slice for this principle is the product catalogue. The store sells physical goods that ship from a warehouse and digital goods that are delivered as download links, and one interface is trying to describe both. The page stands on its own if you have not read the tour.
The store's catalogue grew up around physical products, so its central interface describes what physical fulfilment needs: ship the item to an address, restock the warehouse when inventory runs low. When the store added digital gift cards, delivery by download link joined the same interface. Now every product implements OrderItem, and no product supports all of it. A gift card cannot ship or restock. A t-shirt has no download link.
Forcing both to implement everything means writing methods that throw, which is a way of admitting in code that the type is lying. The symptom on the caller side is just as bad: code holding an OrderItem cannot tell from the type whether shipTo will work or throw, so it either guards every call with a try/catch or checks the concrete class and gives up on polymorphism. Both are workarounds for an interface that promised more than its implementers could deliver.
You may have met this principle as the office-printer example: a fat OfficeDevice interface declares print, scan, and fax, and a basic printer is forced to stub two of them. That is the textbook version, the one interviewers usually reach for, and it is the same disease. The exercise at the end of this page uses it.
| Smell | What it looks like | Why it's a problem |
|---|---|---|
| Methods that throw on some implementations | String downloadLink() { throw new UnsupportedOperationException(...); } | The type promises a capability the class does not deliver |
| Methods with no-op bodies | void restock(int qty) { /* not supported */ } | Same lie, just silent. Callers cannot tell the request was discarded |
| An interface named after a category, not a capability | OrderItem, OfficeDevice, Vehicle, MediaItem | Likely bundles unrelated operations that not every member supports |
| Tests that include "if (x instanceof Y) skip()" | if (item instanceof DigitalProduct) { skip the shipping test } | The test code is admitting that the type lied |
| A method most implementers stub | Six product types implement OrderItem, but only one supports downloadLink | The other five are paying for a method none of their callers will use |
Two thirds of DigitalProduct's public API consists of methods that exist only because the interface required them. They throw when called, which puts the burden of correct usage on the caller. A developer who sees that DigitalProduct implements OrderItem reasonably assumes all three operations work. The throw is how they find out otherwise, and only at runtime.
The fat interface has also made the system harder to extend. Every new kind of product starts life owing stubs for whatever it cannot do: a made-to-order item that ships but keeps no stock, a service appointment that neither ships nor downloads. Each one adds more methods that lie, and every consumer of OrderItem has to learn which products throw on which calls.
What this costs in production
During a flash sale, the warehouse team's overnight job walked the full catalogue calling restock() to top up anything running low. The first digital gift card in the list threw the UnsupportedOperationException from its stub, the job aborted halfway, and physical stock counts stayed stale through the store's biggest sale of the year. The stub existed only to satisfy the fat interface, and the job had trusted the type.
Each interface now declares one capability, and each product claims only the capabilities it has. The throw stubs are gone because the methods that used to throw are no longer on the classes at all.
Notice who the interfaces serve. The courier integration needs Shippable and nothing else. The order-confirmation email needs Downloadable. The warehouse job that caused the outage needs Restockable, and because it now iterates Restockable items, a gift card can never reach it; the compiler filters the catalogue before the job runs. The runtime exception that used to surface a missing capability has been replaced by a compile error, which is exactly where you want it to surface.
Adding a new product type becomes a question of which capabilities it has. There is no template of throwing stubs to copy, and nothing about the new class has to be reviewed against a fat parent contract.
The catalogue split also shows the deeper reading of the principle: interfaces belong to the code that calls them. Each role interface exists because a specific client needs exactly that slice. The courier integration needs Shippable, the confirmation email needs Downloadable, the warehouse job needs Restockable. When you cannot decide how to split a fat interface, list its callers and group the methods by who calls them; the seams usually fall out of that list. The name for the result is role interfaces: one interface per role a client needs an object to play, rather than one per class.
The principle also applies to interfaces you consume rather than design. SDK listener interfaces are the classic offender: to observe one event you implement eight callbacks, seven of them empty. Java's ecosystem grew adapter base classes (extend, override the one method you care about) and later default methods to blunt the pain, but both are mitigations. When you own the API, the fix is the same as in the catalogue: one small interface per concern, so no consumer pays for events it does not care about.
Six months later the store adds print-on-demand posters. They ship to the customer, but each one is printed per order, so there is no warehouse stock to top up, and there is nothing to download. Under the fat interface the new product would owe two throw stubs on day one, and every consumer of OrderItem would need to learn its quirks. With role interfaces, the release is one honest file.
What this saves in production
The overnight restock job is structurally protected: it iterates Restockable items, posters are not Restockable, so the flash-sale abort from the war story cannot recur with any future product type. New products declare exactly what they can do and inherit no lies, and no existing consumer of the catalogue is reviewed or retested for the release.
ISP is not a rule that every interface must declare exactly one method. If a class genuinely groups several methods that always travel together (an Iterator with hasNext and next, a Connection with commit and rollback), keeping them on one interface is fine. The principle targets interfaces where the methods do not all describe the same thing, not interfaces with more than one method.
A related trap is splitting interfaces ahead of demand. If today every implementer of Foo uses every method on Foo, the interface is the right size. Splitting it into ten one-method interfaces just in case some future implementer might want a subset adds complexity without any payoff. Wait until you have an implementer that genuinely needs a subset before you split.
The companion exercise applies the same refactor to the textbook version of this problem, the one interviewers usually reach for: a fat OfficeDevice interface and three devices, two of which are forced to throw on capabilities they do not have. Split it into Printer, Scanner, and Faxer role interfaces so that each device implements only what it actually supports. The validator's reflection check confirms that no device exposes a method it does not implement, including through inheritance.
Open the exercise: Interface Segregation: Split the OfficeDevice Interface.