What do the Liskov Substitution Principle and the Interface Segregation Principle actually require, and how do violations of each show up above the code level — in service contracts and public APIs?
answer
- LSP: preconditions no stronger, postconditions no weaker, invariants kept
- Smell: caller must know which implementation it got
- Square/Rectangle breaks on mutation, not on shape
- ISP: segregate by client role, not method count
- Fat interface → stub throws → LSP break; contract tests enforce both
basics
~20 sLSP: any implementation of a type must be usable wherever that type is expected, without callers needing to know which one they got. ISP: don't force a client to depend on methods it doesn't use — prefer several small, role-specific interfaces over one fat one.
solid answer
~50 s**LSP** (Barbara Liskov, 1987) is a behavioral subtyping rule, not a syntax rule: a subtype must honor the supertype's contract — it may weaken preconditions and strengthen postconditions, must preserve invariants and history constraints, and must not throw new unexpected exceptions. The tell-tale violation is a caller that must type-check or catch `UnsupportedOperation` to work out which implementation it has. **ISP** says clients should not be forced to depend on methods they don't use; fat interfaces cause recompilation, force empty stub implementations, and drag unnecessary dependencies across boundaries. Above code level both scale directly: LSP is what makes multiple providers behind one API interchangeable — if `/v2` silently tightens validation or one implementation of a payment port is synchronous and another eventual, callers branch on the provider and the abstraction is fake. ISP becomes endpoint/consumer-specific contracts: separate read and write endpoints, consumer-driven contracts, or a per-consumer facade instead of one god service that every client couples to.
code
pseudocode · 15 lines// LSP violation: subtype strengthens a precondition / throws a new exception
interface Storage { fun delete(key: String) } // contract: key removed after return
class S3Storage : Storage {
override fun delete(key: String) { /* eventually consistent: may still read */ }
} // weaker postcondition -> callers branch
class ReadOnlyStorage : Storage {
override fun delete(key: String) = throw UnsupportedOperationException()
} // new exception -> not substitutable
// ISP + LSP fix: split by role so no one implements what it cannot honor
interface ReadableStorage { fun get(key: String): Bytes }
interface WritableStorage : ReadableStorage { fun delete(key: String) }
// and make the consistency guarantee explicit in the contract, tested by one shared suitego deeper
Define both simply and give one example each: a subclass that throws on an inherited method breaks LSP; splitting a big interface so a class doesn't implement empty methods is ISP.
State LSP's contract rules (preconditions can't strengthen, postconditions can't weaken, invariants preserved, no new exceptions) and show how ISP violations cause LSP violations via stub methods.
Diagnose by the 'client must know which implementation it got' smell, prescribe contract tests, apply role interfaces, and carry both principles up to API versioning and multi-provider ports including non-functional guarantees.
Treat the contract — including consistency, idempotency, ordering, and error taxonomy — as the real architectural asset; institutionalize it with shared/consumer-driven contract tests, additive-only evolution policy, and deprecation processes, and be explicit about when a genuinely non-substitutable provider should get its own port rather than a leaky common one.
## Liskov Substitution Principle ### Statement Barbara Liskov's 1987 formulation, later refined with Jeannette Wing: if `S` is a subtype of `T`, objects of type `T` may be replaced with objects of type `S` **without altering any of the desirable properties of the program** that a client written against `T` relies on. Crucially it constrains *behavior*, not just method signatures — a compiler can enforce the signatures; only design discipline enforces the contract. ### The concrete rules Expressed as a contract (Bertrand Meyer's design by contract vocabulary): - **Preconditions may not be strengthened.** If the base accepts any non-negative amount, the subtype may not reject amounts over 100. A caller written to the base contract would suddenly fail. - **Postconditions may not be weakened.** If the base guarantees the list is returned sorted, the subtype must too. - **Invariants must be preserved.** Anything the base promises always holds (a balance never negative) must still hold. - **History constraint.** The subtype must not allow state changes the base forbade — e.g., making an immutable base type mutable. - **Exceptions.** The subtype must not throw new exception types the base contract didn't allow (`UnsupportedOperationException` for a documented-valid input is the archetypal violation). - Signature-level variance rules (contravariant parameters, covariant returns) fall out of the same reasoning and are the part languages *can* check. ### Diagnosing violations The practical smell: **the client has to know which implementation it got.** `if (shape is Square) …`, catching `UnsupportedOperationException`, checking a `supportsX()` capability flag before every call, or documentation that says "note: with the S3 implementation, deletes are eventually consistent". Canonical examples: - **Square/Rectangle.** If `Rectangle` allows setting width and height independently, `Square` cannot be a behavioral subtype — code that sets width to 5, height to 4, and asserts area 20 breaks. The real fix is not clever code; it's that the *mutable* rectangle contract doesn't admit a square. Immutable value types dissolve the problem. - **Read-only collection typed as a mutable collection.** `add()` throws — a caller written against the mutable contract fails at runtime. - **A bird hierarchy where `Penguin.fly()` throws.** Model capabilities, not taxonomy. ### Fixes Narrow the base contract to what *all* implementations can honor; split the type by capability (which is ISP arriving from the other direction); prefer composition over inheritance; use immutability to eliminate mutator-driven violations; and test the contract, not the class — write a shared test suite ("contract test") that every implementation must pass. That last one is the strongest practical tool, and it scales to services. ### Why it is the substitutability enabler OCP, DIP, and every plugin architecture rest on LSP. If implementations aren't truly substitutable, the abstraction is decorative: callers branch, the boundary leaks, and adding an implementation modifies existing code — which is exactly what OCP promised to avoid. ## Interface Segregation Principle ### Statement "Clients should not be forced to depend upon interfaces they do not use." Robert Martin's origin story is a Xerox printer system where one fat `Job` class meant a change for one job type triggered recompilation and redeployment of everything. ### What the violation costs - **Recompilation/redeploy ripple**: a change to a method you never call still forces you to rebuild (and in a distributed system, to re-certify a contract). - **Stub implementations**: implementers write empty or throwing methods for parts that don't apply — which then becomes an LSP violation. This is the direct link between the two principles. - **Dependency drag**: a fat interface's signatures reference types the client doesn't need, pulling extra packages/modules across the boundary. - **Comprehension cost and fake substitutes**: test doubles must implement 20 methods to exercise one. ### Applying it Segregate by **client role**, not by arbitrary size. A file abstraction used by a reader and a writer becomes `Reader` and `Writer`; a class may still implement both. This is Martin Fowler's *role interface* vs. *header interface* distinction: define the interface from the consumer's need, not by mirroring an existing class. Note that ISP and DIP compose: the port belongs to the consumer *and* is shaped by the consumer's role. Over-application produces one-method interfaces everywhere and an explosion of types with no cohesive concept; the criterion is *does a distinct client depend on a distinct subset?*, not "fewer methods is better". ## Scaling both above the code boundary **LSP at API/service scale:** - Multiple providers behind one abstraction (payment gateways, storage backends, identity providers) are substitutable only if they honor one behavioral contract — including error taxonomy, idempotency, ordering, and consistency guarantees. "Same interface, different semantics" is the most expensive form of fake abstraction, because it is invisible at compile time. - API evolution is subtyping over time: a new version may accept *more* inputs (weaker precondition) and guarantee *more* about outputs (stronger postcondition) without breaking clients; tightening validation or removing a guarantee is an LSP violation shipped as a minor release. Robustness rules like "be conservative in what you send, liberal in what you accept" and additive-only schema evolution are LSP restated for the wire. - **Consumer-driven contract tests** are the service-scale version of a shared contract test suite: each consumer's expectations run against every provider implementation and against new versions. **ISP at API/service scale:** - One god service that every client depends on recreates the printer problem: any change forces coordination with all consumers. Splitting by consumer role — separate endpoints, separate schemas, per-client facades or BFFs (backend-for-frontend), CQRS-style separation of read and write contracts — restores independent evolution. - Fat DTOs and "return the whole aggregate" endpoints are stamp coupling plus an ISP violation: clients depend on fields they never read, so any field change becomes a negotiation, and over-fetching leaks data the client shouldn't see. - GraphQL-style field selection and per-consumer projections are one mechanism to let each client depend only on what it uses. ## How they interact ISP violations *cause* LSP violations: force a class to implement methods it can't support and it will throw. LSP violations *undermine* DIP and OCP: if the plugin isn't substitutable, the port isn't a boundary. And both are ultimately statements about the same thing this topic is about — **making a boundary trustworthy enough that a caller can depend on the abstraction and stop thinking about what is behind it.**
- Two implementations of the same interface differ only in that one is eventually consistent. Is that an LSP violation?Yes, if callers written against the interface assume read-after-write. LSP covers behavioral guarantees, not just signatures; consistency, ordering, idempotency, latency class, and error taxonomy are part of the contract. Either weaken the published contract so *all* implementations honor it (and make callers handle it), or stop pretending the two are substitutable behind one port.
- How do you enforce LSP in practice rather than by review discipline?Write the tests against the *interface*: a shared contract test suite that every implementation must pass, including error cases and edge inputs. At service scale, consumer-driven contract tests do the same job across versions and providers. Property-based tests are especially effective for invariants like idempotency and ordering.
- Isn't ISP just 'make interfaces small'?No — the criterion is client roles, not method counts. If every client uses all ten methods, the ten-method interface is fine. If two clients use disjoint subsets, split even a four-method interface. Blind minimization produces a swarm of one-method types with no cohesive concept and worse discoverability.
- Give a real ISP violation at the service level and how you'd fix it.A single 'customer service' endpoint returning the full aggregate, consumed by a mobile app that needs a name and a billing job that needs a tax ID. Any field change requires coordinating both consumers, and each over-fetches data it shouldn't hold. The fix is role-shaped contracts: separate endpoints or projections per consumer (or a BFF), with consumer-driven contract tests pinning what each actually depends on.
saying these in an interview costs you the question
- Reducing LSP to 'subclasses must implement the parent's methods' — that's the compiler's job; LSP is about behavior, contracts, and exceptions.
- Presenting the Square/Rectangle case as solvable by a clever override rather than by fixing the contract (or using immutable values).
- Throwing `UnsupportedOperationException` from an interface method and calling it 'just documentation'.
- Treating capability flags (`supportsDelete()`) that callers must check as compatible with substitutability.
- Interpreting ISP as 'every interface should have one method' instead of segregating by client role.
- Ignoring that non-functional guarantees — consistency, ordering, idempotency, error taxonomy — are part of the contract that substitutability depends on.
- Assuming an added-in-a-minor-version stricter validation rule is safe because the schema is unchanged.