Implementing OCP with interfaces and polymorphism in Java
Fast recall
Quick revision
Core answer
Interview-ready guide
Interview answer
A common Java implementation of OCP is to isolate a behavior that genuinely varies behind an interface. The stable class depends on that contract, and each variant implements it.
For example, PricingService can receive a DiscountPolicy with discountFor(Order). SeasonalDiscount and MemberDiscount contain their own rules. Adding FirstOrderDiscount adds a class and tests without adding another type check to PricingService. The composition root or Spring configuration still selects which policy applies.
The interface is useful only if its contract stays stable. If the client still uses instanceof, or every new variant forces another method into the interface, the change has not been isolated. OCP reduces changes to stable logic; it does not promise zero changes or automatic runtime switching.
Implementing OCP with interfaces and polymorphism in Java
Direct Answer
Use an interface when a real behavior varies, make the client call that interface, and place each variant in its own implementation. Adding a variant then leaves the client algorithm unchanged, although composition-root registration, configuration, and tests may still change. An interface alone does not guarantee OCP if the client still switches on concrete types.
Code Example
// The abstraction - defines the contract
interface DiscountStrategy {
double applyDiscount(double amount);
}
// Implementation 1: Percentage discount
class PercentageDiscount implements DiscountStrategy {
private final double percentage;
public PercentageDiscount(double percentage) {
this.percentage = percentage;
}
public double applyDiscount(double amount) {
return amount * (1 - percentage / 100);
}
}
// Implementation 2: Fixed amount discount
class FixedAmountDiscount implements DiscountStrategy {
private final double discountAmount;
public FixedAmountDiscount(double discountAmount) {
this.discountAmount = discountAmount;
}
public double applyDiscount(double amount) {
return Math.max(0, amount - discountAmount);
}
}
@Service
class PricingService {
private final DiscountStrategy discountStrategy;
// Injected by Spring - depends on interface only
public PricingService(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
public double calculateFinalPrice(double basePrice) {
return discountStrategy.applyDiscount(basePrice);
}
}
// Adding new strategy later - zero changes to PricingService
class BuyOneGetOneDiscount implements DiscountStrategy {
public double applyDiscount(double amount) {
return amount * 0.5; // 50% off for BOGO
}
}
Common Mistakes & Pitfalls
- Creating anemic interfaces: Defining an interface with just one or two methods but no clear abstraction purpose. Interfaces should represent meaningful concepts, not just mirror one class.
- Hardcoding types somewhere else: Creating the interface but then using instanceof checks or a factory that hardcodes specific implementations. The whole point is avoiding type checks.
- Not using dependency injection: Manually instantiating implementations inside the service defeats OCP. Use constructor injection and let Spring or a factory provide the implementation.
- Interface explosion: Creating interfaces for every class reflexively. Only abstract things that vary or will vary.
Check your understanding
Answer these without looking back at the article.
- What is the Strategy design pattern?
- How does Spring Boot wiring support OCP?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.