Appearance
Open/Closed Principle (OCP)
SOLID Series — This article is Part 2 of 5. See also: SRP · LSP · ISP · DIP
Introduction
The Open/Closed Principle, formulated by Bertrand Meyer and popularized by Robert C. Martin, states that software entities — classes, modules, functions — should be open for extension but closed for modification. In practice, this means you should be able to add new behavior to a system by writing new code, not by editing existing, proven code. Modifying existing code risks breaking behavior that already works and is already tested.
Core Concept
The key insight behind OCP is that the cost of modifying existing code is higher than the cost of adding new code. Every time you open an existing class to add a case to a switch statement or a branch to an if/else chain, you risk introducing regressions and you invalidate previously passing tests. By designing around stable abstractions — interfaces or abstract classes — you create extension points where new behavior can be plugged in without touching anything that already works.
The Strategy and Template Method design patterns are the canonical mechanisms for achieving OCP in object-oriented code.
Anti-Pattern — Switch Statement Payment Processor
The most common OCP violation is a class that uses switch or if/else to select behavior based on a type discriminator. Adding a new payment method means opening the class, adding a new case, and re-testing everything.
java
// ANTI-PATTERN: PaymentProcessor that must be modified for every new payment method
public class PaymentProcessor {
public void processPayment(String paymentType, double amount) {
switch (paymentType) {
case "CREDIT_CARD":
System.out.println("Processing credit card payment of $" + amount);
// Credit card specific logic: validation, gateway call, etc.
validateCreditCard();
chargeViaGateway(amount);
break;
case "PAYPAL":
System.out.println("Processing PayPal payment of $" + amount);
// PayPal specific logic
redirectToPayPal(amount);
awaitPayPalCallback();
break;
case "BITCOIN":
System.out.println("Processing Bitcoin payment of $" + amount);
// Crypto specific logic
generateBitcoinAddress();
awaitBlockchainConfirmation(amount);
break;
// Adding Apple Pay means opening this class and adding another case ← OCP violation
// case "APPLE_PAY":
// ...
default:
throw new IllegalArgumentException("Unknown payment type: " + paymentType);
}
}
private void validateCreditCard() { /* ... */ }
private void chargeViaGateway(double amount) { /* ... */ }
private void redirectToPayPal(double amount) { /* ... */ }
private void awaitPayPalCallback() { /* ... */ }
private void generateBitcoinAddress() { /* ... */ }
private void awaitBlockchainConfirmation(double amount) { /* ... */ }
}Problems with this design:
- Adding Apple Pay means modifying
PaymentProcessor, risking breakage of existing credit card and PayPal logic. - The class grows unbounded as payment methods multiply.
- Unit tests for credit card logic must include the entire class with all other payment methods present.
- The string-based type discriminator
"CREDIT_CARD"is error-prone.
Correct Implementation — Strategy Pattern
The fix is to extract a PaymentStrategy interface that represents the stable abstraction. Each payment method becomes its own class, and PaymentProcessor depends only on the interface.
java
// CORRECT: Stable abstraction — never needs modification
public interface PaymentStrategy {
void processPayment(double amount);
boolean supports(String paymentType);
String getPaymentType();
}java
// CORRECT: Credit card implementation — closed to modification, written once
public class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
private final String cvv;
private final String expiryDate;
public CreditCardPayment(String cardNumber, String cvv, String expiryDate) {
this.cardNumber = cardNumber;
this.cvv = cvv;
this.expiryDate = expiryDate;
}
@Override
public void processPayment(double amount) {
System.out.printf("Charging $%.2f to credit card ending in %s%n",
amount, cardNumber.substring(cardNumber.length() - 4));
validateCard();
chargeGateway(amount);
}
@Override
public boolean supports(String paymentType) {
return "CREDIT_CARD".equalsIgnoreCase(paymentType);
}
@Override
public String getPaymentType() { return "CREDIT_CARD"; }
private void validateCard() { /* Luhn algorithm, expiry check */ }
private void chargeGateway(double amount) { /* Stripe / Braintree API call */ }
}
// CORRECT: PayPal — separate class, does not affect CreditCard at all
public class PayPalPayment implements PaymentStrategy {
private final String payPalEmail;
public PayPalPayment(String payPalEmail) {
this.payPalEmail = payPalEmail;
}
@Override
public void processPayment(double amount) {
System.out.printf("Redirecting to PayPal for $%.2f (account: %s)%n", amount, payPalEmail);
initiateOAuthFlow();
executePayment(amount);
}
@Override
public boolean supports(String paymentType) {
return "PAYPAL".equalsIgnoreCase(paymentType);
}
@Override
public String getPaymentType() { return "PAYPAL"; }
private void initiateOAuthFlow() { /* PayPal SDK */ }
private void executePayment(double amount) { /* PayPal REST API */ }
}
// CORRECT: Apple Pay — brand new, added without touching any existing code
public class ApplePayPayment implements PaymentStrategy {
private final String deviceToken;
public ApplePayPayment(String deviceToken) {
this.deviceToken = deviceToken;
}
@Override
public void processPayment(double amount) {
System.out.printf("Processing Apple Pay for $%.2f%n", amount);
validateDeviceToken();
authorizeWithPassKit(amount);
}
@Override
public boolean supports(String paymentType) {
return "APPLE_PAY".equalsIgnoreCase(paymentType);
}
@Override
public String getPaymentType() { return "APPLE_PAY"; }
private void validateDeviceToken() { /* PassKit validation */ }
private void authorizeWithPassKit(double amount) { /* Apple Pay API */ }
}java
// CORRECT: PaymentProcessor never needs modification regardless of how many payment methods exist
@Service
public class PaymentProcessor {
private final List<PaymentStrategy> strategies;
// Inject all available strategies — Spring collects them automatically
public PaymentProcessor(List<PaymentStrategy> strategies) {
this.strategies = strategies;
}
public void processPayment(String paymentType, double amount) {
PaymentStrategy strategy = strategies.stream()
.filter(s -> s.supports(paymentType))
.findFirst()
.orElseThrow(() -> new UnsupportedPaymentException(
"No handler registered for payment type: " + paymentType));
strategy.processPayment(amount);
}
}Adding a new payment method — say, Google Pay — only requires writing a new GooglePayPayment class and registering it as a Spring bean. PaymentProcessor is never touched.
Reporting System — Second Example
OCP applies equally to output formatting. A reporting system that generates HTML, PDF, and CSV reports should not need modification when a new format is requested.
java
// Stable abstraction for report formatting
public interface ReportFormatter {
String format(ReportData data);
String getContentType();
String getFileExtension();
}
// HTML implementation
@Component
public class HtmlReportFormatter implements ReportFormatter {
@Override
public String format(ReportData data) {
StringBuilder sb = new StringBuilder();
sb.append("<html><body>");
sb.append("<h1>").append(data.getTitle()).append("</h1>");
data.getRows().forEach(row ->
sb.append("<p>").append(row).append("</p>"));
sb.append("</body></html>");
return sb.toString();
}
@Override public String getContentType() { return "text/html"; }
@Override public String getFileExtension() { return ".html"; }
}
// CSV implementation
@Component
public class CsvReportFormatter implements ReportFormatter {
@Override
public String format(ReportData data) {
StringBuilder sb = new StringBuilder();
sb.append(data.getTitle()).append("\n");
data.getRows().forEach(row -> sb.append(row).append("\n"));
return sb.toString();
}
@Override public String getContentType() { return "text/csv"; }
@Override public String getFileExtension() { return ".csv"; }
}
// PDF implementation — new format, no existing code modified
@Component
public class PdfReportFormatter implements ReportFormatter {
@Override
public String format(ReportData data) {
// Returns base64-encoded PDF bytes as string for simplicity
return generatePdfAsBase64(data);
}
@Override public String getContentType() { return "application/pdf"; }
@Override public String getFileExtension() { return ".pdf"; }
private String generatePdfAsBase64(ReportData data) { /* iText / Apache PDFBox */ return ""; }
}
// Generator is closed for modification regardless of new formats
@Service
public class ReportGenerator {
private final Map<String, ReportFormatter> formatters;
public ReportGenerator(List<ReportFormatter> formatterList) {
this.formatters = formatterList.stream()
.collect(Collectors.toMap(ReportFormatter::getFileExtension, f -> f));
}
public ReportOutput generate(ReportData data, String format) {
ReportFormatter formatter = formatters.getOrDefault(format,
formatters.get(".html")); // default to HTML
return new ReportOutput(
formatter.format(data),
formatter.getContentType(),
data.getTitle() + formatter.getFileExtension()
);
}
}Diagrams
Strategy Pattern Class Diagram
Extension Without Modification
Anti-Pattern vs OCP-Compliant Comparison
Payment Processing Flow
Plugin Architecture — Reporting System
Best Practices
- Use interfaces and abstract classes as extension points — The interface is the contract. New behavior comes from new implementations of existing contracts, not from modifying the contract or its existing implementations.
- Favor composition over inheritance — Inheritance-based extension often leads to fragile hierarchies. Composing behaviors via injected strategies (as shown above) keeps extension points explicit and manageable.
- Think in terms of behaviors, not classes — Ask yourself "what behaviors will need to vary in the future?" Extract those as interfaces first. The Strategy, Decorator, and Observer patterns all embody OCP.
- Identify the axis of variation — Not every class needs to be OCP-compliant for every possible change. Identify which dimensions of your system genuinely vary (payment methods, output formats, logging targets) and apply OCP there.
- Avoid premature abstraction — OCP requires knowing what will change. Apply it when you see a second or third concrete variant emerging (Rule of Three). Abstracting too early for changes that never come is wasted complexity.
- Leverage the DI container — Spring's ability to inject
List<PaymentStrategy>automatically collects all implementations. New classes are discovered and wired without any configuration change to the consuming class. - Write tests per implementation — With OCP in place, each
PaymentStrategyimplementation has its own unit test. Adding Google Pay adds a new test class but does not invalidate any existing test.
Related Concepts
Other SOLID Principles:
- SRP — Single Responsibility Principle: Separate classes are easier to extend independently, and SRP ensures each has only one reason to change.
- LSP — Liskov Substitution Principle: OCP extensions must satisfy LSP — new implementations must be substitutable for the interface.
- ISP — Interface Segregation Principle: Keep extension point interfaces narrow so implementing a new strategy never forces unnecessary methods.
- DIP — Dependency Inversion Principle: OCP relies on DIP —
PaymentProcessordepends on thePaymentStrategyabstraction, not on concrete payment classes.
Design Patterns:
- Strategy Pattern: the primary OCP mechanism shown here.
- Template Method Pattern: an inheritance-based alternative for fixing an algorithm's skeleton while allowing step overrides.
- Decorator Pattern: extends behavior by wrapping an existing implementation without modifying it.
- REST API Design: applying OCP to versioned API design.