State the Design-by-Contract rules a subtype must obey to satisfy the Liskov Substitution Principle — preconditions, postconditions, invariants — and give an example of breaking each one.
answer
- Require no more, promise no less
- Preconditions weaken-only
- Postconditions strengthen-only, exceptions included
- Invariants preserved + history rule
- Covariant returns, contravariant params
basics
~20 sA subtype may not demand more than the base (preconditions can only be weakened), must promise at least as much as the base (postconditions can only be strengthened), and must keep every rule the base always held true (invariants preserved).
solid answer
~50 sDesign by Contract models each method as: caller guarantees the **precondition**, method guarantees the **postcondition**, and the object always satisfies its **invariants**. LSP in those terms: (1) *Preconditions may not be strengthened* — a subtype must accept everything the base accepted. Breaking it: base `withdraw(amount > 0)` but the subtype also demands `amount <= dailyLimit`, so previously legal calls now fail. (2) *Postconditions may not be weakened* — the subtype must deliver everything the base promised. Breaking it: base `find(id)` promises non-null or throws; subtype returns null. (3) *Invariants must be preserved* — properties true before and after every public operation still hold. Breaking it: base guarantees `balance >= 0`; a subtype allowing overdraft violates it. Liskov and Wing add the **history rule**: a subtype must not permit state changes the base's clients believe impossible — e.g. a mutable subclass of an immutable base. Slogan: accept more, promise more, never less.
code
pseudocode · 18 linesBase.withdraw(amount):
pre: amount > 0
post: balance decreased by exactly amount; returns receipt
inv: balance >= 0
// Precondition STRENGTHENED -> violation
SubA.withdraw: pre: amount > 0 AND amount <= 100
// Postcondition WEAKENED -> violation
SubB.withdraw: post: balance may be reduced later (async); returns null
// Invariant BROKEN -> violation
SubC.withdraw: allows balance to go negative
// All three legal directions:
SubOk.withdraw: pre: amount != 0 (weaker: accepts more)
post: also emits audit (stronger: promises more)
inv: balance >= 0 (preserved)go deeper
Recite the three rules in plain words ("can't demand more, can't deliver less, can't break the always-true rules") with one example each.
Use the DbC vocabulary correctly, get the weaken/strengthen directions right, and note that thrown exceptions count as part of the postcondition.
Add the history rule, map the rules onto language variance (covariant returns, contravariant parameters, narrower exceptions), and explain what the compiler cannot check plus how contract tests fill the gap.
Frame contract breadth as an explicit design decision with costs on both sides (loose base contracts = implementation freedom but defensive callers), reference Hyrum's Law for observed-vs-documented behaviour, and describe organisation-level enforcement via shared contract suites and API compatibility gates.
## Design by Contract in one paragraph Bertrand Meyer's **Design by Contract (DbC)** treats a method call as a business deal between caller and callee: - **Precondition** — what the *caller* must guarantee before calling ("amount must be positive", "the list must not be empty", "you must be authenticated"). If it's false, the method is not obliged to do anything sensible. - **Postcondition** — what the *method* guarantees on normal return ("the item is now in the collection", "the returned value is never null", "the balance decreased by exactly `amount`"). - **Class invariant** — a property that holds of the object before and after every public operation ("balance >= 0", "size == number of stored elements", "the connection is open or `closed` is true"). The contract is what *client code* was written against. LSP is precisely the statement that a subtype must not renege on that deal. ## Rule 1 — Preconditions may not be strengthened A subtype must accept **at least** everything the supertype accepted. It may accept *more* (weaker precondition) — no existing caller can be harmed by a method becoming more tolerant. **Violation:** base `PaymentGateway.charge(amount)` documented as "amount > 0". Subtype `SmallMerchantGateway.charge` additionally requires `amount <= 500` and throws otherwise. A client that legitimately charges 900 now fails at runtime, having checked everything it was told to check. More violations in the wild: an override requiring a non-null argument where the base accepted null; requiring `init()` to have been called first; requiring the caller to hold a lock; requiring a specific thread; requiring the collection be sorted. **Why it's a violation:** the caller's obligation was fixed when it was written against the base. Raising the bar retroactively breaks working code. ## Rule 2 — Postconditions may not be weakened A subtype must deliver **at least** everything the supertype promised. It may promise *more* (stronger postcondition) — e.g. also guaranteeing the result is sorted, or that the operation is idempotent. **Violations:** - Base `Repository.findById(id)` promises "returns the entity or throws NotFound"; subtype returns `null`. - Base `Sorter.sort(list)` promises a stable, fully sorted result; subtype sorts approximately or is unstable. - Base `Queue.enqueue(x)` promises the item is retrievable later; a "lossy" subtype drops items under pressure. - Base promises a synchronous completed write; subtype returns after only queuing the write. **Exceptions are part of the postcondition.** Throwing a *new, broader* exception type the client was never told about weakens the postcondition. This is why languages allow overrides to declare **fewer or narrower** checked exceptions, never more. The pathological case — an override that unconditionally throws "operation not supported" — is the most common LSP violation in real codebases. ## Rule 3 — Invariants must be preserved Everything the base guaranteed to be permanently true must remain true. A subtype may *add* invariants only if they don't contradict the base's or restrict operations the base allowed. **Violations:** - Base `Account` invariant `balance >= 0`; `OverdraftAccount` allows negatives — every client's "balance is safe to display as unsigned / to subtract from" logic becomes wrong. - Base `Rectangle` implicitly holds "width and height vary independently"; `Square` adds `width == height`, contradicting it. - Base `Collection` invariant `size() == count of elements iterated`; a subtype with lazy or deduplicating semantics breaks it. ## The history rule (Liskov & Wing's addition) Beyond a single call, the subtype must not allow **state transitions the supertype's clients believe are impossible**. The canonical example: an immutable `Point` base — clients cache it, share it across threads, use it as a map key. A mutable subclass `MutablePoint` compiles fine and shatters all three assumptions, even though no single method's pre/postconditions are violated in isolation. The history rule generalises invariants over the object's whole lifetime. ## The mnemonic and the variance connection > **Require no more, promise no less.** (Or: be liberal in what you accept, conservative in what you promise.) Type systems encode a fragment of this automatically: - **Covariant return types** (override returns a narrower type) = postcondition strengthening → allowed. - **Contravariant parameter types** (override accepts a wider type) = precondition weakening → theoretically allowed, though most mainstream OO languages require invariant parameters and would treat a different parameter type as an overload. - **Exception covariance** (fewer/narrower thrown types) → allowed. Everything expressed in *values* rather than types — "positive", "non-empty", "sorted", "idempotent" — is outside the compiler's reach and must be enforced by documentation, assertions, or tests. ## Enforcing it in practice - **Contract tests / abstract test suites**: one suite written purely against the supertype's contract, parameterised over all implementations. This is the single highest-value technique. - **Assertions or runtime contracts**: `require`/`ensure`-style checks; some languages/frameworks inherit them automatically down the hierarchy so a strengthened precondition fails loudly. - **Review heuristics**: any `UnsupportedOperationException` in an override, any new required call-order, any nullable return where the base was non-null, any `instanceof`/`is` check in client code. - **Property-based testing** on the base's stated properties, run per implementation. ## Trade-offs and nuance - **The contract is a design choice.** If the base explicitly documents "implementations may impose their own limits and throw `LimitExceeded`", the limiting subtype is compliant — the precondition was always that weak. Writing deliberately weak base contracts buys implementation freedom at the cost of pushing defensive handling onto every caller. Writing strong base contracts gives callers simple code but constrains what can implement the type. - **Documented vs. observed contracts.** In practice clients depend on *observed* behaviour, not just documented behaviour (Hyrum's Law). A technically-legal subtype can still break the ecosystem; that's a real-world qualifier to the theory. - **Performance is normally outside the contract**, but a subtype that turns an O(1) documented operation into a network round-trip can break callers just as effectively — worth documenting complexity when it matters.
- Which of these three rules does a compiler actually enforce?Only the type-shaped fragment: covariant return types, and no new/broader checked exceptions. Parameter contravariance is theoretically the precondition rule but most mainstream languages require invariant parameter types. Value-level conditions like "amount must be positive" or "result is never empty" are entirely unchecked.
- Is a subtype that throws a documented exception the base never mentioned always a violation?Yes, if clients were told the base's failure modes are exhaustive — a new failure mode is postcondition weakening. It is *not* a violation if the base contract already says "implementations may throw X under their own conditions", because then the weaker promise was the contract all along.
- How do you actually verify these rules in a codebase?Abstract/contract test suites executed against every implementation of the type, plus runtime assertions for pre/postconditions, plus review heuristics (throw-on-override, new required call ordering, nullable returns where the base was non-null, type checks in client code).
A franchise agreement: the brand promises every outlet accepts any card, opens by 8am, and serves the whole menu. A new outlet may open earlier and add dishes (promise more), but if it starts refusing cards under £5 (demanding more of customers) or closes at noon (delivering less), customers who trusted the brand are stranded — even though the sign over the door is identical.
saying these in an interview costs you the question
- Getting the direction backwards: "subtypes may strengthen preconditions since they're more specific"
- Thinking postconditions and preconditions vary in the same direction
- Forgetting that thrown exceptions are part of the postcondition
- Believing the compiler checks all three rules
- Ignoring the history rule — a mutable subclass of an immutable base looks fine method-by-method