SOLID Design Principles In Salesforce Master Class

The Single Responsibility Principle in Apex (SOLID Ep. 2)

5 min read

Subscribe on YouTube

The Definition Everybody Gets Wrong

Google the Single Responsibility Principle and the first few results will tell you something like: “a class should only do one thing.”

That’s not the definition. It’s not even a good approximation, and it’s the reason so many people over-apply this principle and end up with forty classes that each contain one method.

Here’s the actual definition, from Uncle Bob himself:

“A class should have one, and only one, reason to change.”

Read that again, because the difference matters enormously. It’s not about how many things a class does. It’s about how many reasons exist for someone to come along and edit it.

Uncle Bob later sharpened it further: “a module should be responsible to one, and only one, actor.” Which is the version that finally made it click for me. Who is going to ask for this to change? If the answer is “several different people who want different things,” you’ve got a problem.


A Class With Too Many Bosses

Here’s a class that looks entirely reasonable and that you will find in basically every Salesforce org on Earth:

Apex
public class OpportunityService
{
    public void processOpportunities(Set<Id> oppIds)
    {
        List<Opportunity> opps = [SELECT Id, Amount, StageName, OwnerId
                                 FROM Opportunity WHERE Id IN :oppIds];

        for (Opportunity opp : opps)
        {
            //Commission logic
            Decimal commission = opp.Amount * 0.05;
            opp.Commission__c = commission;

            //Formatting for the UI
            opp.Display_Summary__c = opp.StageName + ' - $'
                + opp.Amount.setScale(2).format();
        }

        update opps;

        //Notification logic
        List<Messaging.SingleEmailMessage> emails = new List<Messaging.SingleEmailMessage>();
        for (Opportunity opp : opps)
        {
            Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
            mail.setTargetObjectId(opp.OwnerId);
            mail.setSubject('Your opportunity was processed');
            emails.add(mail);
        }
        Messaging.sendEmail(emails);
    }
}

It’s bulkified. It works. What’s the problem?

Count the people who might ask you to change it:

1. Finance wants the commission rate changed from 5% to 7%.
2. The UI team wants the summary formatted differently.
3. Sales ops wants the notification email reworded.
4. You want to change how the records are queried.

Four actors. Four reasons to change. One class.

So when Finance asks for a commission tweak, you’re editing the same file that sends the emails. If you get it wrong, you’ve broken emails for a change that had nothing to do with them. And every one of those four groups can create a merge conflict with the other three.

That’s what SRP is actually about. Not “this class does too much” in the abstract, but “too many unrelated people have a claim on this file.”


Splitting It Up Properly

Apex
//Finance owns this one
public class CommissionCalculator
{
    private static final Decimal COMMISSION_RATE = 0.05;

    public Decimal calculate(Opportunity opp)
    {
        return opp.Amount * COMMISSION_RATE;
    }
}

//The UI folks own this one
public class OpportunityFormatter
{
    public String buildSummary(Opportunity opp)
    {
        return opp.StageName + ' - $' + opp.Amount.setScale(2).format();
    }
}

//Sales ops own this one
public class OpportunityNotifier
{
    public void notifyOwners(List<Opportunity> opps)
    {
        List<Messaging.SingleEmailMessage> emails = new List<Messaging.SingleEmailMessage>();
        for (Opportunity opp : opps)
        {
            Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
            mail.setTargetObjectId(opp.OwnerId);
            mail.setSubject('Your opportunity was processed');
            emails.add(mail);
        }
        Messaging.sendEmail(emails);
    }
}

//And this one just coordinates
public class OpportunityService
{
    public void processOpportunities(Set<Id> oppIds)
    {
        List<Opportunity> opps = [SELECT Id, Amount, StageName, OwnerId
                                 FROM Opportunity WHERE Id IN :oppIds];

        CommissionCalculator calculator = new CommissionCalculator();
        OpportunityFormatter formatter  = new OpportunityFormatter();

        for (Opportunity opp : opps)
        {
            opp.Commission__c      = calculator.calculate(opp);
            opp.Display_Summary__c = formatter.buildSummary(opp);
        }

        update opps;

        new OpportunityNotifier().notifyOwners(opps);
    }
}

Now Finance’s change touches exactly one small file that does nothing else. The commission logic can be unit tested without sending a single email or building a summary string.

Notice that OpportunityService still does several things in the loose sense. It queries, it coordinates, it updates. But it has one reason to change: the process of handling opportunities changed. That’s SRP satisfied, and it’s why “one class one thing” is the wrong mental model.


The Same Thing In JavaScript

This isn’t an Apex thing, it’s a design thing. Here’s an LWC doing too much:

JavaScript
// BAD: the component fetches, formats, and validates
export default class OpportunityCard extends LightningElement {
    @wire(getOpportunities) opportunities;

    formatCurrency(amount) { ... }
    validateAmount(amount) { ... }
    calculateCommission(amount) { ... }
}

Pull the logic out into a module and the component goes back to doing its actual job, which is rendering:

JavaScript
// opportunityUtils.js
export function formatCurrency(amount) { ... }
export function validateAmount(amount) { ... }
export function calculateCommission(amount) { ... }

// opportunityCard.js
import { formatCurrency, calculateCommission } from 'c/opportunityUtils';

export default class OpportunityCard extends LightningElement {
    @wire(getOpportunities) opportunities;
}

Bonus: that utility module is now plain JavaScript you can test with Jest without rendering a component at all. Much faster tests, much simpler setup. I’ve got a full guide to LWC Jest testing if you want to go down that road.


How Do You Spot A Violation?

Some practical smells:

The word “and” in your class name. AccountValidatorAndEmailer is telling on itself.

You struggle to name it. If the only honest name is OpportunityHelper or AccountUtils, it’s probably a bin for unrelated things. Naming difficulty is nearly always a design signal.

Your test setup is enormous. If testing the commission calculation needs email deliverability configured, that’s SRP shouting at you.

Merge conflicts. If two developers working on unrelated features keep colliding in the same file, that file has too many owners.

You scroll to find things. Not a rule, but a 2,000 line class is rarely serving one actor.


Don’t Take It Too Far

I warned about this in episode 1 and I’ll say it again, because SRP is the one people overdo.

Splitting CommissionCalculator into CommissionRateProvider, CommissionMultiplier and CommissionRounder does not make your code better. It makes it a scavenger hunt.

The test is always “who asks for this to change?” If the answer is the same person for all three, they belong together. One actor, one class.


What’s Next

Single Responsibility: one reason to change, one actor per class. Not “one thing.”

In the next episode we cover the Open/Closed Principle, which is about adding new behaviour without opening up code that already works.

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