Skip to content
Skip to content
SalesforceFox

SalesforceFox

Easy way Learning & Exploring Salesforce !

  • Home
  • All Blogs
  • How To
  • Tips & Tricks
  • Salesforce Quiz
  • Resources
  • About
Mastering SOLID principles in Apex

Mastering SOLID Principles in Apex: Write Cleaner, Scalable and Maintainable Code for Salesforce

Piyush LakhaniMarch 11, 2025March 21, 2025

SOLID principles are five best practices to better design software system to make it more flexible, maintainable and understandable. Mastering these SOLID principles in Apex is essential for writing cleaner, scalable, maintainable and flexible code in Salesforce.

In this blog, we will understand these SOLID principles through application of real-world apex code within Salesforce.

Table of Contents
[Open][Close]
  • What SOLID stands for
  • Single Responsibility Principle (SRP)
  • Open/Closed Principle (OCP)
  • Liskov’s Substitution Principle (LSP)
  • Dependency Inversion Principle (DIP)
  • Summary

What SOLID stands for

Principle
Explanation
1Single Responsibility PrincipleA class should have only one reason to change which means a class should have only one responsibility or purpose in software system.
2Open/Closed PrincipleSoftware entities (class, function) should be open for extension, but should be closed for any modification.
3Liskov’s Substitution PrincipleThe child classes must be substitutable for their parent classes which means child of its parent class should be usable in place of its parent without any issue.
4Interface Segregation PrincipleThe clients/classes should not be forced to implement an interface that they are not relevant/useful to them
5Dependency Inversion PrincipleHigh-level modules should not depend on low-level modules. Both should depend on abstractions which means classes should depend upon interfaces or abstract classes instead of concrete classes and functions.

Now, Let’s understand SOLID Principles in Apex one by one with real-world salesforce examples.

Single Responsibility Principle (SRP)

This principle asserts that a class or method can be changed for only one reason. It simply means they should be responsible for only one specific task rather than doing multiple task

Let’s look at AccountService Class and how we can improve it

public class AccountService {
  public void updateAccountAndSendEmail(Id accId, String newContactEmail) {
    // Update Contact details
    Contact conToUpdte = [SELECT Id, Email FROM Contact WHERE AccountId = :accId LIMIT 1];

    contactToUpdate.Email = newContactEmail;
    update contactToUpdate;

    // Sending an email
    Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
    mail.setToAddresses(new String[] { contactToUpdate.Email });
    mail.setSubject("Account Update Notification");
    mail.setPlainTextBody('The contact email for your account has been updated.');
    Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
  }
}

What’s wrong with above Apex class :

  • This AccountService class does two task first Database operation (updating account’s contact email) and second Email notification.
  • Therefore, this class violates the principle which states class should do only one task. On top of that this code is hard to maintain and test if we change/update it in future.

Let’s refactor this code to separate each task into its own class as follow this principle.

// Class responsible for updating contact details
public class ContactService {
    public void updateContactEmail(Id accId, String newContactEmail) {

      Contact conToUpdte = [SELECT Id, Email FROM Contact WHERE AccountId = :accId LIMIT 1];
      conToUpdate.Email = newContactEmail;
      update conToUpdate;
    }
}

// Class responsible for sending email notifications
public class EmailService {
    public void sendAccountUpdateEmail(String contactEmail) {
     
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        mail.setToAddresses(new String[] { contactEmail });
        mail.setSubject('Account Update Notification');
        mail.setPlainTextBody('The contact email for your account has been updated.');
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
    }
}

// Class responsible for orchestrating both operations (update + send email)
public class AccountService {
    public void updateAccountAndSendEmail(Id accId, String newContactEmail) {
        // Update the contact's email
        ContactService contactService = new ContactService();
        contactService.updateContactEmail(accId, newContactEmail);
        
        // Send the email notification
        EmailService emailService = new EmailService();
        emailService.sendAccountUpdateEmail(newContactEmail);
    }
}

Above refactored code separates each task into its own class which makes code more cleaner and maintainable.

Open/Closed Principle (OCP)

This principle asserts that classes should be open for extension (adding new functionality) but closed for modification (changing existing code).

it simply means we should be able to add new feature/functionality into class without touching existing code. because whenever we try to modify existing code, we are at risk of creating potential bugs.

Problem Scenario: Let’s say we have discount system for opportunities where we provide discounts to opportunities based on its associated Account Type.

public class OpportunityDiscountCalculator {
    public Decimal calculateDiscount(Opportunity opp) {
        Decimal discount = 0;

        // Apply discount based on Account Type
        if (opp.Account.Type == 'VIP') {
            discount = opp.Amount * 0.2; // 20% discount for VIP accounts
        } else if (opp.Account.Type == 'Standard') {
            discount = opp.Amount * 0.05; // 5% discount for Standard accounts
        } else if (opp.Account.Type == 'Premium') {
            discount = opp.Amount * 0.15; // 15% discount for Premium accounts
        }

        return discount;
    }
}

What’s wrong with above Apex class :

Every time we add new account type, we have to modify this class. This violates OCP because OpportunityDiscountCalculator class is open for modification. this makes code poor due to,

  • Hard to maintain: The code becomes harder to maintain as more account type added
  • Poor scalability: Adding more Account type makes if-else logic grows which make code complex and poor to scale it.
  • Testing complexity: we have tightly coupled discount logic with account type, making it harder to isolate and test.

Let’s refactor this code to follow OCP

// Discount strategy interface
public interface DiscountStrategy {
    Decimal calculateDiscount(Opportunity opp);
}

// Discount strategy for VIP Account Type
public class VIPAccountDiscountStrategy implements DiscountStrategy {
    public Decimal calculateDiscount(Opportunity opp) {
        return opp.Amount * 0.2; // 20% discount for VIP accounts
    }
}
// Discount strategy for Standard Account Type
public class StandardAccountDiscountStrategy implements DiscountStrategy {
    public Decimal calculateDiscount(Opportunity opp) {
        return opp.Amount * 0.05; // 5% discount for Standard accounts
    }
}
// Discount strategy for Premium Account Type
public class PremiumAccountDiscountStrategy implements DiscountStrategy {
    public Decimal calculateDiscount(Opportunity opp) {
        return opp.Amount * 0.15; // 15% discount for Premium accounts
    }
}
// Discount strategy factory to select the correct strategy
public class DiscountStrategyFactory {

  // Map account types to their corresponding strategies
  private static Map<String, DiscountStrategy> accountTypeToStrategy = new Map<String, DiscountStrategy>{
    'VIP' => new VIPAccountDiscountStrategy(),
    'Standard' => new StandardAccountDiscountStrategy(),
    'Premium' => new PremiumAccountDiscountStrategy()
    };

    // Method to get the appropriate strategy based on account type
    public static DiscountStrategy getStrategy(String accountType) {
        if (accountTypeToStrategy.containsKey(accountType)) {
            return accountTypeToStrategy.get(accountType);
        } else {
            // Fallback strategy if account type is unknown
            return new StandardAccountDiscountStrategy(); // Default to Standard discount
        }
    }
}
// Main Discount Calculator class
public class OpportunityDiscountCalculator {

    // Calculate discount using the given strategy
    public Decimal calculateDiscount(Opportunity opp) {
        // Get the discount strategy using the factory based on Account Type
        DiscountStrategy strategy = DiscountStrategyFactory.getStrategy(opp.Account.Type);
        return strategy.calculateDiscount(opp);
    }
}

Explanation of this Refactored code:

  • The DiscountStrategy interface defines the method calculateDiscount that must be implemented by any discount strategy class. It provides a common contract for all discount types.
  • We create a separate class for each Account Type like VIPAccountDiscountStrategy
  • The DiscountStrategyFactory class is responsible for mapping account types to their respective discount strategies using a map and it has method getStrategy to retrieves the appropriate strategy based on the Account Type
  • The OpportunityDiscountCalculator class now simply calls the DiscountStrategyFactory.getStrategy method to get the appropriate discount strategy based on the Account Type

Now this refactored code is not tightly coupled, therefore it is open for any new functionality and closed for any modification, if you want new account type, no need to modify existing code, just introduce new DiscountStrategy class and add its name into map in DiscountStrategyFactory class

Liskov’s Substitution Principle (LSP)

This principle asserts that that child class should be able to substitute its super class without any unexpected issue in code.

It simply means if you a class that inherits from another another class then you should be able to replace the parent class with the child class without causing any issues.

It ensures that inheritance of classes are valid and maintainable all time.

public class Product {
    public virtual Decimal calculatePrice() {
        return 100.00; // Base price for a product
    }
}

public class DiscountedProduct extends Product {
    // Violation: This method changes the expected behavior by returning a negative price
    public override Decimal calculatePrice() {
        return -30.00; // This is an unexpected behavior
    }
}

What’s wrong with above Apex class :

  • Violation Of LSP: The DiscountedProduct class overrides the calculatePrice method and returns a negative value as the price. This changes the expected behavior of the base class, which violates LSP because substituting Product with DiscountedProduct leads to an unexpected and incorrect result (a negative price).

Let’s Refactor code to avoid unexpected result when we use class DiscountedProduct instead of Product

public class Product {
    public virtual Decimal calculatePrice() {
        return 100.00; // Base price for a product
    }
}
public class DiscountedProduct extends Product {
    // Correct behavior: This method applies a discount but returns a positive price
    public override Decimal calculatePrice() {
        Decimal basePrice = super.calculatePrice(); // Get the base price
        return basePrice * 0.7; // Apply a 30% discount, keeping price positive
    }
}

Now this code is LSP compliants because the behavior of the calculatePrice method is consistent across both the parent class and the subclass. You can substitute a Product object with a DiscountedProduct object without changing the correctness of the program.

Interface Segregation Principle (ISP)

This principle asserts that classes should not be forced to implement an interface that they are not relevant/useful to them. it simply means that No class should be forced to depend on methods it does not use.

Let’s take example we have two classes StandardOppsServices and HighValueOppsServices that implement interface called OpportunityActions.

public interface OpportunityActions {
    void createOpportunity(Opportunity opp);
    void updateOpportunity(Opportunity opp);
    void sendFollowupEmail(Opportunity opp);
    void assignToDedicatedTeam(Opportunity opp);
}

public class StandardOpportunityService implements OpportunityActions {
  
    public void createOpportunity(Opportunity opp) {
        // Logic to create a standard opportunity
    }
    public void updateOpportunity(Opportunity opp) {
        // Logic to update a standard opportunity
    }
    public void sendFollowupEmail(Opportunity opp) {
        // Logic to send follow up email to this Opp
    }
    public void assignToDedicatedTeam(Opportunity opp) {
        // Logic to assign this Opp to Dedicated Team
    }
}

public class HighValueOpportunityService implements OpportunityActions {
    
  public void createOpportunity(Opportunity opp) {
        // Logic to create a standard opportunity
    }
    public void updateOpportunity(Opportunity opp) {
        // Logic to update a standard opportunity
    }
    public void sendFollowupEmail(Opportunity opp) {
        // Logic to send follow up email to this Opp
    }
    public void assignToDedicatedTeam(Opportunity opp) {
        // Logic to assign this Opp to Dedicated Team
    }
}

What’s wrong with above Apex class :

As you have seen, here we have dedicated Opportunity classes , one for standard Opps and another for High value Opps. Both are implementing interface called OpportunityActions, at first sight, it looks great design with implementation of interface.

However, If you noticed, Interface’s methods sendFollowupEmail and assignToDedicatedTeam. it seems these 2 methods are only for High Value Opportunities. However, since they are in one common interface, class StandardOpportunityService is forced to use these methods, adding unnecessary complexity and redundancy in code which is also violation of this principle.

Let’s refactor this code to we do not need to force class to use redundant methods.

// Interface for basic opportunity operations
public interface BasicOpportunityActions {
    void createOpportunity(Opportunity opp);
    void updateOpportunity(Opportunity opp);
}
// Interface for only relevant for High-Value Opportunities)
public interface HighValueOppsActions {
    void sendFollowupEmail(Opportunity opp);
    void assignToDedicatedTeam(Opportunity opp);
}

// Standard Opportunity Service - Implements only necessary operations
public class StandardOpportunityService implements BasicOpportunityActions {
    
  public void createOpportunity(Opportunity opp) {
        // Logic to create a standard opportunity
    }
    public void updateOpportunity(Opportunity opp) {
        // Logic to update a standard opportunity
    }
}

// High-Value Opportunity Service - Basic Opp + High Value Opp Interface
public class HighValueOpportunityService implements BasicOpportunityActions, HighValueOppsActions {
    
  public void createOpportunity(Opportunity opp) {
        // Logic to create a high-value opportunity
    }
    public void updateOpportunity(Opportunity opp) {
        // Logic to update a high-value opportunity
    }
    public void sendFollowupEmail(Opportunity opp) {
        // Logic to send follow-up email to High Value Opp
    }
    public void assignToDedicatedTeam(Opportunity opp) {
        // Logic to assign High Value Opp to Dedicated Team
    }
}

Now this refactored code contains dedicated Interface for high value Opportunities, so you are not forcing Standard Opps class to implement unnecessary method making this code cleaner, easy to maintain and in compliant with this principle.

Dependency Inversion Principle (DIP)

This principle asserts that the classes should depend upon interfaces or abstract classes instead of concrete classes and functions and Abstractions should not depend on details rather details should depend on abstractions.

Let’s take example of LeadService class here:

public class LeadService {
    public void convertLead(Lead lead) {
        // Direct dependency on concrete Lead to Contact and Account conversion logic
        Contact contact = new Contact(FirstName = lead.FirstName, LastName = lead.LastName, Email = lead.Email);
        insert contact;

        Account account = new Account(Name = lead.Company);
        insert account;

        // Link the Contact to Account (tightly coupled logic)
        lead.Status = 'Converted';
        lead.ContactId = contact.Id;
        lead.AccountId = account.Id;
        update lead;
    }
}

What’s wrong with above Apex class :

  • The class directly depends on concrete logic for creating and inserting Contacts and Accounts based on the Lead instead of abstract class which violates DIP principle.
  • It’s difficult to replace or extend the logic (e.g., using an external service to convert Leads), and testing the convertLead method is harder because it’s coupled to specific implementations.

Let’s refactor this code to apply DIP principle so that this class can depend on abstractions instead of tightly coupled logics.

// Define an abstraction for converting Leads
public interface ILeadConverter {
    void convertLead(Lead lead);
}

// Concrete implementation for converting Leads to Contact and Account
public class LeadToContactAccountConverter implements ILeadConverter {
  public void convertLead(Lead lead) {
    // Convert Lead to Contact
    Contact contact = new Contact(FirstName = lead.FirstName, LastName = lead.LastName, Email = lead.Email);
    insert contact;

    // Convert Lead to Account
    Account account = new Account(Name = lead.Company);
    insert account;

    // Link the Contact to Account
    lead.Status = 'Converted';
    lead.ContactId = contact.Id;
    lead.AccountId = account.Id;
    update lead;
  }
}

// High-level class that depends on abstraction (ILeadConverter)
public class LeadService {
    private ILeadConverter leadConverter;

    // Constructor Injection: Inject the conversion logic
    public LeadService(ILeadConverter converter) {
        this.leadConverter = converter;
    }

    public void convertLead(Lead lead) {
        // Delegate the conversion task to the injected converter
        leadConverter.convertLead(lead);
    }
}

Explanation of this refactored code:

  • The LeadService class depends on the abstraction ILeadConverter instead of the concrete LeadToContactAccountConverter. This makes the LeadService class flexible and open for extension and apparently DIP-compliant.
  • The details (conversion logic) depend on abstractions (ILeadConverter), and the high-level class (LeadService) depends on abstractions, making the system more modular and extensible.

Summary

The SOLID principles provide a foundation for creating code that is easier to maintain, extend, and understand. By following SOLID principles in Apex along with Official Apex Best Practices, you can significantly improve your Salesforce code’s scalability, maintainability, and cleanliness.

In summary:

  • SRP: A class should have only one responsibility.
  • OCP: A class should be open for extension but closed for modification.
  • LSP: Subtypes classes must be substitutable for their base class types.
  • ISP: Clients should not be forced to depend on interfaces they do not use.
  • DIP: Classes should depend on interfaces or abstract classes instead of concrete classes/functions


Share this Now :

Related Posts:

  • Create Dynamic Job Scheduling Framework in Salesforce
    Create a Dynamic Job Scheduling Framework in…
  • Best way to avoid Recursion in Apex Triggers
    Best way to avoid Recursion in Apex Triggers
  • How To Zip & UnZip Files In Salesforce
    How to Zip & Unzip files Natively in Salesforce: Use…
  • 12 Must-Know Javascript Concepts for LWC: A guide for Salesforce Developers
    12 Must-know Javascript Concepts for LWC : A guide…
Best Practices, Development

Post navigation

Previous: How to Find Duplicate Records in Salesforce
Next: Send Automatic Reminder Emails to Inactive Users in Salesforce

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Get Weekly Salesforce Quiz

Categories

  • Admin (8)
  • Apex (6)
  • Automation (7)
  • Best Practices (6)
  • Flow (5)
  • LWC (4)
  • Security (1)
  • SOQL (1)
  • Trigger (1)
  • Uncategorized (1)

Recent Posts

  • User Access Policy in Salesforce : The Smartest way to Automate User Management Jobs
  • Display Apex-Defined Collection Data directly into Screen Flow Data Table
  • How to Secure Your Salesforce Org: First Steps Against Cyber Attacks and Social Engineering
  • Make Salesforce Screen flows more Interactive with Toast Messages
  • How to Zip & Unzip files Natively in Salesforce: Use This Ready-to-Use ZIP Utility

Tags

Admin Apex Aura Component Automation Best Practices Development Email Alerts Flow Flow Approval Process How To Lightning Experience LWC Reports Scheduled Jobs Security SOQL Tips Trigger User Management

Archives

  • November 2025 (1)
  • October 2025 (1)
  • August 2025 (1)
  • July 2025 (1)
  • June 2025 (2)
  • May 2025 (1)
  • April 2025 (2)
  • March 2025 (2)
  • January 2025 (2)
  • December 2024 (1)
  • October 2024 (1)
  • September 2024 (1)
  • August 2024 (1)
  • June 2024 (1)
  • April 2024 (1)
  • March 2024 (1)
  • October 2023 (1)
  • Linkedin
  • Youtube
  • Privacy Policy
  • Terms of Use
  • Contact
Theme: BlockWP by Candid Themes.
© 2023-2026 | SalesforceFox.com