skip to content

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?

level: seniorimportance: must knowfreq 58%

answer

  1. LSP: preconditions no stronger, postconditions no weaker, invariants kept
  2. Smell: caller must know which implementation it got
  3. Square/Rectangle breaks on mutation, not on shape
  4. ISP: segregate by client role, not method count
  5. Fat interface → stub throws → LSP break; contract tests enforce both

basics

~20 s

LSP: 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
pseudocode
// 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 suite

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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.

context