Interface Segregation Principle
Core. Expect to meet this one, and expect to be asked for it by name.
Start with the problem
A logger needs a destination to send finished lines to: a console, a file, a list a test can inspect. The first interface anyone reaches for tries to cover the destination's whole lifecycle at once.
public interface Destination {
void write(String formattedLine);
void flush();
void close();
List<String> registeredNames();
}
For a FileDestination backed by a real file, all four methods have an obvious body. There is nothing wrong with writing the interface this way for a design that only ever plugs in files.
Watch where it goes
Then a test wants a destination that is nothing more than a captured list: whatever gets written, check it afterwards. With four methods on the interface, that destination cannot be a lambda, so it has to be a full class.
final class CapturingDestination implements Destination {
private final List<String> lines = new ArrayList<>();
@Override public void write(String line) { lines.add(line); }
@Override public void flush() { }
@Override public void close() { }
@Override public List<String> registeredNames() { return List.of(); }
List<String> captured() { return lines; }
}
Every test that wants to see what was logged now writes three empty methods it will never call. Every new kind of destination, a network socket or a rolling file, has to answer what close() should do for it, whether or not the question makes sense there. A console destination that never closes still has to pick some behaviour for close(), because the interface demands one.
That is the cost: a caller of Destination that only ever calls write cannot tell, from the type alone, that the other three methods are safe to ignore. Implementing an interface commits you to methods you may not use and cannot promise callers you have correctly implemented.
The move
Split by who actually calls what. A logger calling write needs nothing about flushing, closing, or listing anything.
public interface Appender {
void write(String formattedLine);
}
Now CapturingDestination is not a class, it is an expression.
List<String> lines = new ArrayList<>();
Appender captured = lines::add;
Whatever else a real file destination needs, closing or flushing, lives in whatever concrete type wraps the file. That type is reachable by whoever manages the file's lifecycle, and invisible to the logger, which only ever calls write. That is Interface Segregation: no caller should be forced to depend on, or provide, methods it does not use.
What modern Java changes here
A single-method interface is, by construction, a functional interface, so Java accepts a lambda or a method reference anywhere one is expected. That is not a coincidence. Once an interface is narrowed to the one thing a caller actually calls, there is usually only one method left. corpus/logger/contract/Appender.java says as much in its own javadoc: a single-method interface is whatever lambda you like. Segregating an interface and making it functional turn out to be the same piece of work, seen from two different angles.
The version of this that is wrong
Taken further than the caller needs, segregation splits a class's methods into one interface each, even when nothing ever asks for less than the whole set.
interface Writable { void write(String formattedLine); }
interface Flusher { void flush(); }
interface Closer { void close(); }
final class FileDestination implements Writable, Flusher, Closer { /* one body */ }
Nobody in this design ever holds a Writable without also needing it to be Closer in the same breath: whatever opens the file is the one thing that also has to close it. Three files exist where one did. A reader who wants to know what a file destination can do now opens three of them, and no second implementation of the three interfaces exists to justify the split. STANDARD v1.0's D3 level 3 penalises this the same way it penalises a speculative Strategy interface: a seam nobody asked for is not free because it looks like more discipline. The tag is over-engineered (premature interface).
The corpus draws this line explicitly in the other direction, too. corpus/rate-limiter's KeyBudget keeps lock() and unlock() beside hasRoom() and charge(), wider than a strict reading of this principle allows. The caller has to hold one lock across a check and a charge on several budgets at once, and no single method can enforce that alone. The rule this leaves you with: narrow to exactly what each caller calls, and widen only when narrowing would take a correctness guarantee somewhere that cannot see the whole operation.
Where this lives in the app
Syllabus item C5 is the full lesson, built entirely around KeyBudget's wider surface and why it survives review. Read it for the defended exception; this page is the narrow default it is the exception to.