Skip to content

Explore "Gang of Four" (GoF) Design Patterns in Object Oriented Software explained in detail with Salesforce related examples to build high-quality, reusable, loosely-coupled and maintainable Software on Salesforce Platform

Notifications You must be signed in to change notification settings

khushal-ganani/design-patterns-salesforce

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Design Patterns

Design patterns are proven solutions to recurring problems in software design, which are high-quality, reusable, loosely-coupled and maintainable object-oriented software systems. They capture design experience in a usable form, providing a standard vocabulary for developers. Essentially, a design pattern names, abstracts, and identifies the key aspects of a common design structure that make it useful for creating a reusable object-oriented design. The well-known "Gang of Four" (GoF) patterns consist of 23 such patterns, grouped into three categories: Creational, Structural, and Behavioural.

Why Design Patterns?

Learning design patterns is essential for several reasons:

  • Improved Code Quality: Using design patterns often results in more maintainable, flexible, and extensible code. They help promote loose coupling and encapsulate design decisions, making codebases easier to understand, modify, and extend. They embody best practices and principles of software design. For Example, most of the Salesforce Apex frameworks that organisations follow are built following these design patterns.
  • Enhanced Understanding and Development: Knowing design patterns makes it easier to understand existing object-oriented systems. They can help you become a better designer, enabling novices to act more like experts. They are also useful for transitioning from analysis models to implementation models and can provide targets for refactoring, making designs more robust to change.
  • Reusable Solutions: They offer tested solutions to common design issues, preventing developers from having to "reinvent the wheel". By applying these patterns, you can reuse successful designs and architectures.
  • Standardized Terminology: They provide a common language for developers, which enhances collaboration and understanding among team members. This allows you to talk about designs at a higher level of abstraction.
  • Cross-Domain Applicability: Many patterns are language and domain-agnostic, making them valuable tools in diverse development environments. These principles can be applied to any OOP language, and also Salesforce Apex or LWC development.

In summary, design patterns are valuable because they provide reusable solutions, a common language, and guide developers toward creating flexible, maintainable, loosely-coupled and high-quality software systems.

Object-Oriented Programming Principles (OOPS!)

Object-Oriented Programming (OOP) principles are fundamental concepts that underpin the design of object-oriented software systems. Before diving into design patterns, it is crucial to understand some of these core principles. Understanding these OOP principles is critical because design patterns are built upon these fundamental techniques. Learning OOP principles first helps you understand why these problems exist and how patterns use principles like encapsulation, inheritance, and polymorphism to solve them

Here are the OOPS concepts that you need to know before starting on Design Patterns :

  • Encapsulation
  • Abstraction
  • Inheritance
  • Polymorphism
  • Coupling
  • Composition

Encapsulation

  • Encapsulation is a fundamental principle of object-oriented programming. It involves bundling the data ("fields" or "properties") and the behaviours (or methods) that operate on that data into a single unit, called a class.
  • The primary goal of encapsulation is to hide the internal implementation details of a class by only exposing the necessary functionalities to the outside world. This means the object's internal state is hidden and can only be changed via operations. The procedures (methods/operations) are the only way to access and modify an object's representation.
  • A common way to achieve encapsulation is by marking the data members as private. This prevents direct access to the data from outside the class. Instead, controlled access is provided through public "Getter" methods to retrieve the data and methods to manipulate it, ensuring that operations are performed safely and according to defined rules and logic.

Here is a bad example that does not follow Encapsulation :

public with sharing class AccountScoreService_Bad {
public List<Id> accountIds;
public Map<Id, Decimal> opportunityScore;
public Map<Id, Decimal> activityScore;
// Private method to calculate score based on Opportunities
public void calculateOpportunityScore() {
List<Opportunity> relatedOpps = [
SELECT Amount FROM Opportunity
WHERE AccountId IN :accountIds
];
for (Opportunity opp : relatedOpps) {
// Some logic on score
}
}
// Private method to calculate score based on Activities
public void calculateActivityScore() {
List<Task> relatedTasks = [
SELECT Status FROM Task
WHERE WhatId IN :accountIds
];
for (Task t : relatedTasks) {
// Some logic on score
}
}
}

Users of this class have direct access to the internal fields/properties and methods/logic of the AccountScoreService_Bad class. For example, users can directly assign the Map<Id, Decimal> opportunityScore and Map<Id, Decimal> activityScore since they are public. Also, The Users of this class are required to call the public methods to calculate the score:

List<Id> accountIds = new List<Id>();
for (Account acc : [SELECT Id FROM Account LIMIT 10]) {
accountIds.add(acc.Id);
}
AccountScoreService_Bad service = new AccountScoreService_Bad();
service.accountIds = accountIds;
service.calculateOpportunityScore();
service.calculateActivityScore();
Decimal opportunityScore = service.opportunityScore.get(accountIds[0]);
Decimal activityScore = service.activityScore.get(accountIds[0]);

A better way to define the class that follows the Encapsulation principle and hides the fields and internal logic:

public with sharing class AccountScoreService_Good {
private List<Id> accountIds;
private Map<Id, Decimal> opportunityScore;
private Map<Id, Decimal> activityScore;
// Constructor accepts the AccountPlan object
public AccountScoreService_Good(List<Id> accountIds) {
this.accountIds = accountIds;
this.opportunityScore = new Map<Id, Decimal>();
this.activityScore = new Map<Id, Decimal>();
calculateScore();
}
// Public method to trigger score calculation
private void calculateScore() {
calculateOpportunityScore();
calculateActivityScore();
}
// Public method to get the final Opportunity score
public Decimal getOpportunityScore(Id accountId) {
return opportunityScore.get(accountId);
}
// Public method to get the final Activity score
public Decimal getActivityScore(Id accountId) {
return activityScore.get(accountId);
}
// Private method to calculate score based on Opportunities
private void calculateOpportunityScore() {
List<Opportunity> relatedOpps = [
SELECT Amount FROM Opportunity
WHERE AccountId IN :accountIds
];
for (Opportunity opp : relatedOpps) {
// Some logic on score
}
}
// Private method to calculate score based on Activities
private void calculateActivityScore() {
List<Task> relatedTasks = [
SELECT Status FROM Task
WHERE WhatId IN :accountIds
];
for (Task t : relatedTasks) {
// Some logic on score
}
}
}

We can call this class as follows :

List<Id> accountIds = new List<Id>();
for (Account acc : [SELECT Id FROM Account LIMIT 10]) {
accountIds.add(acc.Id);
}
AccountScoreService_Good service = new AccountScoreService_Good(accountIds);
Decimal opportunityScore = service.getOpportunityScore(accountIds[0]);
Decimal activityScore = service.getActivityScore(accountIds[0]);

In the above example :

  • The AccountScoreService_Good class encapsulated the scoring data (Map<Id, Decimal> opportunityScore and Map<Id, Decimal> activityScore) and methods working on this data (calculateScore()) into a single unit which are private, encapsulating them within the class preventing direct access from the outside of the class by the user of this class.
  • The "Getter" methods (getOpportunityScore(Id accountId) and getActivityScore(Id accountId)) are used to provide controlled access to the data according to the logic defined.
  • private methods like calculateScore(), calculateOpportunityScore(), calculateActivityScore() are used internally by the class to handle the business logic, which the user of the class doesn't need to worry about.

In summary, Encapsulation is used to separate the public interface and the internal implementation/business logic of the class, allowing users to focus on the higher-level functionality.


Abstraction

  • Abstraction is the process of hiding the complex internal implementation details of a class or methods and exposing only the necessary features.
  • For example, when pressing a button on a TV remote, we don't have to worry about or interact directly with the internal circuit board – those details are abstracted away.

In Apex, we achieve abstraction using:

  • Abstract classes
  • Interfaces
  • Sometimes, base classes with virtual methods

✅ Scenario: Sending Notifications in Different Ways

Imagine you're building a system where:

  • A Contact may need to be notified by Email or SMS, based on user preference.

You want to create a flexible and extendable design where:

  • The core logic knows only how to trigger notifications, not how each type works.
  • You can add more notification types (like WhatsApp or Push) in the future without changing core logic.

Here is a bad example that does not follow Abstraction :

public class ContactNotificationService_Bad {
public void notifyContact(Contact c, String message, String method) {
if (method == 'Email') {
if (c.Email != null) {
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(new String[] { c.Email });
mail.setSubject('Notification');
mail.setPlainTextBody(message);
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
} else if (method == 'SMS') {
if (c.Phone != null) {
// Simulate sending SMS through external API like Twillio
System.debug('Sending SMS to: ' + c.Phone + ' with message: ' + message);
}
} else {
System.debug('Unsupported notification method: ' + method);
}
}
}

🚨 What's Wrong with This?

Problem Why It's a Violation of Abstraction
No interface or base class There’s no abstraction layer. The service class directly controls how email or SMS is sent.
Tightly coupled logic The service class knows about every notification method and how to implement them.
Hard to extend Adding WhatsApp or Push notifications means adding more else if blocks and more code changes to the same class.
Hard to test You can't mock or isolate the notification behaviour; it's all baked into one method.
Violates Single Responsibility Principle (SRP) This class is doing too much — both determining what and how to notify.

Here is a better way to define this logic using Abstraction :

Create an Interface (Abstraction)

public interface NotificationStrategy {
void sendNotification(Contact c, String message);
}

This interface provides a contract:

  • Any class implementing this interface must define how to sendNotification().

Concrete Implementations (Hidden Complexity)

Email Notification:

public class EmailNotification implements NotificationStrategy {
public void sendNotification(Contact c, String message) {
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(new String[] { c.Email });
mail.setSubject('Notification');
mail.setPlainTextBody(message);
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
}

SMS Notification (Dummy Example):

public class SMSNotification implements NotificationStrategy {
public void sendNotification(Contact c, String message) {
// Imagine this is an API call to external API like Twilio or similar to send SMS
System.debug('Sending SMS to: ' + c.Phone + ' with message: ' + message);
}
}

Notification Service (Abstracts the Logic)

public class ContactNotificationService_Good {
private NotificationStrategy notifier;
// Constructor takes in any notifier implementation
public ContactNotificationService_Good(NotificationStrategy notifier) {
this.notifier = notifier;
}
public void notifyContact(Contact c, String msg) {
if (c != null && msg != null) {
notifier.sendNotification(c, msg);
}
}
}

How to Use It in Apex

Let’s say you want to notify a contact by email:

Contact contact = [SELECT Id, Email FROM Contact WHERE Email != null LIMIT 1];
NotificationStrategy emailNotifier = new EmailNotification();
ContactNotificationService_Good service = new ContactNotificationService_Good(emailNotifier);
service.notifyContact(contact, 'Welcome to our platform!');

🧠 What Makes This Abstraction?

Element Role in Abstraction
NotificationStrategy interface Abstracts what a notification is, so that the classes using this interface don't have to know the concrete implementation for this interface
EmailNotification, SMSNotification Hides how the notification is sent into concrete implementation classes which implement the NotificationStrategy interface
ContactNotificationService_Good Uses the abstracted interface, not concrete logic. This way, ContactNotificationService_Good doesn't have to know about the exact logic for sending, let us say, Email or SMS notifications. It just has to know how to send a notification, which is by just calling the sendNotification() method on the NotificationStrategy interface

🔥 Advantages of Using Abstraction:

  • Loose coupling: Core logic doesn't depend on specific implementations, hence changing the concrete EmailNotification, SMSNotification classes won't break the ContactNotificationService_Good service class.
  • Easy extension Add new types (e.g., WhatsAppNotification) without breaking existing code.
  • Testable and maintainable: Each piece has a clear responsibility and can be tested individually.

Inheritance

  • Inheritance is the mechanism in object-oriented programming where one class (called a child or subclass) can inherit the properties and methods of another class (called a parent or superclass).
  • Subclasses inherit properties and behaviours from its superclasses and can also add new features or override existing ones.
  • Inheritance is described as a "is-a" relationship. For example, A Car "is-a" Vehicle and a Bike "is-a" Vehicle. So Vehicle can be a Super-class while Care and Bike can be child sub-classes.

✅ Salesforce Apex Example: Custom Validation Rules via Inheritance

🧠 Scenario:

You need to build a validation framework for different objects, like:

  • Contact: Must have a Name and Email.
  • Opportunity: Must have a Name, Amount and Close Date.

Instead of writing logic separately or duplicating code, you want a reusable, extensible structure that uses inheritance.

🎯 Goal:

  • Create a base class RecordValidator
  • Each object gets its own validator by inheriting the base class
  • All validators implement their own custom logic

Step 1: Base Class

public with sharing abstract class RecordValidator {
protected abstract List<String> validate(SObject record);
public List<String> validateRecord (SObject record) {
List<String> errors = basicValidation(record);
errors.addAll(validate(record));
return errors;
}
private List<String> basicValidation (SObject record) {
List<String> errors = new List<String>();
// Generic 'Name' check
if (record.get('Name') == null || String.isBlank((String)record.get('Name'))) {
errors.add('Name is required.');
}
return errors;
}
}

Step 2: Create Child Classes

Contact Validator:

public class ContactValidator extends RecordValidator {
protected override List<String> validate (SObject record) {
List<String> errors = new List<String>(); // Inherit default checks
Contact c = (Contact) record;
if (String.isBlank(c?.Email)) {
errors.add('Email is required for Contact.');
}
return errors;
}
}

Opportunity Validator:

public class OpportunityValidator extends RecordValidator {
protected override List<String> validate (SObject record) {
Opportunity opp = (Opportunity)record;
List<String> errors = new List<String>();
if (opp.Amount == null) {
errors.add('Amount is required for Opportunity.');
}
if (opp.CloseDate == null) {
errors.add('Close Date is required for Opportunity.');
}
return errors;
}
}

Sample Execution (Anonymous Apex):

Opportunity opp = new Opportunity(Amount = 50000);
List<String> errors = new OpportunityValidator().validateRecord(opp);
System.debug('errors ===> ' + errors);

✅ Benefits of Using Inheritance Here

  • Defines a common method validate() in the base RecordValidator class, which is overridden by the sub-classes to define the individual validation logic for each object.
  • ContactValidator, OpportunityValidator child classes customise validation for each object, extending the functionality of the RecordValidator class.
  • Reusable and easily extendible for other objects like Lead, Case, etc.
  • Each class has a clean separation of the logic to have only a single responsibility.
  • Each validator is easy to unit test independently

Polymorphism

  • Polymorphism is a core concept in Object-Oriented Programming (OOP), which is the ability of an object to take many forms.
  • It allows different classes to be treated as instances of a common parent class or interface, and the appropriate method implementation is called at runtime.

In Apex, polymorphism is used when:

  • You define a parent abstract class (or interface)
  • You write methods that use the parent type
  • At runtime, child class methods are executed depending on the actual object type

✅ Real-World Salesforce Use Case: Notification Sender

🧩 Requirement: You need to send different types of notifications to users:

  • Email notification
  • Chatter post notification

You want a clean, scalable way to handle all these using a single reference — that's where polymorphism shines.

First, let us see a bad example which is not following the Polymorphism principle:

public class NotificationService_Bad {
public static void notifyUser(String notificationType, String userId, String message) {
if (notificationType == 'Email') {
sendEmail(userId, message);
} else if (notificationType == 'Chatter') {
postToChatter(userId, message);
} else {
System.debug('❌ Unknown notification type');
}
}
private static void sendEmail(String userId, String message) {
System.debug('📧 Email sent to UserId: ' + userId + ' with message: ' + message);
}
private static void postToChatter(String userId, String message) {
System.debug('💬 Chatter post created for UserId: ' + userId + ' with message: ' + message);
}
}

The above class can be used as follows:

String userId = UserInfo.getUserId();
String message = 'Approval needed.';
NotificationService_Bad.notifyUser('Email', userId, message);
NotificationService_Bad.notifyUser('Chatter', userId, message);

🚨 Why This Is Bad

Problem Description
❌ No Polymorphism Each type is handled with if-else instead of letting objects decide behaviour, which violates the Open/Closed Principle (a SOLID Principle) and Polymorphism.
❌ Not Open/Closed If you want to add "Slack Notification", you'll need to edit the core notifyUser method — risky and error-prone since we are making changes on the same class/code.
❌ Tight Coupling NotificationService_Bad depends on all specific implementations in a single class in if-else conditions instead of delegating to different objects with implementation.
❌ Hard to Test No separation of concerns. You can't easily mock or isolate each type of notification.
❌ Poor Reusability Can't pass the logic around as objects — violates OOP design.

Now let's see a good example which follows the Polymorphism Principle:

✅ Step 1: Define an Interface

public interface NotificationSender {
void sendNotification(String userId, String message);
}

✅ Step 2: Implement Different Notification Classes

📧 Email Notification:

public class EmailNotificationSender implements NotificationSender {
public void sendNotification(String userId, String message) {
// Simulate sending email
System.debug('📧 Email sent to UserId: ' + userId + ' with message: ' + message);
}
}

💬 Chatter Notification:

public class ChatterNotificationSender implements NotificationSender {
public void sendNotification(String userId, String message) {
// Simulate posting to Chatter
System.debug('💬 Chatter post created for UserId: ' + userId + ' with message: ' + message);
}
}

✅ Step 3: Use Polymorphism

You can write a single method that works with the interface:

public with sharing class NotificationService_Good {
private NotificationSender sender;
public NotificationService_Good(NotificationSender sender) {
this.sender = sender;
}
public void notifyUser (String userId, String message) {
sender.sendNotification(userId, message);
}
}

The above class can be used as follows:

String userId = UserInfo.getUserId();
String message = 'Your approval is required.';
// Polymorphic behavior
new NotificationService_Good(new EmailNotificationSender()).notifyUser(userId, message);
new NotificationService_Good(new ChatterNotificationSender()).notifyUser(userId, message);

⚙️ What's Happening Here?

Although notifyUser uses the interface type (NotificationSender), Apex automatically calls the correct sendNotification() method based on the actual concrete class object passed (EmailNotificationSender ChatterNotificationSender).

🔍 Summary of OOP Concepts Applied

OOP Principle How It’s Used
Polymorphism One method call (sendNotification) behaves differently for each object type passed (EmailNotificationSender ChatterNotificationSender)
Interface Declares a common contract (NotificationSender), and concrete classes adhering to this contract have to define the sendNotification method
Encapsulation Each (EmailNotificationSender ChatterNotificationSender) class hides how it sends the message
Abstraction The Caller doesn't need to know how the notification is implemented, it just has to call the sendNotification on the NotificationSender interface
Inheritance (optional) Not used here, but could be if using abstract classes

Coupling

  • Coupling refers to the degree of direct knowledge or dependency one class or module has about another. It tells us how tightly connected different pieces of code are.
  • High coupling means that classes are tightly interconnected, making it difficult to modify or maintain them independently. Low coupling, on the other hand, indicates loose connections between classes, allowing for greater flexibility and ease of modification.

Why Is Coupling Important?

  • Low (loose) coupling makes your code more flexible, reusable, and testable.
  • High (tight) coupling leads to rigid, harder-to-maintain, and error-prone code.

High Coupling

  • Suppose when a class creates an object of another class or directly calls a static method from another class, it makes the two classes tightly coupled and any changes to one class may require modifications to the other class.

Low Coupling

  • To reduce coupling, we can introduce an abstraction (e.g., an interface) between the two classes. This allows the class to interact with the other class through the abstraction, making it easier to replace or modify the implementation of the abstract class or interface without affecting the other class.
  • This decouples the classes from the specific implementation of the other class, making the codebase more maintainable, flexible and can be modified independently without breaking the codebase.

Example:

  • The above example mentioned in the Polymorphism section can also be considered as a great example of loose and tight coupling, where the logic related to different notification types is tightly coupled with the same NotificationService_Bad class, which handles the sending notification logic.
  • But the NotificationService_Good class, which has the logic to send notifications, is loosely coupled with the logic related to each different type of notification using an abstraction by using the NotificationSender interface.

Composition

  • Composition is an OOP design principle where a class contains instances of other classes to reuse their behaviour and functionality instead of inheriting from them.
  • In composition, objects are assembled to form larger structures, with each component object maintaining its own state and behaviour.
  • Composition is often described in terms of a "has-a" relationship.
  • For example, let us consider a Car class (object) which has various components such as Engine, Wheels, which are separate classes responsible for their own functionality. The Car object is composed of these components and delegates the tasks to them for their functionality.

✅ Real-Life Salesforce Scenario for Composition

🧩 Use Case: Account Creation with Multiple Post-Processing Steps

You're asked to build an Account creation service in Apex that performs several tasks after an Account is inserted:

  1. Sends a welcome email to the primary contact.
  2. Logs the action in a custom object (AccountLog__c).

🎯 Goal: Compose Behaviour Using Small, Independent Classes

🔶 Step 1: Define an Interface for Post-Creation Actions

public interface AccountCreationAction {
public void createAccounts(List<Account> accounts);
}

🔷 Step 2: Implement Different Post-Action Behaviours

🔹 Send Welcome Email

public class SendWelcomeEmailAction implements AccountCreationAction {
public void createAccounts(List<Account> accounts) {
for (Account acc : accounts) {
// Simulate sending an email
System.debug('📧 Welcome email sent to Account: ' + acc.Name);
}
}
}

🔹 Log to AccountLog__c

public class CreateAccountLogAction implements AccountCreationAction {
public void createAccounts(List<Account> accounts) {
List<AccountLog__c> accountLogs = new List<AccountLog__c>();
for (Account acc : accounts) {
accountLogs.add(new AccountLog__c(
Account__c = acc.Id,
Action__c = 'Account Created'
));
System.debug('📝 Log entry created for Account: ' + acc.Name);
}
insert accountLogs;
}
}

🔷 Step 3: Compose the Service with These Behaviours

public with sharing class AccountCreationService {
private List<AccountCreationAction> actions;
public AccountCreationService(List<AccountCreationAction> actions) {
this.actions = actions;
}
public void createAccounts(List<Account> accounts) {
insert accounts;
// Post craation actions :
for (AccountCreationAction action : actions) {
action.createAccounts(accounts);
}
}
}

Here is how we can use the above solution:

Account acc = new Account(Name = 'Acme Inc.');
List<AccountCreationAction> postActions = new List<AccountCreationAction>{
new SendWelcomeEmailAction(),
new CreateAccountLogAction()
};
AccountCreationService service = new AccountCreationService(postActions);
service.createAccounts(new List<Account>{ acc });

✅ Why This Is a Great Example of Composition

Benefit Explanation
🔁 Reusable Each action class (email, log, assign) is reusable elsewhere.
Extensible Want to add another action? Just create a new class and add to the list --- no changes to existing logic.
🧪 Testable You can unit test each action in isolation or mock them if needed.
🔧 Decoupled The AccountCreationService knows nothing about what actions exist --- it just loops and calls insertAccounts() which decouples the AccountCreationService from each type of implementation of AccountCreationAction.
📦 Flexible You can dynamically change the list of actions based on config, environment, or user role.

Composition Vs Inheritance

💥 What if We Used Inheritance Here Instead? (Not Ideal Here)

  • Imagine if SendWelcomeEmailAction and CreateAccountLogAction inherited from AccountCreationAction, you’d end up duplicating logic or violating SRP. That’s where composition wins — you keep formatting separate from generating logic.
  • Also, let's say you want to call only a certain combination of AccountCreationAction, then you would have to again create sub-classes for those combinations, which would have allot of code duplication and aslo violating SRP too.

Let's try to develop the above example with Inheritance:

🔶 Step 1: Define an Interface for Post-Creation Actions

public with sharing virtual class AccountCreationAction_Inheritance {
public virtual void createAccounts (List<Account> accounts) {
insert accounts;
}
}

🔷 Step 2: Implement Different Post-Action Behaviours

🔹 Send Welcome Email

public with sharing class SendWelcomeEmailAction_Inheritance extends AccountCreationAction_Inheritance {
public override void createAccounts (List<Account> accounts) {
super.createAccounts(accounts);
for (Account acc : accounts) {
System.debug('Welcome email send to : ' + acc.Name);
}
}
}

🔹 Log to AccountLog__c

public with sharing class CreateAccountLogAction_Inheritance extends AccountCreationAction_Inheritance {
public override void createAccounts(List<Account> accounts) {
super.createAccounts(accounts);
List<AccountLog__c> accountLogs = new List<AccountLog__c>();
for (Account acc : accounts) {
accountLogs.add(new AccountLog__c(
Account__c = acc.Id,
Action__c = 'Account Created'
));
}
insert accountLogs;
}
}

🔹 Both Welcome Email and Log to AccountLog__c (Not Ideal)

public with sharing class AllAcountCReationActions_Inheritance extends AccountCreationAction_Inheritance {
public override void createAccounts (List<Account> accounts) {
super.createAccounts(accounts);
List<AccountLog__c> accountLogs = new List<AccountLog__c>();
for (Account acc : accounts) {
System.debug('Welcome email send to : ' + acc.Name);
accountLogs.add(new AccountLog__c(
Account__c = acc.Id,
Action__c = 'Account Created'
));
System.debug('Account Log created for Account : ' + acc.Name);
}
insert accountLogs;
//! Allot of code duplication here which violates the Single Responsibility Principle (a SOLID Principle)
}
}

The above example can be used as follows :

Account acc = new Account(Name = 'Acme Inc.');
AccountCreationAction_Inheritance action = new AllAcountCReationActions_Inheritance();
action.createAccounts(new List<Account>{ acc });

⚠️ The Problem with this

You want to apply all two behaviors, but Apex does not support multiple inheritance. You cannot extend more than one class at a time.

This means you're forced to create a class like this:

public with sharing class AllAcountCReationActions_Inheritance extends AccountCreationAction_Inheritance {
public override void createAccounts (List<Account> accounts) {
super.createAccounts(accounts);
List<AccountLog__c> accountLogs = new List<AccountLog__c>();
for (Account acc : accounts) {
System.debug('Welcome email send to : ' + acc.Name);
accountLogs.add(new AccountLog__c(
Account__c = acc.Id,
Action__c = 'Account Created'
));
System.debug('Account Log created for Account : ' + acc.Name);
}
insert accountLogs;
//! Allot of code duplication here which violates the Single Responsibility Principle (a SOLID Principle)
}
}

❌ Why This Inheritance-Based Design Is Not Ideal

🚫 Problem 💡 Explanation
No multiple inheritance You can't mix SendWelcomeEmailAction_Inheritance and CreateAccountLogAction_Inheritance, and if you want to do so, you have to create a new sub-class similar to AllAcountCReationActions_Inheritance.
Poor Separation of Concerns Let's say if we consider the AllAcountCReationActions_Inheritance class, all logic is now in a single class, having poor separation of concerns.
Low Reusability Want just logging OR Welcome Email, OR Both? You'll have to duplicate code for each use case.
Hard to Extend Want to add a new behavior? You have to modify or duplicate classes.
Tightly Coupled Hard to test one behaviour in isolation since for a combination of actions, all the logic is defined in a single class.

Fragile Base Class Issue in OOP Software

Fragile Base Class Problem — is a common design issue caused by Inheritance and can be avoided using Composition. It's a software design issue in Object-Oriented Programming (OOP), which occurs when a change is made in the base or parent class, can cause issues and break the functionality of the derived child classes. This occurs due to a tight coupling between the base and derived classes in Inheritance hierarchies.

🛡️ How to Avoid the Fragile Base Class Problem

  • ✅ Prefer Composition Over Inheritance : Composition promotes loose coupling between classes, thus making the codebase more maintainable and scalable.
  • ✅ Keep Base Classes Simple : Don’t call virtual or abstract methods in constructors or base logic defined in the base class unexpectedly. Also, avoid deep Inheritance hierarchies.
  • ✅ Use Interfaces for Behavior Extension : This allows behavior injection without inheritance coupling.

SOLID Principles

SOLID is an acronym for five design principles intended to make software systems more understandable, flexible, and maintainable. These principles are fundamental in Object-Oriented Programming (OOP) and lay the foundation for good software design.

Letter Principle Name Description
S Single Responsibility Principle (SRP) A class should have only one reason to change
O Open/Closed Principle (OCP) Software entities should be open for extension, but closed for modification
L Liskov Substitution Principle (LSP) Objects of subclasses should be substitutable with objects of their base classes without altering correctness
I Interface Segregation Principle (ISP) Clients should not be forced to depend on methods/interfaces they do not use
D Dependency Inversion Principle (DIP) Depend on abstractions, not on concrete implementations

🎯 How Do SOLID Principles Relate to Design Patterns?

Think of SOLID principles as the "rules" or "guiding principles" of clean object-oriented design.
Think of Design Patterns as "solutions to recurring design problems".

If you understand SOLID, you'll:

  • Know why a design pattern is structured a certain way,
  • Avoid misusing patterns (e.g., using inheritance wrongly),
  • Build your own reusable designs effectively.

✅ Why Are They Important?

Without these principles, code can become:

  • Tightly coupled

  • Hard to test

  • Difficult to change

  • Prone to bugs

By following SOLID, your code becomes:

  • Easier to understand

  • Easier to extend with new features

  • Easier to maintain and debug

  • Easier to test (unit/integration)

  • A good foundation for applying design patterns

✅ Summary

  • SOLID is a set of five key principles for building well-structured, maintainable, and testable object-oriented software.

  • These principles guide your thinking when choosing or building design patterns.

  • In Salesforce Apex, they help you build code that's easier to test, change, and scale.

  • Learning SOLID is like learning the grammar of clean software design --- design patterns are the sentences you write with it.

S - Single Responsibility Principle

The Single Responsibility Principle states that: “A class/module should have only one reason to change, meaning that it should have only one responsibility or purpose.”

This principle encourages you to create classes that are more focused and perform a single well-defined task, rather than multiple tasks. Breaking up classes into smaller, more focused units makes code easier to understand, maintain, and test.

🔍 Real-Life Salesforce Scenario

💼 Business Requirement:

When a Case is created in Salesforce:

  1. It should automatically assign the Support Queue as the owner.

  2. It should send a notification email to the assigned queue.

  3. It should create a custom object record (e.g., CaseAudit__c) to log the event.

  4. It should send a Slack message to support channel (in the future).

We will first violate SRP and then refactor it using SRP.


❌ Bad Design (Violates SRP)

public with sharing class CaseTriggerHandler_Bad {
public void handleBeforeInsert (List<Case> newCases) {
// Assign Queue
Group supportQueue = [SELECT Id FROM Group WHERE Name = 'Support Queue' LIMIT 1];
for (Case cs : newCases) {
cs.OwnerId = supportQueue.Id;
}
}
public void handleAfterInsert (List<Case> newCases) {
// Send Notification
List<Messaging.SingleEmailMessage> mails = new List<Messaging.SingleEmailMessage>();
for (Case cs : newCases) {
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(new String[] { '[email protected]' });
mail.setSubject('New Case Created');
mail.setPlainTextBody('Case created: ' + cs.Subject);
mails.add(mail);
}
Messaging.sendEmail(mails);
// Log to CaseAudit__c
List<CaseAudit__c> caseAudits = new List<CaseAudit__c>();
for (Case cs : newCases) {
CaseAudit__c audit = new CaseAudit__c(
Case__c = cs.Id,
Action__c = 'Case Created'
);
caseAudits.add(audit);
}
insert caseAudits;
}
}

❌ Problems with the Above Code:

Problem Why it's bad
🚨 Too many responsibilities Assigning owner, sending email, logging audit
❌ Hard to maintain Any change to one behaviour affects others
❌ Hard to test Can't test email or audit separately
❌ Hard to extend Adding Slack or SMS later will clutter it even more

✅ Refactored Design Using SRP

We break the logic into separate classes --- each doing one job. This follows SRP.


1️⃣ CaseOwnerAssigner -- Assigns to queue

public class CaseOwnerAssigner {
public void assignQueue(List<Case> newCases) {
Group queue = [SELECT Id FROM Group WHERE Name = 'Support Queue' LIMIT 1];
for (Case c : newCases) {
c.OwnerId = queue.Id;
}
}
}


2️⃣ CaseNotifier -- Sends email notifications

public class CaseNotifier {
public void notifySupport(List<Case> newCases) {
List<Messaging.SingleEmailMessage> mails = new List<Messaging.SingleEmailMessage>();
for (Case c : newCases) {
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(new String[] { '[email protected]' });
mail.setSubject('New Case Created');
mail.setPlainTextBody('Case created: ' + c.Subject);
mails.add(mail);
}
Messaging.sendEmail(mails);
}
}


3️⃣ CaseAuditLogger -- Creates audit record

public class CaseAuditLogger {
public void logCreation(List<Case> newCases) {
List<CaseAudit__c> audits = new List<CaseAudit__c>();
for (Case c : newCases) {
CaseAudit__c audit = new CaseAudit__c(
Case__c = c.Id,
Action__c = 'Case Created'
);
audits.add(audit);
}
insert audits;
}
}


4️⃣ CaseTiggerHandler -- Now acts as an orchestrator

public with sharing class CaseTriggerHandler_Good {
private CaseOwnerAssigner assigner = new CaseOwnerAssigner();
private CaseNotifier notifier = new CaseNotifier();
private CaseAuditLogger logger = new CaseAuditLogger();
public void handleBeforeInsert (List<Case> newCases) {
assigner.assignQueue(newCases);
}
public void handleAfterInsert (List<Case> newCases) {
notifier.notifySupport(newCases);
logger.logCreation(newCases);
}
}


✅ Benefits of Following SRP

Benefit Explanation
🔁 Reusable code You can reuse CaseNotifier in other places
🧪 Easier testing Test each class in isolation
🔄 Easy to change Changing queue name only affects CaseOwnerAssigner
➕ Easier to add features Want to send Slack message? Just add a new class like SlackNotifier
📦 Clean architecture Each class does one job, making the system modular

O - Open/Closed Principle

"Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification."

This means:

  • You should be able to add new behaviour without changing existing, tested, working code.
  • You achieve this with abstraction (interfaces, virtual classes) and polymorphism (override methods, strategies, etc.).

📘 Real-Life Salesforce Scenario

Your company uses Salesforce to manage Orders and each Order can come from different Sales Channels (e.g., Online, Partner, Internal, Marketplace, etc.).

Each sales channel has a different commission calculation rule:

  • Online: 12% commission
  • Partner: 10% + bonus if over ₹100,000
  • Internal: No commission
  • Marketplace: 8% + flat platform fee deduction

You need to calculate and populate the Commission__c field for each Order__c record based on its Channel__c.

New channels might be added frequently in the future.

❌ Bad Design -- Violates OCP

Here's a version that does everything in one class:

public class OrderCommissionCalculator_Bad {
public static void calculate(List<Order__c> orders) {
for (Order__c order : orders) {
if (order.Channel__c == 'Online') {
order.Commission__c = calculateOnlineCommission(order);
} else if (order.Channel__c == 'Partner') {
order.Commission__c = calculatePartnerCommission(order);
} else if (order.Channel__c == 'Internal') {
order.Commission__c = calculateInternalCommission(order);
} else if (order.Channel__c == 'Marketplace') {
order.Commission__c = calculateMarketplaceCommission(order);
} else {
order.Commission__c = 0;
}
}
update orders;
}
private static Decimal calculateOnlineCommission(Order__c order) {
Decimal commission = 0;
// Some code logic to calculate the Commission__c
return commission;
}
private static Decimal calculatePartnerCommission(Order__c order) {
Decimal commission = 0;
// Some code logic to calculate the Commission__c
return commission;
}
private static Decimal calculateInternalCommission(Order__c order) {
Decimal commission = 0;
// Some code logic to calculate the Commission__c
return commission;
}
private static Decimal calculateMarketplaceCommission(Order__c order) {
Decimal commission = 0;
// Some code logic to calculate the Commission__c
return commission;
}
}

❌ Problems:

  • Every time a new channel is introduced or logic changes, you must modify this class.
  • Very hard to test each channel independently.
  • You risk breaking working logic while adding new logic.
  • Violates Open/Closed Principle.

✅ Good Design -- Follows OCP (Open for Extension, Closed for Modification)

We'll refactor using abstraction + polymorphism.

1️⃣ Define the Strategy Interface

public interface ICommissionCalculator {
void applyCommission(Order__c order);
}

2️⃣ Implement Channel-Specific Strategies

public class OnlineOrderCommissionCalculator implements ICommissionCalculator {
public void applyCommission(Order__c order) {
order.Commission__c = calculateOnlineCommission(order);
}
private static Decimal calculateOnlineCommission(Order__c order) {
Decimal commission = 0;
// Some code logic to calculate the Commission__c
return commission;
}
}

public class PartnerOrderCommissionCalculator implements ICommissionCalculator {
public void applyCommission(Order__c order) {
order.Commission__c = calculatePartnerCommission(order);
}
private static Decimal calculatePartnerCommission(Order__c order) {
Decimal commission = 0;
// Some code logic to calculate the Commission__c
return commission;
}
}

public class InternalOrderCommissionCalculator implements ICommissionCalculator {
public void applyCommission(Order__c order) {
order.Commission__c = calculateInternalCommission(order);
}
private static Decimal calculateInternalCommission(Order__c order) {
Decimal commission = 0;
// Some code logic to calculate the Commission__c
return commission;
}
}

3️⃣ Define the OrderCommissionCalculator_Good

public class OrderCommissionCalculator_Good {
private static Map<String, System.Type> COMMISSION_CALCULATOR_MAP = new Map<String, System.Type>{
'Online' => OnlineOrderCommissionCalculator.class,
'Partner' => PartnerOrderCommissionCalculator.class,
'Internal' => InternalOrderCommissionCalculator.class,
'Marketplace' => MarketplaceOrderCommissionCalculator.class
};
public static void calculate(List<Order__c> orders) {
for (Order__c order : orders) {
if (COMMISSION_CALCULATOR_MAP.containsKey(order.Channel__c)) {
ICommissionCalculator commissionCalculator = (ICommissionCalculator) COMMISSION_CALCULATOR_MAP.get(order.Channel__c).newInstance();
commissionCalculator.applyCommission(order);
}
}
update orders;
}
}

🚀 Future Expansion — The OCP Power

Suppose your company introduces a new channel: Marketplace, where there is a different Commission calculation logic.

All you do is:

public class MarketplaceOrderCommissionCalculator implements ICommissionCalculator {
public void applyCommission(Order__c order) {
order.Commission__c = calculateMarketplaceCommission(order);
}
private static Decimal calculateMarketplaceCommission(Order__c order) {
Decimal commission = 0;
// Some code logic to calculate the Commission__c
return commission;
}
}

And update the Map in the OrderCommissionCalculator_Good class:

public class OrderCommissionCalculator_Good {
private static Map<String, System.Type> COMMISSION_CALCULATOR_MAP = new Map<String, System.Type>{
'Online' => OnlineOrderCommissionCalculator.class,
'Partner' => PartnerOrderCommissionCalculator.class,
'Internal' => InternalOrderCommissionCalculator.class,
'Marketplace' => MarketplaceOrderCommissionCalculator.class
};

  • ✅ No changes to existing logic and OrderCommissionCalculator class (just a minimum change of the static Map<String, System.Type>).
  • ✅ No impact on existing tested logic.
  • ✅ Open to extension, but closed to modification.

🧠 Summary

🔍 Principle 💬 Explanation
Open/Closed You can extend the system for new logic without modifying existing code
How Using interfaces and strategy pattern
Why Safer, more testable, scalable and maintainable

L - Liskov Substitution Principle

Overview

Objects of a superclass should be replaceable with objects of its subclasses without breaking the application functionality.

The Liskov Substitution Principle (LSP) is the "L" in the SOLID principles of object-oriented design. This principle ensures that inheritance relationships are designed correctly and that subclasses can seamlessly replace their parent classes without causing unexpected behaviour.

In practical terms, LSP means that if you have a method that expects a Vehicle object, you should be able to pass it a Car object, Truck object, or any other subclass of Vehicle without the method failing or behaving unexpectedly. The subclass should honour the "contract" established by the parent class.

This principle is crucial for creating robust and maintainable inheritance hierarchies, ensuring that polymorphism works correctly in your Salesforce Apex applications.

📘 Real Life Salesforce Scenario

Your company processes various types of Payment Methods in Salesforce, and each payment type has different processing rules and validation requirements.

The payment types include:

  • Credit Card: Requires card validation, authorization, and charges processing fees
  • Bank Transfer: Requires account verification and has longer processing times
  • Digital Wallet: Requires token validation and instant processing
  • Gift Card: Requires balance checking and redemption tracking

You need to create a flexible system that allows any payment processor to handle different payment types without requiring knowledge of their specific implementation details, ensuring that all payment methods can be used interchangeably.

❌ Bad Example (Anti-Pattern)

A common violation of LSP occurs when subclasses change the expected behavior of the parent class methods, throw unexpected exceptions, or have different preconditions/postconditions than the parent class.

Code Example - Bad Implementation

📃 The contract (abstract class)

public abstract class PaymentProcessor_Bad {
public abstract Decimal calculateProcessingFee(Decimal amount);
public abstract Boolean processPayment(Decimal amount);
public abstract Boolean validatePayment(Decimal amount);
}

1️⃣ The Credit Card Processor Strategy

// Violates LSP - throws unexpected exceptions
public class CreditCardProcessor_Bad extends PaymentProcessor_Bad {
public override Decimal calculateProcessingFee(Decimal amount) {
if (amount <= 0) {
throw new IllegalArgumentException('Amount must be positive');
}
return amount * 0.03; // 3% fee
}
public override Boolean processPayment(Decimal amount) {
return amount > 0 && amount <= 10000;
}
public override Boolean validatePayment(Decimal amount) {
return amount > 0;
}
}

2️⃣ The Gift Card Processor Strategy

// Violates LSP - changes method behavior expectations
public class GiftCardProcessor_Bad extends PaymentProcessor_Bad {
private Decimal availableBalance = 500;
public override Decimal calculateProcessingFee(Decimal amount) {
return 0; // No fees for gift cards - this breaks expectations
}
// Violates LSP - fails for valid amounts that parent class should handle
public override Boolean processPayment(Decimal amount) {
if (amount > availableBalance) {
throw new PaymentException('Insufficient gift card balance');
}
availableBalance -= amount;
return true;
}
// Violates LSP - different validation rules than expected
public override Boolean validatePayment(Decimal amount) {
return amount > 0 && amount <= availableBalance;
}
}

3️⃣ The Bank Transfer Processor Strategy

// Violates LSP - completely changes expected behavior
public class BankTransferProcessor_Bad extends PaymentProcessor_Bad {
public override Decimal calculateProcessingFee(Decimal amount) {
// Returns negative value - completely unexpected behavior
return -5.00; // Bank transfer gives discount?
}
public override Boolean processPayment(Decimal amount) {
// Always returns false - breaks substitutability
System.debug('Bank transfer queued for batch processing');
return false; // Doesn't actually process immediately
}
public override Boolean validatePayment(Decimal amount) {
return amount >= 1000; // Arbitrary minimum not in parent contract
}
}

❌ Usage - Bad Example

The Service Class

public class PaymentService_Bad {
public static void processOrderPayment(Order__c order, PaymentProcessor_Bad processor) {
try {
// This code expects consistent behavior from all processors
if (processor.validatePayment(order.Amount__c)) {
Decimal fee = processor.calculateProcessingFee(order.Amount__c);
Boolean success = processor.processPayment(order.Amount__c);
if (success) {
order.Processing_Fee__c = fee;
order.Status__c = 'Processed';
} else {
order.Status__c = 'Failed';
}
}
} catch (Exception e) {
// Unexpected exceptions break the flow
order.Status__c = 'Error';
}
}
}

Usage of the Service Class (Anonymous Window)

// Usage that demonstrates the problems
Order__c order1 = new Order__c(Amount__c = -100);
Order__c order2 = new Order__c(Amount__c = 1000);
Order__c order3 = new Order__c(Amount__c = 1000);
PaymentService_Bad.processOrderPayment(order1, new CreditCardProcessor_Bad()); // Might throw unexpected exception
PaymentService_Bad.processOrderPayment(order2, new GiftCardProcessor_Bad()); // Might fail due to balance check
PaymentService_Bad.processOrderPayment(order3, new BankTransferProcessor_Bad()); // Always returns false

Problems with This Approach

Problem Description Impact
Unexpected Exceptions Subclasses throw exceptions that parent class contract doesn't specify Client code breaks when switching between implementations
Inconsistent Return Values Different subclasses return different types of values (negative fees, always false) Business logic produces incorrect results
Changed Preconditions Subclasses have stricter requirements than parent class Code that works with parent class fails with subclasses
Broken Contracts Methods don't fulfill the promises made by the parent class interface Polymorphism becomes unreliable and dangerous
Unpredictable Behavior Each subclass behaves differently in unexpected ways System becomes fragile and hard to maintain

✅ Good Example (Proper Implementation following LSP)

The correct implementation ensures that all subclasses can be used interchangeably with the parent class, maintaining consistent behaviour and honouring the established contract.

Code Example - Good Implementation

📃 Abstract Payment Processor Base Class

public abstract class PaymentProcessor_Good {
protected String paymentType;
public PaymentProcessor_Good(String paymentType) {
this.paymentType = paymentType;
}
// Contract: Always returns non-negative fee or 0
public abstract Decimal calculateProcessingFee(Decimal amount);
// Contract: Returns true if payment processed successfully, false otherwise
// Should not throw exceptions for business logic failures
public abstract Boolean processPayment(Decimal amount);
// Contract: Returns true if amount is valid for this payment type
public virtual Boolean validatePayment(Decimal amount) {
return amount != null && amount > 0;
}
// Common behavior that all subclasses inherit
public String getPaymentType() {
return this.paymentType;
}
}

1️⃣ Credit Card Processor Implementation

public class CreditCardProcessor_Good extends PaymentProcessor_Good {
private static final Decimal PROCESSING_FEE_RATE = 0.03;
private static final Decimal MAX_AMOUNT = 10000;
public CreditCardProcessor_Good() {
super('Credit Card');
}
public override Decimal calculateProcessingFee(Decimal amount) {
// Always returns non-negative fee, honoring parent contract
if (amount == null || amount <= 0) {
return 0;
}
return amount * PROCESSING_FEE_RATE;
}
public override Boolean processPayment(Decimal amount) {
// Honors parent contract - returns boolean, doesn't throw exceptions
if (!validatePayment(amount)) {
return false;
}
// Simulate credit card processing
try {
// Process credit card payment logic here
System.debug('Processing credit card payment: ' + amount);
return true;
} catch (Exception e) {
System.debug('Credit card processing failed: ' + e.getMessage());
return false;
}
}
public override Boolean validatePayment(Decimal amount) {
// Maintains parent's validation contract while adding specific rules
return super.validatePayment(amount) && amount <= MAX_AMOUNT;
}
}

2️⃣ Gift Card Processor Implementation

public class GiftCardProcessor_Good extends PaymentProcessor_Good {
private Decimal availableBalance;
public GiftCardProcessor_Good(Decimal balance) {
super('Gift Card');
this.availableBalance = balance != null ? balance : 0;
}
public override Decimal calculateProcessingFee(Decimal amount) {
// Honors parent contract - returns non-negative fee
return 0; // Gift cards have no processing fees
}
public override Boolean processPayment(Decimal amount) {
// Honors parent contract - returns boolean without throwing exceptions
if (!validatePayment(amount)) {
return false;
}
if (amount > availableBalance) {
System.debug('Insufficient gift card balance');
return false;
}
// Deduct from balance and process
availableBalance -= amount;
System.debug('Gift card payment processed: ' + amount);
return true;
}
public override Boolean validatePayment(Decimal amount) {
// Maintains parent's validation while adding balance check
return super.validatePayment(amount) && amount <= availableBalance;
}
public Decimal getAvailableBalance() {
return availableBalance;
}
}

3️⃣ Bank Transfer Processor Implementation

public class BankTransferProcessor_Good extends PaymentProcessor_Good {
private static final Decimal FLAT_FEE = 5.00;
private static final Decimal MIN_AMOUNT = 10.00;
public BankTransferProcessor_Good() {
super('Bank Transfer');
}
public override Decimal calculateProcessingFee(Decimal amount) {
// Honors parent contract - returns non-negative fee
if (amount == null || amount <= 0) {
return 0;
}
return FLAT_FEE;
}
public override Boolean processPayment(Decimal amount) {
// Honors parent contract - processes immediately or returns false
if (!validatePayment(amount)) {
return false;
}
try {
// Simulate bank transfer processing
System.debug('Processing bank transfer: ' + amount);
return true;
} catch (Exception e) {
System.debug('Bank transfer failed: ' + e.getMessage());
return false;
}
}
public override Boolean validatePayment(Decimal amount) {
// Maintains parent's validation contract
return super.validatePayment(amount) && amount >= MIN_AMOUNT;
}
}

✅ Usage - Good Example

The Service Class

public class PaymentService_Good {
public static PaymentResult processOrderPayment(Order__c order, PaymentProcessor_Good processor) {
PaymentResult result = new PaymentResult();
// Thanks to LSP, this method works with ANY PaymentProcessor_Good subclass
if (processor.validatePayment(order.Amount__c)) {
Decimal fee = processor.calculateProcessingFee(order.Amount__c);
Boolean success = processor.processPayment(order.Amount__c);
result.success = success;
result.processingFee = fee;
result.paymentType = processor.getPaymentType();
if (success) {
order.Processing_Fee__c = fee;
order.Status__c = 'Processed';
order.Payment_Method__c = processor.getPaymentType();
} else {
order.Status__c = 'Failed';
}
} else {
result.success = false;
result.errorMessage = 'Payment validation failed';
order.Status__c = 'Invalid';
}
return result;
}
// This method can work with ANY payment processor without modification
public static List<PaymentResult> processBulkPayments(List<Order__c> orders, PaymentProcessor_Good processor) {
List<PaymentResult> results = new List<PaymentResult>();
for (Order__c order : orders) {
results.add(processOrderPayment(order, processor));
}
return results;
}
}

Usage of the Service Class (Anonymous Window)

// Usage examples - all work identically due to LSP compliance
PaymentProcessor_Good creditProcessor = new CreditCardProcessor_Good();
PaymentProcessor_Good giftProcessor = new GiftCardProcessor_Good(1000);
PaymentProcessor_Good bankProcessor = new BankTransferProcessor_Good();
// All of these calls work exactly the same way
PaymentResult result1 = PaymentService_Good.processOrderPayment(order1, creditProcessor);
PaymentResult result2 = PaymentService_Good.processOrderPayment(order2, giftProcessor);
PaymentResult result3 = PaymentService_Good.processOrderPayment(order3, bankProcessor);

Benefits of This Approach

Benefit Description Business Value
Interchangeability Any payment processor can be used without changing client code Easy to add new payment methods without system changes
Consistent Behavior All processors follow the same contract and behavioral expectations Predictable system behavior and fewer bugs
Simplified Testing Mock implementations can easily replace real processors Better test coverage and easier unit testing
Future-Proof Design New payment types can be added without modifying existing code Reduced development time for new features

Key Benefits

  • Seamless Substitutability: Any subclass can replace the parent class without breaking functionality
  • Behavioral Consistency: All implementations follow the same contract and expectations
  • Reduced Coupling: Client code depends on abstractions, not concrete implementations
  • Enhanced Polymorphism: True polymorphic behavior where objects can be used interchangeably
  • Easier Maintenance: Changes to specific implementations don't affect client code

✅ When to Use

  • When designing inheritance hierarchies with multiple implementations
  • When you need polymorphic behavior where objects should be interchangeable
  • When building plugin-style architectures in Salesforce
  • When creating frameworks or reusable components that others will extend
  • When implementing the Strategy pattern or similar behavioral patterns
  • When you have multiple ways to accomplish the same business goal

❌ When NOT to Use

  • When subclasses have fundamentally different purposes or behaviors
  • When the relationship is "has-a" rather than "is-a" (use composition instead)
  • When subclasses would need to violate the parent class contract
  • For simple utility classes that don't need inheritance
  • When performance is critical and polymorphism adds unnecessary overhead

💡 Real-World Salesforce Scenarios

  1. Notification Systems: Different notification channels (Email, SMS, Push) that all implement a common NotificationSender interface, allowing the system to send notifications through any channel without knowing the specific implementation.

  2. Data Validation Frameworks: Various validation rules (Required Field, Format, Range) that all extend a base ValidationRule class, enabling the validation engine to process any rule type uniformly.

  3. Integration Adapters: Different external system connectors (REST API, SOAP, Database) that all implement a common ExternalSystemAdapter interface, allowing the integration layer to work with any system using the same code.

📃 Summary

The Liskov Substitution Principle ensures that inheritance relationships are designed correctly by requiring subclasses to be fully substitutable for their parent classes. In Salesforce development, this principle helps create robust, flexible systems where new implementations can be added without breaking existing functionality, leading to more maintainable and extensible code that truly leverages the power of object-oriented programming.

I - Interface Segregation Principle (ISP)

Overview

Clients should not be forced to depend on interfaces they do not use.

The Interface Segregation Principle is the "I" in SOLID principles and focuses on creating focused, role-specific interfaces rather than monolithic ones. This principle states that no class should be forced to implement methods it doesn't need or use. Instead of having one large interface that handles multiple responsibilities, we should break it down into smaller, more specific interfaces that serve particular needs.

Key Benefits:

  • Reduces coupling between classes and unnecessary dependencies
  • Improves maintainability by making interfaces focused and cohesive
  • Enhances flexibility by allowing classes to implement only what they need

📧 Real Life Salesforce Scenario

Your Salesforce org needs to send Notifications to different types of users based on various events:

User Types and Their Notification Needs:

  • Customers: Need email notifications only
  • Sales Reps: Need email and SMS notifications
  • Managers: Need email, SMS, and push notifications to mobile app

Currently, you have a single notification interface that all notification services must implement, but most services only need a subset of these notification methods.

❌ Bad Example (Anti-Pattern)

The violation occurs when we create a single "fat" interface that forces all implementing classes to implement notification methods they don't support.

❌ Code Example - Bad Implementation

The Fat Interface

// Fat interface that violates ISP
public interface NotificationService_Bad {
void sendEmail(String recipient, String message);
void sendSMS(String phoneNumber, String message);
void sendPushNotification(String userId, String message);
}

The Implementation

// Customer service forced to implement methods it doesn't need
public class CustomerNotificationService_Bad implements NotificationService_Bad {
public void sendEmail(String recipient, String message) {
System.debug('Sending email to customer: ' + recipient);
// Actual email implementation
}
// Forced to implement SMS even though customers don't receive SMS
public void sendSMS(String phoneNumber, String message) {
throw new UnsupportedOperationException('Customers do not receive SMS notifications');
}
// Forced to implement push notifications even though customers don't have the app
public void sendPushNotification(String userId, String message) {
throw new UnsupportedOperationException('Customers do not receive push notifications');
}
}

❌ Usage - Bad Example

// Usage in Anonymous Apex
CustomerNotificationService_Bad customerService = new CustomerNotificationService_Bad();
// This works fine
customerService.sendEmail('[email protected]', 'Your order is ready!');
// These will throw exceptions even though the interface allows them
try {
customerService.sendSMS('1234567890', 'SMS message'); // Throws exception!
customerService.sendPushNotification('user123', 'Push message'); // Throws exception!
} catch (UnsupportedOperationException e) {
System.debug('Error: ' + e.getMessage());
}

❌ Problems with This Approach

Problem Description Impact
Forced Implementation Classes must implement methods they don't support Leads to exceptions or empty implementations
Interface Pollution Single interface contains unrelated notification methods Difficult to understand what each service actually supports
Runtime Errors Unused methods throw exceptions when called Creates unreliable code that fails at runtime
Tight Coupling All services depend on all notification types Changes affect services that don't use those methods

✅ Good Example (Proper Implementation following ISP)

The correct approach is to segregate the interface into smaller, focused interfaces based on specific notification types. Each service implements only the notification methods it actually supports.

✅ Code Example - Good Implementation

1️⃣ Segregated Notification Interfaces

public interface EmailNotifier_Good {
void sendEmail(String recipient, String message);
}

public interface SMSNotifier_Good {
void sendSMS(String phoneNumber, String message);
}

public interface PushNotifier_Good {
void sendPushNotification(String userId, String message);
}

2️⃣ Focused Implementation Classes

Notifications for Customers

public class CustomerNotificationService_Good implements EmailNotifier_Good {
public void sendEmail(String recipient, String message) {
System.debug('Sending email to customer: ' + recipient);
// Actual email implementation for customers
}
}

Notifications for Sales Reps

public class SalesRepNotificationService_Good implements EmailNotifier_Good, SMSNotifier_Good {
public void sendEmail(String recipient, String message) {
System.debug('Sending email to sales rep: ' + recipient);
// Email implementation for sales reps
}
public void sendSMS(String phoneNumber, String message) {
System.debug('Sending SMS to sales rep: ' + phoneNumber);
// SMS implementation for sales reps
}
}

Notifications for Manager

public class ManagerNotificationService_Good implements EmailNotifier_Good, SMSNotifier_Good, PushNotifier_Good {
public void sendEmail(String recipient, String message) {
System.debug('Sending email to manager: ' + recipient);
// Email implementation for managers
}
public void sendSMS(String phoneNumber, String message) {
System.debug('Sending SMS to manager: ' + phoneNumber);
// SMS implementation for managers
}
public void sendPushNotification(String userId, String message) {
System.debug('Sending push notification to manager: ' + userId);
// Push notification implementation for managers
}
}

✅ Usage - Good Example

// Usage in Anonymous Apex
CustomerNotificationService_Good customerService = new CustomerNotificationService_Good();
SalesRepNotificationService_Good repService = new SalesRepNotificationService_Good();
ManagerNotificationService_Good managerService = new ManagerNotificationService_Good();
// Each service only has methods it actually supports
customerService.sendEmail('[email protected]', 'Order update');
repService.sendEmail('[email protected]', 'New lead assigned');
repService.sendSMS('1234567890', 'Urgent: Follow up needed');
managerService.sendEmail('[email protected]', 'Monthly report');
managerService.sendSMS('0987654321', 'Team meeting reminder');
managerService.sendPushNotification('mgr123', 'Approval needed');
// Can work with services through specific interfaces
EmailNotifier_Good emailService = customerService;
emailService.sendEmail('[email protected]', 'Test message');

✅ Benefits of This Approach

Benefit Description Impact
No Forced Methods Classes only implement methods they actually support No exceptions or empty implementations
Clear Contracts Each interface represents a specific capability Easy to understand what each service can do
Flexible Composition Services can implement multiple focused interfaces Mix and match capabilities as needed
Isolated Changes Changes to one notification type don't affect others Better maintainability and stability

✅ Key Benefits

  • Eliminates unnecessary methods - Classes only implement what they actually support
  • Clear responsibilities - Each interface has a single, focused purpose
  • Better composition - Services can combine multiple capabilities as needed
  • No runtime exceptions - All implemented methods are actually supported
  • Easier testing - Can mock specific notification types independently
  • Improved maintainability - Changes are isolated to relevant interfaces

🎯 When to Use

  • When classes implement interface methods by throwing exceptions or leaving them empty
  • When different clients need different subsets of functionality
  • When you have a "fat" interface that serves multiple types of users
  • When changes to interface methods affect classes that don't use those methods
  • When designing systems with optional or conditional capabilities

⚠️ When NOT to Use

  • When all implementing classes genuinely need all interface methods
  • When interfaces are already small and focused
  • When over-segregation creates unnecessary complexity
  • In simple systems where a single interface is sufficient
  • When the cost of multiple interfaces outweighs the benefits

🌟 Real-World Salesforce Scenarios

  1. Record Processing: Separate interfaces for validation, transformation, and persistence rather than one large record processor interface

  2. API Integrations: Different interfaces for authentication, data retrieval, and data pushing instead of one monolithic API interface

  3. Reporting Services: Segregated interfaces for data extraction, formatting, and delivery rather than forcing all report types to support all operations

📝 Summary

The Interface Segregation Principle ensures that classes only depend on the methods they actually use by creating focused, role-specific interfaces. In Salesforce development, this leads to cleaner, more maintainable code where each class implements only the capabilities it genuinely supports, eliminating forced implementations and runtime exceptions.

D - Dependency Inversion Principle (DIP)

Overview

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions.

The Dependency Inversion Principle is the "D" in SOLID principles and is fundamental to creating flexible, maintainable code. Instead of having your business logic classes directly instantiate and depend on concrete implementations, they should depend on interfaces or abstract classes.

Key concepts include:

  • High-level modules (business logic) should not depend on low-level modules (implementation details)
  • Both should depend on abstractions (interfaces/abstract classes)
  • Dependency Injection is a technique to achieve DIP by injecting dependencies from the outside

📧 Real Life Salesforce Scenario

Your company uses Salesforce to process Orders and needs to send notifications when orders are created. The system should support multiple notification channels and be flexible enough to add new ones without changing existing code.

Current requirements:

  • Send email notifications to customers via Email Service
  • Send SMS notifications for high-priority orders via SMS Service
  • Future: Add Slack notifications, push notifications, etc.

You need to build an OrderProcessor that can handle notifications through different channels without being tightly coupled to specific notification implementations.

❌ Bad Example (Anti-Pattern)

In this approach, the OrderProcessor directly depends on concrete notification classes, violating the Dependency Inversion Principle. The high-level module (OrderProcessor) depends directly on low-level modules (EmailService, SMSService).

🚫 Code Example - Bad Implementation

Email Service Class

public class EmailService_Bad {
public void sendEmail(String recipient, String message) {
// Direct email sending logic
System.debug('Sending email to: ' + recipient);
System.debug('Message: ' + message);
// Simulate email API call
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setToAddresses(new String[]{recipient});
email.setSubject('Order Notification');
email.setPlainTextBody(message);
// Messaging.sendEmail(new Messaging.SingleEmailMessage[]{email});
}
}

SMS Service Class

public class SMSService_Bad {
public void sendSMS(String phoneNumber, String message) {
// Direct SMS sending logic
System.debug('Sending SMS to: ' + phoneNumber);
System.debug('Message: ' + message);
// Simulate SMS API call
HttpRequest req = new HttpRequest();
req.setEndpoint('https://sms-api.com/send');
req.setMethod('POST');
// Http http = new Http().send(req);
}
}

Order Processor Class

public class OrderProcessor_Bad {
private EmailService_Bad emailService;
private SMSService_Bad smsService;
public OrderProcessor_Bad() {
// Tight coupling - creating dependencies inside the class
this.emailService = new EmailService_Bad();
this.smsService = new SMSService_Bad();
}
public void processOrder(Order__c order, String notificationType) {
// Process order logic
System.debug('Processing order: ' + order.Id);
// Notification logic tightly coupled to concrete classes
if (notificationType == 'EMAIL') {
emailService.sendEmail(order.Customer_Email__c, 'Your order has been processed');
} else if (notificationType == 'SMS') {
smsService.sendSMS(order.Customer_Phone__c, 'Your order has been processed');
}
System.debug('Order processed successfully');
}
}

🔧 Usage - Bad Example

// Anonymous Apex execution
OrderProcessor_Bad processor = new OrderProcessor_Bad();
// Create sample order
Order__c testOrder = new Order__c(
Customer_Email__c = '[email protected]',
Customer_Phone__c = '+1234567890'
);
processor.processOrder(testOrder, 'EMAIL');
processor.processOrder(testOrder, 'SMS');

⚠️ Problems with This Approach

Problem Description Impact
Tight Coupling OrderProcessor directly creates and depends on concrete classes Hard to modify or extend notification types
Difficult Testing Cannot easily mock EmailService or SMSService for unit tests Poor testability and test coverage
Violates Open/Closed Must modify OrderProcessor to add new notification types Breaks existing functionality when adding features
Hard to Configure Cannot change notification services at runtime Inflexible system configuration
Code Duplication Similar notification logic repeated for each service type Maintenance overhead and inconsistency

✅ Good Example (Proper Implementation following DIP)

The correct implementation uses interfaces (abstractions) that both high-level and low-level modules depend on. The OrderProcessor depends on the INotificationService interface, not concrete implementations. Dependencies are injected from outside, following the Dependency Injection pattern.

🎯 Code Example - Good Implementation

1️⃣ Notification Service Interface

public interface INotificationService {
void sendNotification(Order__c order, String message);
NotificationType getServiceType();
}

2️⃣ Email Service Implementation

public class EmailService_Good implements INotificationService {
public void sendNotification(Order__c order, String message) {
String recipient = order.Customer_Email__c;
System.debug('Sending email to: ' + recipient);
System.debug('Email message: ' + message);
// Email-specific implementation
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setToAddresses(new String[]{recipient});
email.setSubject('Order Notification');
email.setPlainTextBody(message);
// Messaging.sendEmail(new Messaging.SingleEmailMessage[]{email});
}
public NotificationType getServiceType() {
return NotificationType.EMAIL;
}
}

3️⃣ SMS Service Implementation

public class SMSService_Good implements INotificationService {
public void sendNotification(Order__c order, String message) {
String recipient = order.Customer_Phone__c;
System.debug('Sending SMS to: ' + recipient);
System.debug('SMS message: ' + message);
// SMS-specific implementation
HttpRequest req = new HttpRequest();
req.setEndpoint('https://sms-api.com/send');
req.setMethod('POST');
req.setBody('{"phone":"' + recipient + '","message":"' + message + '"}');
// Http http = new Http().send(req);
}
public NotificationType getServiceType() {
return NotificationType.SMS;
}
}

4️⃣ Order Processor (High-Level Module)

public class OrderProcessor_Good {
private INotificationService notificationService;
// Constructor Injection - Dependencies injected from outside
public OrderProcessor_Good(INotificationService service) {
this.notificationService = service;
}
public void processOrder(Order__c order) {
// Process order logic
System.debug('Processing order: ' + order);
// Find appropriate service based on type
notificationService.sendNotification(order, 'Your order has been processed successfully!');
System.debug('Order processing done!');
}
}

🚀 Usage - Good Example

// Anonymous Apex execution - Dependency Injection in action
// Create notification service implementations
INotificationService emailService = new EmailService_Good();
INotificationService smsService = new SMSService_Good();
// Create sample order
Order__c testOrder = new Order__c(
Customer_Email__c = '[email protected]',
Customer_Phone__c = '+1234567890'
);
// Inject dependencies into OrderProcessor
new OrderProcessor_Good(emailService).processOrder(testOrder);
new OrderProcessor_Good(smsService).processOrder(testOrder);

🎉 Benefits of This Approach

Benefit Description Value
Loose Coupling OrderProcessor depends on interface, not concrete classes Easy to swap implementations
Easy Testing Can inject mock services for unit testing Better test coverage and reliability
Extensibility Add new notification types without changing existing code Follows Open/Closed Principle
Flexibility Can configure different services at runtime Adaptable to changing requirements
Maintainability Changes to notification logic don't affect OrderProcessor Reduced maintenance overhead

✨ Key Benefits

  • Follows SOLID Principles: Especially DIP and Open/Closed Principle
  • Improved Testability: Easy to mock dependencies for unit testing
  • Better Flexibility: Can easily add new notification channels (Slack, Teams, etc.)
  • Reduced Coupling: High-level modules independent of low-level implementation details
  • Runtime Configuration: Can change notification services without code changes
  • Code Reusability: Notification services can be reused across different processors

🎯 When to Use

  • When building service layers that depend on external systems (APIs, databases, email services)
  • When you need to support multiple implementations of the same functionality
  • When creating testable code that requires dependency mocking
  • When building configurable systems that need to swap implementations
  • When working with integrations that may change frequently
  • For any business logic that depends on infrastructure concerns

🚨 When NOT to Use

  • For simple, one-time scripts or utilities with no testing requirements
  • When you're absolutely certain the implementation will never change
  • For very small projects where the overhead doesn't justify the benefits
  • When working with Salesforce standard objects that have fixed APIs
  • For simple data transformations that don't involve external dependencies

🏢 Real-World Salesforce Scenarios

  1. Payment Processing: OrderProcessor depending on IPaymentGateway (Stripe, PayPal, Square) implementations
  2. Data Synchronisation: SyncService depending on IDataRepository (Salesforce, External DB, File System) implementations
  3. Document Generation: ReportGenerator depending on IDocumentService (PDF, Word, Excel) implementations
  4. Lead Assignment: LeadDistributor depending on IAssignmentStrategy (Round-Robin, Territory-Based, Skills-Based) implementations

💡 Summary

The Dependency Inversion Principle, combined with Dependency Injection, creates flexible and maintainable Salesforce applications. By depending on abstractions rather than concrete implementations, your business logic becomes independent of infrastructure concerns, making your code more testable, extensible, and robust. This is especially valuable in Salesforce environments where integrations and business requirements frequently evolve.


Gang of Four Design Patterns in Salesforce Development

Introduction

The Gang of Four (GoF) Design Patterns, introduced by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides in their seminal book "Design Patterns: Elements of Reusable Object-Oriented Software," provide time-tested solutions to common software design problems. For Salesforce developers working with Apex, these patterns offer powerful approaches to writing maintainable, scalable, and robust code within the Salesforce ecosystem.

While Salesforce's platform has its own unique constraints—such as governor limits, the multi-tenant architecture, and specific execution contexts—the fundamental principles of GoF patterns remain highly applicable. These patterns help Salesforce developers tackle common challenges like managing complex business logic, handling data operations efficiently, and creating flexible integrations that can adapt to changing business requirements.

Understanding and applying these patterns in your Apex code will not only improve your code quality but also make your solutions more maintainable and easier for other developers to understand and extend.

The Three Categories of Design Patterns

The 23 GoF patterns are organized into three distinct categories based on their primary purpose:

1️⃣ Creational Patterns

Purpose: Deal with object creation mechanisms, trying to create objects in a manner suitable to the situation while hiding the creation logic and making the system independent of how objects are created, composed, and represented.

These patterns are particularly valuable in Salesforce development when you need to control how objects are instantiated, especially when dealing with complex business logic, service classes, or when you want to ensure certain constraints are met during object creation within governor limits.

Patterns in this category:

  • Abstract Factory
  • Builder
  • Factory Method
  • Prototype
  • Singleton

2️⃣ Structural Patterns

Purpose: Deal with object composition and typically identify simple ways to realize relationships between different objects.

In Salesforce development, structural patterns help you compose classes and objects to form larger structures while keeping these structures flexible and efficient. They're especially useful when working with complex data models, integrations, or when you need to adapt existing code to work with new requirements without violating platform constraints.

Patterns in this category:

  • Adapter
  • Bridge
  • Composite
  • Decorator
  • Facade
  • Flyweight
  • Proxy

3️⃣ Behavioral Patterns

Purpose: Focus on communication between objects and the assignment of responsibilities between objects to accomplish specific tasks.

Behavioral patterns are crucial in Salesforce development for managing complex business processes, handling different execution contexts (triggers, batch jobs, queueable jobs), and creating flexible workflows that can adapt to different business scenarios while respecting platform limitations.

Patterns in this category:

  • Chain of Responsibility
  • Command
  • Interpreter
  • Iterator
  • Mediator
  • Memento
  • Observer
  • State
  • Strategy
  • Template Method
  • Visitor

❓Why Design Patterns Matter in Salesforce Development

Salesforce development presents unique challenges that make design patterns particularly valuable:

Code Maintainability

  • Team Collaboration: Multiple developers working on the same codebase
  • Long-term Maintenance: Code that will be maintained and extended over years
  • Testing Requirements: Comprehensive test coverage is essential for deployment

Business Complexity

  • Custom Business Logic: Complex business requirements need well-structured solutions
  • Integration Requirements: Multiple system integrations require flexible architectures
  • Scalability: Solutions must scale with growing data volumes and user bases

Platform Constraints

  • Governor Limits: Apex has strict execution limits that require efficient code design
  • Bulkification: Code must handle multiple records efficiently
  • Resource Management: Memory and CPU usage must be carefully managed

✅Getting Started with Design Patterns in Apex

When implementing design patterns in Salesforce, consider these key principles:

  1. Understand the Problem First: Don't implement patterns for the sake of it; identify the specific problem you're trying to solve.
  2. Consider Salesforce Best Practices: Ensure your pattern implementation follows Salesforce coding standards and bulkification principles.
  3. Plan for Governor Limits: Design your patterns with Salesforce's execution limits in mind.
  4. Test Thoroughly: Implement comprehensive test classes that verify both the pattern implementation and business logic.
  5. Document Your Approach: Clearly document why you chose specific patterns and how they should be used by other developers.

💡Conclusion

The Gang of Four design patterns provide a solid foundation for building robust, maintainable Salesforce applications. By understanding these three categories—Creational, Structural, and Behavioral—you can choose the right pattern for your specific development challenges.

As you progress through learning these patterns, remember that they are tools to solve problems, not solutions looking for problems. The key to successful pattern implementation in Salesforce is understanding both the pattern's intent and how it can be adapted to work effectively within the Salesforce platform's unique constraints and capabilities.

In the following sections, we'll dive deep into specific patterns, providing detailed Apex implementations and real-world Salesforce use cases for each pattern type.

About

Explore "Gang of Four" (GoF) Design Patterns in Object Oriented Software explained in detail with Salesforce related examples to build high-quality, reusable, loosely-coupled and maintainable Software on Salesforce Platform

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages