LSP and interface contracts — behavioral expectations
Fast recall
Quick revision
Core answer
Interview-ready guide
Interview answer
LSP treats an interface or base class as a behavioral contract. Clients rely not only on parameter and return types, but also on valid inputs, required results, state changes, invariants, and documented errors.
For example, if CustomerRepository.save promises to return the persisted customer and reject duplicate email addresses with a documented domain error, both an in-memory test repository and a database repository must preserve those behaviors. Returning null from one implementation would break substitutability even though the signature compiles.
Formally, a subtype must not strengthen preconditions or weaken postconditions, and it must preserve invariants. Shared contract tests are a practical way to check those guarantees across implementations.
LSP and interface contracts — behavioral expectations
Direct Answer
LSP gives interface and base-class contracts behavioral meaning. An implementation must follow more than the method signature: it must accept valid inputs, provide required results, preserve invariants, and use documented failures. Preconditions cannot be strengthened and postconditions cannot be weakened for a subtype.
Code Example
interface PaymentService {
// Contract: returns transaction ID, throws PaymentException on failure
// Postcondition: order status is updated to PAID or FAILED
String processPayment(Long orderId, BigDecimal amount);
}
@Service
class StripePaymentService implements PaymentService {
@Override
public String processPayment(Long orderId, BigDecimal amount) {
var transactionId = stripeApi.charge(amount); // Call Stripe
orderRepo.updateStatus(orderId, OrderStatus.PAID); // Honors contract
return transactionId;
}
}
// Contract violation: returns null, doesn't update order
@Service
class MockPaymentService implements PaymentService {
@Override
public String processPayment(Long orderId, BigDecimal amount) {
return null; // Violates postcondition: must return transaction ID
// Missing: order status update
}
}
Common Mistakes & Pitfalls
- Checking only signatures: matching parameter and return types does not preserve behavior.
- Strengthening preconditions: a subtype cannot reject inputs the base contract accepts.
- Weakening results or invariants: an implementation must provide what the contract promises.
- Assuming one failure style without documentation: decide whether not-found means empty, null, or an exception and state it.
- Using a test double with different behavior: fakes should preserve the guarantees relevant to the client.
Check your understanding
Answer these without looking back at the article.
- What is Design by Contract?
- How do you document behavioral contracts in Java?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.