Skip to content

Open/Closed

TL;DR: Classes should be open for extension but closed for modification. When you need to add a new payment type, you should be able to write a new class without touching the existing payment processing code. If adding a feature means editing working code, the design is wrong.

Open/Closed Principle

Why This Matters in Interviews

OCP is what separates candidates who build rigid systems from those who build flexible ones. When an interviewer says "now add support for cryptocurrency payments," they're testing whether your design can absorb new requirements without modifying existing, tested code.

If your answer is "I'd add another if/else in the PaymentProcessor," you've just demonstrated a design that gets worse with every new feature. If your answer is "I'd create a new CryptoPayment class that implements the PaymentMethod interface," you've shown a design that scales cleanly.

The Problem: If/Else Chains That Never Stop Growing

// BAD: Every new payment type requires modifying this class
class PaymentProcessor {
    public void processPayment(String type, double amount) {
        if (type.equals("credit_card")) {
            System.out.println("Charging credit card: $" + amount);
            // Credit card API calls...
            // Fraud checks specific to cards...
        } else if (type.equals("paypal")) {
            System.out.println("Processing PayPal: $" + amount);
            // PayPal OAuth flow...
            // PayPal-specific refund handling...
        } else if (type.equals("bank_transfer")) {
            System.out.println("Initiating bank transfer: $" + amount);
            // ACH processing...
            // 3-day settlement delay logic...
        }
        // Next month: add crypto? Another else-if.
        // Month after: add Apple Pay? Another else-if.
        // This class grows forever.
    }
}
// BAD: Same problem in C++
class PaymentProcessor {
public:
    void processPayment(const std::string& type, double amount) {
        if (type == "credit_card") {
            std::cout << "Charging credit card: $" << amount << std::endl;
        } else if (type == "paypal") {
            std::cout << "Processing PayPal: $" << amount << std::endl;
        } else if (type == "bank_transfer") {
            std::cout << "Initiating bank transfer: $" << amount << std::endl;
        }
    }
};
# BAD: Same problem in Python
class PaymentProcessor:
    def process_payment(self, payment_type: str, amount: float) -> None:
        if payment_type == "credit_card":
            print(f"Charging credit card: ${amount}")
        elif payment_type == "paypal":
            print(f"Processing PayPal: ${amount}")
        elif payment_type == "bank_transfer":
            print(f"Initiating bank transfer: ${amount}")

What's Wrong Here?

Every new payment type requires modifying PaymentProcessor. This means:

  • Risk — you're editing tested, working code every time you add a feature
  • Merge conflicts — every developer adding a payment type touches the same file
  • Testing burden — you have to re-test all payment paths, not just the new one
  • Growing complexity — the method gets longer and harder to read with each addition

The Fix: Extend, Don't Modify

// GOOD: Define a contract, then extend with new classes

interface PaymentMethod {
    void pay(double amount);
    boolean supports(String type);
}

class CreditCardPayment implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Charging credit card: $" + amount);
        // Credit card API calls, fraud checks...
    }

    @Override
    public boolean supports(String type) {
        return "credit_card".equals(type);
    }
}

class PayPalPayment implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Processing PayPal: $" + amount);
        // PayPal OAuth, refund handling...
    }

    @Override
    public boolean supports(String type) {
        return "paypal".equals(type);
    }
}

// PaymentProcessor is now CLOSED for modification
class PaymentProcessor {
    private final List<PaymentMethod> methods;

    public PaymentProcessor(List<PaymentMethod> methods) {
        this.methods = methods;
    }

    public void processPayment(String type, double amount) {
        PaymentMethod method = methods.stream()
            .filter(m -> m.supports(type))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("Unknown payment type: " + type));
        method.pay(amount);
    }
}
// GOOD: Same approach in C++

class PaymentMethod {
public:
    virtual ~PaymentMethod() = default;
    virtual void pay(double amount) = 0;
    virtual bool supports(const std::string& type) const = 0;
};

class CreditCardPayment : public PaymentMethod {
public:
    void pay(double amount) override {
        std::cout << "Charging credit card: $" << amount << std::endl;
    }

    bool supports(const std::string& type) const override {
        return type == "credit_card";
    }
};

class PayPalPayment : public PaymentMethod {
public:
    void pay(double amount) override {
        std::cout << "Processing PayPal: $" << amount << std::endl;
    }

    bool supports(const std::string& type) const override {
        return type == "paypal";
    }
};

class PaymentProcessor {
private:
    std::vector<std::unique_ptr<PaymentMethod>> methods;

public:
    PaymentProcessor(std::vector<std::unique_ptr<PaymentMethod>> methods)
        : methods(std::move(methods)) {}

    void processPayment(const std::string& type, double amount) {
        for (const auto& method : methods) {
            if (method->supports(type)) {
                method->pay(amount);
                return;
            }
        }
        throw std::invalid_argument("Unknown payment type: " + type);
    }
};
# GOOD: Same approach in Python

from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, amount: float) -> None:
        pass

    @abstractmethod
    def supports(self, payment_type: str) -> bool:
        pass


class CreditCardPayment(PaymentMethod):
    def pay(self, amount: float) -> None:
        print(f"Charging credit card: ${amount}")

    def supports(self, payment_type: str) -> bool:
        return payment_type == "credit_card"


class PayPalPayment(PaymentMethod):
    def pay(self, amount: float) -> None:
        print(f"Processing PayPal: ${amount}")

    def supports(self, payment_type: str) -> bool:
        return payment_type == "paypal"


class PaymentProcessor:
    def __init__(self, methods: list[PaymentMethod]):
        self._methods = methods

    def process_payment(self, payment_type: str, amount: float) -> None:
        for method in self._methods:
            if method.supports(payment_type):
                method.pay(amount)
                return
        raise ValueError(f"Unknown payment type: {payment_type}")

Now adding cryptocurrency payments means writing one new class:

// Adding a new payment type = ZERO changes to existing code
class CryptoPayment implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Processing crypto payment: $" + amount);
        // Blockchain transaction logic...
    }

    @Override
    public boolean supports(String type) {
        return "crypto".equals(type);
    }
}

No existing class was modified. No existing tests need to change. The PaymentProcessor didn't even know crypto existed, and it works anyway.

Interview tip: This pattern is actually the Strategy pattern in disguise. When you apply OCP by defining an interface and swapping implementations, you're using Strategy. We'll see it again in the design patterns chapter. Recognizing this connection shows depth.

When OCP Doesn't Apply

Not every if/else needs to become an interface. If the set of options is small, stable, and unlikely to change (e.g., checking if a number is positive, negative, or zero), an if/else is fine. OCP pays off when the list of options is expected to grow.

Ask yourself: "Will someone need to add a new case here in six months?" If yes, design for extension. If no, keep it simple.

Interview tip: Don't reflexively apply OCP everywhere. If the interviewer asks you to handle two cases and you build an elaborate plugin architecture, you've over-engineered. Apply OCP when you can see the extension point coming.

Quick Recap

Concept What it means Why it matters
Closed for modification Existing, tested code stays untouched when you add features No risk of breaking working functionality
Open for extension New behavior is added by writing new classes, not editing old ones Features scale without growing complexity
Interface as extension point Define a contract; new implementations extend behavior Each implementation is independent and testable
Strategy pattern preview OCP + interfaces = Strategy pattern Same concept will reappear in design patterns