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.
What SOLID stands for
| 1 | Single Responsibility Principle | A class should have only one reason to change which means a class should have only one responsibility or purpose in software system. |
| 2 | Open/Closed Principle | Software entities (class, function) should be open for extension, but should be closed for any modification. |
| 3 | Liskov’s Substitution Principle | The 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. |
| 4 | Interface Segregation Principle | The clients/classes should not be forced to implement an interface that they are not relevant/useful to them |
| 5 | Dependency Inversion Principle | High-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
AccountServiceclass 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
DiscountStrategyinterface defines the methodcalculateDiscountthat 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
DiscountStrategyFactoryclass is responsible for mapping account types to their respective discount strategies using a map and it has methodgetStrategyto retrieves the appropriate strategy based on the Account Type - The
OpportunityDiscountCalculatorclass now simply calls theDiscountStrategyFactory.getStrategymethod 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
DiscountedProductclass overrides thecalculatePricemethod and returns a negative value as the price. This changes the expected behavior of the base class, which violates LSP because substitutingProductwithDiscountedProductleads 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
convertLeadmethod 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
LeadServiceclass depends on the abstractionILeadConverterinstead of the concreteLeadToContactAccountConverter. This makes theLeadServiceclass 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




