This interface has twelve methods and every implementation throws UnsupportedOperationException for four of them. What went wrong, and how would you fix it?
The interface was assembled from the union of what implementations do rather than from what any one caller needs, so no implementation can honour the whole contract. Split it by client into role interfaces, working from the call sites rather than from the classes.
What the interviewer is scoring
- Whether the candidate names both the segregation failure and the substitutability failure
- Does the proposed grouping come from reading call sites rather than from reading implementations
- That optional operations are recognised as a deliberate library trade in some cases rather than always a defect
- Whether a silent no-op default is rejected where it would discard work
- Does the candidate resist fragmenting into one interface per method
Answer
Two principles are failing at once
The visible failure is interface segregation: callers depend on methods they do not use, so a change to the export half of the interface forces recompilation and retesting on classes that only ever read. That is the cost usually quoted, and on its own it sounds like tidiness.
The more serious failure is substitutability. An interface is a promise that any implementation can be used wherever the type is declared, and an implementation that throws on a third of the contract breaks that promise. The practical consequence is that callers start asking which concrete type they hold, either explicitly or by catching the exception, and once that happens the interface has stopped providing polymorphism and is only providing a shared name. You have paid for an abstraction and are getting the coupling of a concrete dependency plus a runtime failure mode.
The cause is almost always the same. The interface was extracted by taking the union of the methods the existing classes happened to have, rather than by asking what any single caller needs in order to do its job. Union-derived interfaces grow every time an implementation is added, which is the diagnostic to remember: an interface that changes whenever a new implementer appears is being written from the wrong end.
Group by client, not by class
The refactoring is driven from the call sites. For each place the interface is used, list the methods that call site actually invokes. Those lists cluster, and the clusters are the real interfaces — the roles the type plays for somebody.
A document store that has grown twelve methods usually turns out to serve three separate roles: readers that fetch and search, writers that store and delete, and an administrative path that reindexes and reports statistics. Those become DocumentReader, DocumentWriter and IndexMaintenance. The read-only implementation implements the first and nothing else, so its inability to write is now expressed in the type system instead of at runtime, and a caller that only reads cannot accidentally be handed something that mutates.
A class may implement several of these, and the concrete production class probably implements all three. That is not a failure of the split. The point was never to fragment the implementations, it was to narrow what each caller depends on, and the same object can present three faces to three collaborators.
Two guards keep this from going too far. One method per interface is not the goal; methods that a caller always uses in one unit of work belong together, and splitting them multiplies parameters and names without reducing any real coupling. And a split that leaves every caller depending on all the pieces has bought nothing except more files, which is the honest signal that the interface was cohesive after all and the throwing methods are a different problem.
Which alternatives are honest and which are not
Three other fixes get proposed, and only some of them are defensible.
Capability queries — an isMutable() that callers check before calling — are honest in that nothing lies, and they push branching into every caller and leave a race between the check and the call. Acceptable for a library that genuinely must expose heterogeneous implementations behind one type, poor as a first choice in your own code, where the type system can carry the distinction for free.
A no-op default implementation is the dangerous one. Replacing a thrown exception with an empty method body converts a loud failure into silent data loss: the caller believes the document was stored, nothing was stored, and nothing anywhere records that. A no-op is only correct where doing nothing is a genuine, documented semantic — a metrics recorder that discards, a cache that chooses not to retain — never where it fakes a capability the object lacks.
Default methods on the interface are for adding convenience over existing behaviour, so that adding a method does not break implementers. Using them to supply a plausible-looking body for an operation the implementation cannot perform is the same mistake as the no-op, dressed in newer syntax.
When optional operations are a deliberate trade
It is worth knowing that this exact design exists on purpose in the Java collections library, where mutating methods are documented as optional and the immutable implementations throw UnsupportedOperationException. That was a considered decision: one hierarchy that every collection in the language shares, at the price of a contract with holes in it, taken at a time when the alternative was several parallel hierarchies and no interoperability between them.
The reason it is tolerable there is that the holes are documented as part of the specification, the exception is the specified behaviour rather than an accident, and the callers are the whole world rather than a codebase you control. None of those conditions holds for an internal interface with three implementations, where you can simply split it. So citing the JDK as precedent is a weak defence of your own fat interface, and knowing why the trade was made there is a much stronger answer than either praising or condemning it.
Doing it on an interface others implement
If the interface is published, the split is a migration rather than an edit. Introduce the narrow interfaces first and have the wide one extend them, which is source-compatible for everybody: existing implementers still satisfy it, existing callers still compile. Then move call sites to the narrowest interface each one needs, one at a time, which is where the benefit is actually realised. Only once nothing depends on the wide type does it get deprecated and eventually removed.
That sequence matters because it makes the change reversible at every step, and because it lets you stop early if the new grouping turns out to be wrong — which you will only discover by trying to move real callers onto it.
An implementation that throws for part of its interface has told you the interface belongs to nobody. Derive interfaces from what individual callers need, and let a class implement several, rather than making every caller depend on the union of everything anybody does.
Likely follow-ups
- Where should each new interface physically live, and which side of the boundary owns it?
- One caller genuinely needs every method. Has the split bought you anything at all?
- The JDK ships collection interfaces with optional operations. Why was that a defensible trade there?
- How would you perform this split on an interface other teams already implement?
Related questions
- Give me a real single-responsibility violation you have seen, and how would you refactor it?mediumAlso on solid and refactoring5 min
- A test that has passed for a year starts failing after a refactor that was supposed to change nothing. Is it a bug or a bad test?mediumAlso on refactoring4 min
- Give me a subclass that the compiler accepts but that still breaks the Liskov substitution principle. What rule does it break?mediumAlso on liskov-substitution4 min
- When would you use the strategy pattern instead of inheritance?mediumAlso on refactoring5 min
- What is the dependency inversion principle, and what exactly is being inverted?mediumAlso on solid3 min
- A customer bought cover on your site last night and this morning the card payment failed. Are they on risk, and what does your system do next?hardSame kind of round: scenario5 min
- Your suite mocks the database, every test passes, and the release still broke. What should you have tested instead?mediumSame kind of round: concept5 min
- This table has fourteen indexes and writes have got slower. How do you work out which ones to drop?hardSame kind of round: scenario6 min