SOLID Design Principles In Salesforce Master Class

The Open/Closed Principle in Apex (SOLID Ep. 3)

5 min read

Subscribe on YouTube

Open For Extension, Closed For Modification

The Open/Closed Principle says:

“Software entities should be open for extension, but closed for modification.”

Which sounds like a contradiction the first time you read it. How do you change what something does without changing it?

Plain English version: you should be able to add new behaviour by writing new code, not by editing code that already works.

Why does that matter? Because code that works is valuable. Every time you open a working class to add another branch, you’re risking something that was fine yesterday. Its tests have passed a hundred times. Its bugs have been found and fixed. Adding a new file risks nothing. Editing an old one risks everything it already does.


The Growing If Statement

Here’s how this violation always starts. Innocently.

Apex
public class DiscountCalculator
{
    public Decimal calculateDiscount(Account acct, Decimal amount)
    {
        if (acct.Type == 'Customer - Direct')
        {
            return amount * 0.10;
        }
        else if (acct.Type == 'Customer - Channel')
        {
            return amount * 0.15;
        }
        return 0;
    }
}

Perfectly fine. Two customer types, two discounts.

Then sales add a Partner tier. So you open the class and add a branch. Then a Reseller tier. Another branch. Then someone wants Partners in Europe to get a different rate than Partners in the US, so now there’s a nested if. Then there’s a promotional rate that only applies in Q4.

Eighteen months later this method is 200 lines, four people have edited it, nobody fully understands it, and every change requires regression testing every discount type because they all live in the same method.

You’ve seen this class. It might be in your org right now.


Fixing It With An Interface

Define a contract, then let each discount type be its own small class:

Apex
public interface DiscountStrategy
{
    Decimal calculate(Decimal amount);
}

public class DirectCustomerDiscount implements DiscountStrategy
{
    public Decimal calculate(Decimal amount)
    {
        return amount * 0.10;
    }
}

public class ChannelCustomerDiscount implements DiscountStrategy
{
    public Decimal calculate(Decimal amount)
    {
        return amount * 0.15;
    }
}

An interface is just a contract. It says “anything implementing me must have a calculate method that takes a Decimal and returns a Decimal.” No implementation, only the promise.

Now the calculator picks a strategy and gets out of the way:

Apex
public class DiscountCalculator
{
    private static final Map<String, DiscountStrategy> STRATEGIES =
        new Map<String, DiscountStrategy>{
            'Customer - Direct'  => new DirectCustomerDiscount(),
            'Customer - Channel' => new ChannelCustomerDiscount()
        };

    public Decimal calculateDiscount(Account acct, Decimal amount)
    {
        DiscountStrategy strategy = STRATEGIES.get(acct.Type);
        return strategy == null ? 0 : strategy.calculate(amount);
    }
}

Adding a Partner tier is now a new file plus one line in the map. You never touch the logic that calculates the existing discounts. Their tests still pass because their code hasn’t changed. It literally cannot have broken.

That’s Open/Closed. Open to extension (add a class), closed to modification (don’t edit working ones).

This particular shape is called the Strategy pattern, and it’s one of the most useful things you can have in your toolkit. There’s more on patterns like it in my Design Patterns series.


The Very Salesforce Version: Custom Metadata

Here’s where this gets genuinely nice on our platform. Those discount rates are still hard coded in Apex, which means changing 10% to 12% needs a deployment.

Put them in Custom Metadata instead:

Apex
public class DiscountCalculator
{
    public Decimal calculateDiscount(Account acct, Decimal amount)
    {
        List<Discount_Rule__mdt> rules = [SELECT Rate__c
                                          FROM Discount_Rule__mdt
                                          WHERE Account_Type__c = :acct.Type
                                          LIMIT 1];

        return rules.isEmpty() ? 0 : amount * rules[0].Rate__c;
    }
}

Now adding a whole new discount tier requires no code at all. An admin adds a metadata record. That’s about as open for extension as it gets.

Which route you take depends on what varies. If it’s just a number, Custom Metadata wins every time. If each type needs genuinely different logic, use the interface approach. And you can combine them, using metadata to decide which Apex class to instantiate. I did exactly that in my post on configurable LWCs.


In JavaScript

JavaScript
// BAD: every new type means editing this function
export function calculateDiscount(accountType, amount) {
    if (accountType === 'Customer - Direct')  { return amount * 0.10; }
    if (accountType === 'Customer - Channel') { return amount * 0.15; }
    return 0;
}

// GOOD: adding a type is adding a key
const DISCOUNT_STRATEGIES = {
    'Customer - Direct':  (amount) => amount * 0.10,
    'Customer - Channel': (amount) => amount * 0.15
};

export function calculateDiscount(accountType, amount) {
    const strategy = DISCOUNT_STRATEGIES[accountType];
    return strategy ? strategy(amount) : 0;
}

JavaScript makes this even tidier since functions are values you can stick straight in an object.


When Not To Bother

Be honest with yourself here, because this principle is very easy to over-apply.

If you have two discount types and there is no realistic prospect of a third, the if statement is fine. Building an interface, two implementations and a strategy map for something that will never change is not good design, it’s five files where one would do.

The signal to refactor is when you find yourself editing the same method for the third time to add another case. Once is fine. Twice is a coincidence. Three times means this thing varies, and you should stop fighting it and design for it.

Uncle Bob’s own advice was to apply Open/Closed to the parts of your system that you have observed changing. Not the parts you imagine might. Speculative flexibility is just complexity you haven’t been punished for yet.


What’s Next

Open/Closed: add behaviour with new code, don’t edit code that already works. Interfaces and Custom Metadata are your two main tools on this platform.

In the next episode we tackle the Liskov Substitution Principle, which has the scariest name and is really just “don’t write subclasses that lie.”

See you next time!


Get Coding With The Force Merch!!

We now have a redbubble store setup so you can buy cool Coding With The Force merchandise! Please check it out! Every purchase goes to supporting the blog and YouTube channel.

Get Shirts Here!
Get Cups, Artwork, Coffee Cups, Bags, Masks and more here!


Check Out More Coding With The Force Stuff!

If you liked this post make sure to follow us on all our social media outlets to stay as up to date as possible with everything!

Youtube
Patreon
Github
Facebook
Twitter
Instagram


Salesforce Development Books I Recommend

Advanced Apex Programming
Salesforce Lightning Platform Enterprise Architecture
Mastering Salesforce DevOps

Good Non-SF Specific Development Books:

Clean Code
Clean Architecture