Skip to content

OCP - The Open/Closed Principle

Add new behaviour by adding new code, not by editing code that already works. Usually one interface and one class per variant.

Builds on Interfaces and Method overriding and polymorphism

Stage 2core2 min read

The code

package SOLID.OCP.GoodCode;

// The processor can work with any object that keeps this contract.
public interface PaymentMethod {
    void pay(double amount);
}

public class CreditCard implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Making payment via Credit Card : " + amount);
    }
}

public class UPI implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Making payment via UPI : " + amount);
    }
}

// DebitCard.java and Paypal.java implement the same interface.
public class PaymentProcessor {
    public void processPayment(PaymentMethod paymentMethod, double amount) {
        // Polymorphism chooses the implementation at runtime.
        paymentMethod.pay(amount);
    }
}

Main chooses an implementation and passes it as the interface type:

PaymentProcessor processor = new PaymentProcessor();
processor.processPayment(new CreditCard(), 100);
processor.processPayment(new UPI(), 200);

Read the second call in three steps: new UPI() creates the strategy, the processor receives it as a PaymentMethod, and paymentMethod.pay(200) dispatches to UPI.pay. This is polymorphism: the same pay call runs different code for each object.

What got better

  • UPI was added without changing PaymentProcessor. The processor is closed for modification when a new payment method arrives, and the set of implementations is open for extension.
  • Each method is isolated. A card change cannot accidentally edit UPI logic.
  • Types replace internal strings. Code passes a PaymentMethod, and an object of the wrong type fails to compile.
  • Tests become smaller. Test UPI directly, or give the processor a fake method that records the amount.
  • Polymorphism replaces the business-logic chain. The caller never asks which subtype it received.

"Closed" does not mean the file can never change. Bugs and new requirements outside this extension point still require ordinary edits.

Where did the if/else go?

The decision did not disappear. An HTTP request may still contain the string "UPI", and something must map that value to an object. Keep that mapping at the application edge:

Map<String, PaymentMethod> methods = Map.of(
        "CARD", new CreditCard(),
        "UPI", new UPI());
// Illustration only: the repo also has DebitCard and Paypal, and its Main never uses a map.

PaymentMethod method = methods.get(request.method());
if (method == null) {
    throw new IllegalArgumentException("Unsupported payment method: " + request.method());
}
processor.processPayment(method, request.amount());

The map has one small job: turn a name from outside into an implementation. Adding Net Banking means adding its class and registering it here. Payment processing itself remains unchanged. A factory or a dependency-injection container (a framework that builds objects and passes them in for you) can own the same decision in a larger application.

One thing to watch

OCP only works if every implementation keeps the same promise. A PaymentMethod whose pay() always throws "not supported" compiles, but it cannot safely replace the others. That is the concern of LSP.

There is also a readability cost: a beginner must move between an interface and several classes. Use this design where variants really change. For one stable payment method, one concrete class is simpler.

Compare with BadCode, and read OCP-Notes.md for discount and retry examples. This structure is the Strategy pattern.

In this topic

After code7 files

GoodCode/CreditCard.java10 linesGitHub
package SOLID.OCP.GoodCode;public class CreditCard implements PaymentMethod {    @Override    public void pay(double amount) {        System.out.println("Making payment via Credit Card : " + amount);    }}
GoodCode/DebitCard.java10 linesGitHub
package SOLID.OCP.GoodCode;public class DebitCard implements PaymentMethod {    @Override    public void pay(double amount) {        System.out.println("Making payment via Debit Card: " + amount);    }}
GoodCode/Main.java15 linesGitHub
package SOLID.OCP.GoodCode;public class Main {    @SuppressWarnings("unused")    public static void main(String[] args) {        PaymentProcessor processor = new PaymentProcessor();        PaymentMethod creditCard = new CreditCard();        PaymentMethod upi = new UPI();        processor.processPayment(creditCard, 100);        processor.processPayment(upi, 200);    }}
GoodCode/PaymentMethod.java6 linesGitHub
package SOLID.OCP.GoodCode;public interface PaymentMethod {    void pay(double amount);}
GoodCode/PaymentProcessor.java9 linesGitHub
package SOLID.OCP.GoodCode;public class PaymentProcessor {    public void processPayment(PaymentMethod paymentMethod, double amount) {        paymentMethod.pay(amount); // Run time poly    }}
GoodCode/Paypal.java10 linesGitHub
package SOLID.OCP.GoodCode;public class Paypal implements PaymentMethod {    @Override    public void pay(double amount) {        System.out.println("Making payment via paypal: " + amount);    }}
GoodCode/UPI.java10 linesGitHub
package SOLID.OCP.GoodCode;public class UPI implements PaymentMethod {    @Override    public void pay(double amount) {        System.out.println("Making payment via UPI : " + amount);    }}

Done with this topic?

How well do you know it?

PreviousSRP - The Single Responsibility Principle
Practise 6 questions