Open/Closed Principle: Scaling Salesforce Without Breaking It

Learn how to extend your Apex logic without rewriting your existing classes.


Open/Closed Principle: Scaling Salesforce Without Breaking It

If the Single Responsibility Principle taught us to keep classes small, the Open/Closed Principle (OCP) teaches us how to make them flexible.

The rule is: Software entities (classes, modules, functions) should be open for extension, but closed for modification. In the other words you should be able to add new features to your code without touching the code that already works. This is huge in Salesforce, where one small change in a shared utility can trigger a "ripple effect" of failing unit tests across your entire org.


Why Should You Care?

  • Zero Regression: Since you aren't changing existing code to add new features, you don't risk breaking old features.
  • Cleaner PRs: Code reviews become a breeze when you're only looking at a new class rather than a 500-line edit to a legacy one.
  • Easier Deployment: You can deploy new logic modules independently.

The "Switch Statement" Trap (The Wrong Way)

Imagine a discount engine. Every time the business adds a new discount type, you have to open the DiscountService and add another if/else or switch block.

public class DiscountService {
    public Decimal calculate(Decimal price, String type) {
        if (type == 'VIP') {
            return price * 0.80;
        } else if (type == 'FlashSale') {
            return price * 0.50;
        }
        // Every new discount type means we MUST edit this class.
        // This class is "Open for Modification"
        return price;
    }
}

The Problem: This class is "fragile." As the list of discounts grows, the class becomes a giant mess, and a typo in the VIP logic could break the FlashSale calculation.

The "Interface" Approach (The Right Way)

To follow OCP, we use Interfaces or Virtual Classes. We define a contract for what a "Discount" looks like, and then create separate classes for each specific rule.

1. The Contract

public interface IDiscountStrategy {
    Decimal applyDiscount(Decimal price);
}

2. The Implementation (Open for Extension)

Now, each discount lives in its own tiny, safe box.

public class VipDiscount implements IDiscountStrategy {
    public Decimal applyDiscount(Decimal price) { return price * 0.80; }
}
 
public class FlashSaleDiscount implements IDiscountStrategy {
    public Decimal applyDiscount(Decimal price) { return price * 0.50; }
}

3. The Service (Closed for Modification)

The main service doesn't care how many discounts exist. It just runs whatever it's given.

public class DiscountService {
    public Decimal calculate(Decimal price, IDiscountStrategy strategy) {
        return strategy.applyDiscount(price);
    }
}

Applying OCP to Salesforce Triggers

The most common place for OCP in Salesforce is Trigger Frameworks.

Instead of putting all your logic in a TriggerHandler class with 50 methods, you can use a "Metadata-driven" approach.

Create a TriggerAction interface.

Create separate classes for ValidateAddress, SyncToNetSuite, and SendWelcomeEmail.

Use Custom Metadata to tell your trigger which classes to run.

Result: When you need a new trigger action, you don't edit the Trigger or the Handler. You just create a new class and add a row in Custom Metadata. That is OCP in action.

Final Thoughts

The Open/Closed Principle might feel like more work upfront because it requires more planning. However, once your Salesforce org grows, it becomes the difference between a "Stable System" and a "House of Cards."