SOLID Design Principles In Salesforce Master Class

The Dependency Inversion Principle in Apex (SOLID Ep. 6)

5 min read

Subscribe on YouTube

The One With The Biggest Payoff

Last principle, and in my opinion the most practically valuable one on this platform. Two parts:

“High level modules should not depend on low level modules. Both should depend on abstractions.”
“Abstractions should not depend on details. Details should depend on abstractions.”

Plain English: your business logic shouldn’t be welded to specific implementations. Depend on interfaces, and have the specifics handed to you.

Why care? Because this is the principle that makes your Apex genuinely testable, and on a platform that requires 75% code coverage to deploy, that is not a small thing.


The Untestable Class

Apex
public class OpportunityProcessor
{
    public void processOpportunity(Id oppId)
    {
        //Reaches straight into the database
        Opportunity opp = [SELECT Id, Amount, StageName FROM Opportunity WHERE Id = :oppId];

        //Builds its own emailer
        EmailService emailer = new EmailService();

        //Calls an external system directly
        ExternalPricingApi api = new ExternalPricingApi();
        Decimal price = api.getPrice(opp.Id);

        opp.Amount = price;
        update opp;

        emailer.notifyOwner(opp);
    }
}

Perfectly readable. Absolutely miserable to test.

To write a unit test for that pricing logic you must: create a real Opportunity in the database, arrange for a real email to be sent, and make a real HTTP callout to an external pricing API. In a test. Which will fail if that API is down, or slow, or changes its response.

The problem is that OpportunityProcessor is constructing its own dependencies. Every new in there is a hard wire to a specific implementation you cannot swap out.


Inverting The Dependencies

Define what you need as interfaces:

Apex
public interface IOpportunitySelector
{
    Opportunity getById(Id oppId);
}

public interface IEmailService
{
    void notifyOwner(Opportunity opp);
}

public interface IPricingService
{
    Decimal getPrice(Id oppId);
}

Then take them in through the constructor rather than building them:

Apex
public class OpportunityProcessor
{
    private IOpportunitySelector selector;
    private IEmailService emailer;
    private IPricingService pricing;

    public OpportunityProcessor(IOpportunitySelector selector,
                                IEmailService emailer,
                                IPricingService pricing)
    {
        this.selector = selector;
        this.emailer  = emailer;
        this.pricing  = pricing;
    }

    public void processOpportunity(Id oppId)
    {
        Opportunity opp = selector.getById(oppId);
        opp.Amount = pricing.getPrice(opp.Id);
        update opp;
        emailer.notifyOwner(opp);
    }
}

Notice the class no longer knows or cares how opportunities get fetched or emails get sent. It just knows something will do it. That’s the inversion: instead of the high level logic reaching down to grab the details, the details get handed up to it.

This is called dependency injection, and constructor injection like this is the most common flavour.


And Now The Test Is Trivial

Apex
@IsTest
private class OpportunityProcessorTest
{
    //Fake implementations that do nothing but answer
    private class MockSelector implements IOpportunitySelector
    {
        public Opportunity getById(Id oppId)
        {
            return new Opportunity(Name = 'Test', StageName = 'Prospecting');
        }
    }

    private class MockPricing implements IPricingService
    {
        public Decimal getPrice(Id oppId) { return 500; }
    }

    private class MockEmailer implements IEmailService
    {
        public Boolean wasCalled = false;
        public void notifyOwner(Opportunity opp) { wasCalled = true; }
    }

    @IsTest
    static void itSetsThePriceAndNotifies()
    {
        MockEmailer emailer = new MockEmailer();

        OpportunityProcessor processor = new OpportunityProcessor(
            new MockSelector(), emailer, new MockPricing()
        );

        Test.startTest();
        processor.processOpportunity(null);
        Test.stopTest();

        System.assertEquals(true, emailer.wasCalled, 'Owner should be notified');
    }
}

No database. No callout. No email. The test runs in milliseconds and tests exactly one thing: does the processor do the right work in the right order.

That is the entire payoff of this principle, and once you’ve felt it you won’t want to go back.


“But Now My Constructor Is Horrible”

Yes. Nobody wants to write this at every call site:

Code
new OpportunityProcessor(new OpportunitySelector(),
                         new EmailService(),
                         new PricingService()).processOpportunity(oppId);

Two common fixes.

1. A convenience constructor with the real defaults:

Apex
public OpportunityProcessor()
{
    this(new OpportunitySelector(), new EmailService(), new PricingService());
}

Production code calls new OpportunityProcessor(), tests use the full constructor. Simple, and good enough for most orgs.

2. A factory, which is what the Apex Common Library’s fflib_Application class does. You register your implementations in one place and ask the factory for them. In tests you swap the registration for a mock. It’s more setup, but in a large codebase it’s genuinely worth it, and it’s covered properly in the Separation of Concerns series.


The Static Problem, Revisited

Remember me warning you off static methods back in the Apex Master Class? This is why.

Code
//Cannot be mocked. Ever. You get the real one.
EmailService.sendEmail(opp);

//Can be swapped for anything.
emailer.notifyOwner(opp);

A static call is a hard dependency baked into your code at compile time. There’s no seam, no way to slide a fake in. That’s fine for genuine utilities that just transform inputs, and a real problem for anything touching the database, sending email, or calling out.

Rule of thumb: if it has side effects, make it an instance method behind an interface.


Where To Stop

You do not need an interface for everything. A class with no side effects that just does arithmetic doesn’t need injecting, it’s already perfectly testable.

The things genuinely worth putting behind an interface:

1. Database access (selectors)
2. HTTP callouts
3. Email and messaging
4. Anything time based, so you can test with a fixed date
5. Anything genuinely slow

That’s the list. Those are the things that make tests slow, flaky or impossible. Everything else can usually stay concrete.


That’s All Five

You made it through the whole series. Quick recap:

S – One reason to change. One actor per class.
O – Add behaviour with new code, don’t edit working code.
I – Small focused interfaces, no empty method bodies.
L – Subclasses must work anywhere the parent does.
D – Depend on abstractions, inject your dependencies.

Notice how much they overlap. Following Dependency Inversion pushes you towards Interface Segregation. Interfaces are how you get Open/Closed. Small single-responsibility classes are easier to substitute cleanly. They’re five angles on the same underlying idea: build things you can change safely.

And remember what I said in episode 1. These are principles, not laws. You’ll over-apply them at first, everybody does. The judgement comes with reps.

Where to go next: Separation of Concerns and the Apex Common Library takes these ideas and turns them into a full architecture for a Salesforce codebase. It’s the natural sequel to this series.

Thanks for sticking with it. Go build something maintainable!


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