Module 02 · SOLID and friends · Lesson 4 of 7
Interface segregation
No client should be forced to depend on methods it does not use. Why fat interfaces couple unrelated clients and force fake implementations, how to split them into role interfaces shaped by their callers, and where to stop.
Why it matters
A single large interface that every part of the system uses looks tidy and behaves badly. A reporting module that only reads orders still depends on methods for cancelling and refunding them; when those methods change, the reporting module recompiles and its tests need fakes with twenty stubbed methods. Implementations that cannot support part of the interface start throwing, which is the Liskov violation from the previous lesson. The interface segregation principle keeps dependencies as narrow as the clients' actual needs.
The idea
No client should be forced to depend on methods it does not use. Instead of one interface shaped by the implementation (everything the order store can do), offer several role interfaces shaped by the clients (what the reporting module reads, what the checkout writes, what the admin tool manages). One class can implement all of them; each client depends only on its own.
interface OrderReader { Optional<Order> find(OrderId id); List<Order> search(OrderQuery q); }
interface OrderWriter { void save(Order order); void cancel(OrderId id, Reason reason); }
interface OrderAdmin { void refund(OrderId id, Money amount); void purge(Instant before); }
final class SqlOrderStore implements OrderReader, OrderWriter, OrderAdmin { ... }
final class RevenueReport {
private final OrderReader orders; // cannot cancel or purge, even by accident
RevenueReport(OrderReader orders) { this.orders = orders; }
}
What it buys
| Benefit | Why |
|---|---|
| Smaller blast radius | Changing the admin methods does not touch reporting code or its tests |
| Simpler test doubles | A fake OrderReader needs two methods, not twenty |
| Honest capabilities | A read-only replica implements OrderReader only; nobody calls save on it and gets an exception |
| Clearer intent and least privilege | A constructor taking OrderReader documents and enforces that the class only reads |
Shape interfaces by their callers
The most reliable way to get segregation right is to let the consumer define the interface it needs. Go makes this idiomatic: interfaces are declared next to the code that uses them and satisfied implicitly. In Java or C#, the same effect comes from asking, for each new client, "which methods does this actually call?" and declaring an interface of exactly those, rather than reusing the biggest one available.
Where to stop
Splitting every method into its own interface creates a fog of tiny types and constructors with ten parameters. Group methods that are used together by the same kind of client. Two to five methods per role interface is common; one-method interfaces are fine when they represent a genuine role (a Clock, a Notifier, a strategy).
Trade-offs
Role interfaces cost more type declarations and a moment's thought about which one each client needs. They pay back with narrower coupling, simpler tests and capabilities that cannot be misused. A single fat interface is acceptable inside a small module with one client; the principle matters most at module boundaries and for interfaces with many implementers or clients.
In an interview
Apply it where a component plainly only reads: "The fare display only needs to read prices, so it depends on a PriceReader, not the full PricingService with its admin and override methods. That also means the display can be backed by a read replica." It shows you size dependencies to the client.
Check yourself
A Machine interface declares print, scan, fax and staple. A BasicPrinter implementation throws for scan, fax and staple, and the print-queue tests need fakes for all four methods. Which change best follows the interface segregation principle?