Introduction
When starting your journey with Salesforce development, understanding Apex design patterns can set you apart as a professional developer. These patterns are tried-and-tested solutions to common software challenges, making your code more structured, scalable, and maintainable. In this guide, we’ll break down popular Apex design patterns for beginners.
What Are Design Patterns?
Design patterns are reusable solutions to recurring problems in software design. In Salesforce, they help you structure your Apex code to handle complex requirements efficiently while adhering to best practices.
They are grouped into three main types based on their purpose:
- Creational Patterns: Focus on how objects are created, making it flexible and reusable. For example, the Singleton Pattern ensures only one instance of a class exists. The Factory Pattern creates objects without exposing the exact class.
- Structural Patterns: Help organize objects and classes into larger structures, improving flexibility and making it easier to add features. Examples include the Adapter Pattern, which connects incompatible classes, and the Decorator Pattern, which adds new features dynamically.
- Behavioral Patterns: Manage how objects communicate and share tasks. For instance, the Observer Pattern updates multiple objects when one changes, and the Strategy Pattern allows changing behavior at runtime.
Why Use Design Patterns in Apex?
- Reusability: Write once, use often. Patterns provide a blueprint for solving similar problems across projects.
- Maintainability: Modular code is easier to update and debug.
- Scalability: Design patterns help your code adapt to growing business needs without major overhauls.
Common Apex Design Patterns
1. Singleton Pattern
The Singleton pattern is a creational design pattern that ensures a class has only one instance throughout a specific context. It is particularly useful when managing shared resources or configurations across an application.
Example Problem Scenario :
In scenarios such as retrieving application settings, creating multiple instances of a utility class can lead to unnecessary database queries. This can also increase memory usage. Additionally, it may cause possible breaches of Salesforce governor limits.
Let’s explore this through the initial implementation:
The ConfigurationUtility class is intended to centralize the management of configuration settings, typically stored in a custom object (e.g., AppConfig__c). This is useful when application-level settings need to be accessed consistently. These settings can include API keys, feature toggles, or default values. They must be consistently accessed across multiple parts of an application.
Without Singleton Pattern :
public class ConfigurationUtility {
private Map<String, String> configSettings;
public ConfigurationUtility() {
configSettings = new Map<String, String>();
List<AppConfig__c> settings = [SELECT Name, Value__c FROM AppConfig__c];
for (AppConfig__c setting : settings) {
configSettings.put(setting.Name, setting.Value__c);
}
}
// Method to retrieve a configuration value by key
public String getConfigValue(String key) {
return configSettings.containsKey(key) ? configSettings.get(key) : null;
}
}
Calling the ConfigurationUtility in Main Class :
public class AppHandler {
public void useSingletonExample1(String key) {
ConfigurationUtility utils = new ConfigurationUtility();
utils.getConfigValue(key);
}
public void useSingletonExample2(String key) {
ConfigurationUtility utils = new ConfigurationUtility();
utils.getConfigValue(key);
}
}
Running via anonymous window :
AppHandler objAppHandler = new AppHandler();
objAppHandler.useSingletonExample1('Test1');
objAppHandler.useSingletonExample2('Test2');

Issues with this implementation :
- Multiple instances can be created inadvertently.
- Each instance queries the database, leading to wasted SOQL calls.
The Solution: Singleton Pattern
By implementing the Singleton pattern, we ensure that only one instance of the class exists. This avoids redundant database queries and optimizes resource usage.
public class ConfigurationUtility {
private static ConfigurationUtility instance;
private Map<String, String> configSettings;
// Private constructor to prevent instantiation
private ConfigurationUtility() {
// Query custom settings or metadata records and populate the map
configSettings = new Map<String, String>();
List<AppConfig__c> settings = [SELECT Name, Value__c FROM AppConfig__c];
for (AppConfig__c setting : settings) {
configSettings.put(setting.Name, setting.Value__c);
}
}
// Public method to get the Singleton instance
public static ConfigurationUtility getInstance() {
if (instance == null) {
instance = new ConfigurationUtility();
}
return instance;
}
// Method to retrieve a configuration value by key
public String getConfigValue(String key) {
return configSettings.containsKey(key) ? configSettings.get(key) : null;
}
}
Calling the ConfigurationUtility in Main Class :
public class AppHandler {
public void useSingletonExample1(String key) {
ConfigurationUtility utils = ConfigurationUtility.getInstance();
utils.getConfigValue(key);
}
public void useSingletonExample2(String key) {
ConfigurationUtility utils = ConfigurationUtility.getInstance();
utils.getConfigValue(key);
}
}
Running via anonymous window :
AppHandler objAppHandler = new AppHandler();
objAppHandler.useSingletonExample1('Test1');
objAppHandler.useSingletonExample2('Test2');

Advantages of This Implementation :
- Single Instance: Ensures only one instance of ConfigurationManager exists throughout the transaction.
- Lazy Initialization: Ensures the instance is created only when needed, reducing unnecessary memory usage.
- No Redundancy: Multiple calls to getInstance return the same object, avoiding duplicate SOQL queries.
- Governor-Limit Friendly: Reduces database queries and helps avoid hitting Salesforce governor limits.
Thread Safe Implementation Example :
public class ConfigurationUtility {
public static final ConfigurationUtility INSTANCE = new ConfigurationUtility();
private Map<String, String> configSettings;
// Private constructor to prevent instantiation
private ConfigurationUtility() {
// Query custom settings or metadata records and populate the map
configSettings = new Map<String, String>();
List<AppConfig__c> settings = [SELECT Name, Value__c FROM AppConfig__c];
for (AppConfig__c setting : settings) {
configSettings.put(setting.Name, setting.Value__c);
}
}
// Method to retrieve a configuration value by key
public String getConfigValue(String key) {
return configSettings.containsKey(key) ? configSettings.get(key) : null;
}
}
Calling the Singleton Pattern In Main Class :
public class AppHandler {
public void useSingletonExample1(String key) {
ConfigurationUtility utils = ConfigurationUtility.INSTANCE;
utils.getConfigValue(key);
}
public void useSingletonExample2(String key) {
ConfigurationUtility utils = ConfigurationUtility.INSTANCE;
utils.getConfigValue(key);
}
}
Running via anonymous window :
AppHandler objAppHandler = new AppHandler();
objAppHandler.useSingletonExample1('Test1');
objAppHandler.useSingletonExample2('Test2');

Code Explanation:
- Eager Initialization:
- The
INSTANCEis initialized when the class is loaded, ensuring that only one instance is created. - The
finalkeyword makesINSTANCEimmutable, ensuring thread safety.
- The
- Private Constructor:
- Prevents external instantiation, ensuring all consumers access the single instance via
INSTANCE.
- Prevents external instantiation, ensuring all consumers access the single instance via
- Simplified Access:
- No need for a
getInstance()method since theINSTANCEis directly accessible as a public static final variable.
- No need for a
Some of the Use Cases for Singleton Pattern in Salesforce :
- Configuration Management: Simplifies access to API keys, authentication credentials, and app settings by loading them once during the application lifecycle.
- Governor Limit Tracking: Centralizes monitoring of resource usage, helping developers stay within Salesforce governor limits.
- Custom Logging: Provides a single instance for consistent log handling and storage across the application.
- Shared Utilities: Encapsulates common functions like data formatting or encryption for reuse across components.
References:
2. Strategy Pattern
The Strategy Pattern is a behavioral design pattern. It is used when multiple solutions exist for a problem. The appropriate solution needs to be chosen dynamically at runtime. It allows you to encapsulate algorithms or behaviors into separate classes, making them interchangeable and easier to manage.
Example Problem Scenario :
In scenarios where application functionality depends on dynamic, interchangeable behaviors (e.g., payment processing, discount calculations, or data validations or other processes), hard-coding the logic into a single class can result in tightly coupled, inflexible, and error-prone systems. This is particularly problematic when business requirements evolve, necessitating frequent changes or extensions to the logic.
Let’s explore this through the initial implementation:
The TaxCalculatorWithoutStrategy class is designed to centralize the tax calculation logic based on regions. This centralization makes it easier to manage simple tax rules within a single class. This approach is suitable for scenarios where the number of regions is limited, and tax rules are relatively straightforward. All logic is included within one class. This design provides a single point of reference for tax calculations. It eliminates the need for additional dependencies or configurations.
Without Strategy Pattern:
public class TaxCalculatorWithoutStrategy {
public Decimal calculateTax(String region, Decimal amount) {
if (region == 'USA') {
return calculateUSATax(amount);
} else if (region == 'Canada') {
return calculateCanadaTax(amount);
} else {
return 0; // No tax
}
}
private Decimal calculateUSATax(Decimal amount) {
return amount * 0.07; // 7% tax
}
private Decimal calculateCanadaTax(Decimal amount) {
return amount * 0.05; // 5% tax
}
}
Running via anonymous window:
Decimal amount = 10000;
TaxCalculatorWithoutStrategy calculator = new TaxCalculatorWithoutStrategy();
System.debug('Tax in USA: ' + calculator.calculateTax('USA', amount));
System.debug('Tax in Canada: ' + calculator.calculateTax('Canada', amount));
Issues with this implementation:
- Tax calculation logic is tightly coupled within the class, making it harder to extend or modify.
- Adding a new region requires modifying the existing class with less scalability.
- The class needs to be changed, adding a new if/else statement every time a new region is added.
- Testing individual tax calculations requires working with the entire
TaxCalculatorWithoutStrategyclass.
The Solution: Strategy Pattern
By implementing the Strategy pattern, we encapsulate the tax calculation logic for each region into separate classes. Each region’s specific tax logic is defined in its own class, adhering to the Open/Closed Principle. The context class (TaxCalculatorWithStrategy) uses Dependency Injection to dynamically select the appropriate strategy at runtime, promoting flexibility and maintainability.
Code Explanation:
1. Strategy Interface
Defines a common interface for all tax calculation strategies.
public interface TaxCalculationStrategy {
Decimal calculateTax(Decimal amount);
}
2. Concrete Strategies
Each region’s tax logic is encapsulated in a separate class.
USA Tax Strategy
public class USATaxStrategy implements TaxCalculationStrategy {
public Decimal calculateTax(Decimal amount) {
return amount * 0.07; // 7% tax
}
}
Canada Tax Strategy
public class CanadaTaxStrategy implements TaxCalculationStrategy {
public Decimal calculateTax(Decimal amount) {
return amount * 0.05; // 5% tax
}
}
3. Context Class
The TaxCalculatorWithStrategy class is responsible for delegating the tax calculation to the appropriate strategy. It uses Dependency Injection to accept the strategy dynamically.
public class TaxCalculatorWithStrategy {
private TaxCalculationStrategy taxStrategy;
// Dependency Injection via Constructor
public TaxCalculatorWithStrategy(TaxCalculationStrategy strategy) {
this.taxStrategy = strategy;
}
public Decimal calculate(Decimal amount) {
return taxStrategy.calculateTax(amount);
}
}
Running via anonymous window:
Decimal amount = 10000;
// Inject USA Tax Strategy
TaxCalculatorWithStrategy usaCalculator = new TaxCalculatorWithStrategy(new USATaxStrategy());
System.debug('Tax in USA: ' + usaCalculator.calculate(amount));
// Inject Canada Tax Strategy
TaxCalculatorWithStrategy canadaCalculator = new TaxCalculatorWithStrategy(new CanadaTaxStrategy());
System.debug('Tax in Canada: ' + canadaCalculator.calculate(amount));
Advantages of This Implementation :
- Flexibility: The tax calculation logic is dynamically selected, allowing easy modification or addition of new strategies without altering existing code.
- Separation of Concerns: Each strategy encapsulates a single responsibility, making the codebase more modular and maintainable.
- Adherence to SOLID Principles:
- Open/Closed Principle: Adding new strategies does not require changes to the core TaxCalculatorWithStrategy class.
- Single Responsibility Principle: Each strategy handles only its specific tax logic.
- Testability: Each strategy can be independently tested, ensuring better code reliability.
- Reusability: The strategies can be reused in different contexts without duplication of logic.
Some of the Use Cases for Strategy Pattern in Salesforce
- Payment Gateways: Encapsulate logic for PayPal, Stripe, or Authorize.Net into separate strategies.
- Discount Calculations: Handle promotional, volume-based, or seasonal discounts dynamically.
- Integrations: Manage API calls to multiple systems with specific strategies.
References:
3. Factory Pattern
The Factory Design Pattern is a creational design pattern that simplifies object creation by centralizing the logic for instantiating objects. It provides flexibility and eliminates the need for tightly coupled code by encapsulating object creation in a dedicated factory class.
Example Problem Scenario :
In scenarios where dynamic object creation is required, such as initializing different types of payment processors, we face challenges. These challenges may include workflows or tax calculators based on user input or conditions. Directly instantiating objects can lead to code duplication. It can also cause tight coupling and complexity.
Let’s explore this through the initial implementation:
Let’s consider the strategy pattern example in the TaxCalculatorWithoutFactory class, it hard-codes object creation logic for different regions. While it works for simple scenarios, it becomes cumbersome when new regions or tax rules are added.
Without Factory Pattern:
public class TaxCalculatorWithoutFactory {
private TaxCalculationStrategy taxStrategy;
// Constructor to inject the appropriate strategy
public TaxCalculatorWithoutFactory(TaxCalculationStrategy strategy) {
this.taxStrategy = strategy;
}
public Decimal calculateTax(Decimal amount) {
return taxStrategy.calculateTax(amount);
}
}
Running via anonymous window :
Decimal amount = 10000;
// Manually deciding which strategy to use
TaxCalculatorWithoutFactory usaCalculator = new TaxCalculatorWithoutFactory(new USATaxStrategy());
System.debug('Tax in USA: ' + usaCalculator.calculateTax(amount));
TaxCalculatorWithoutFactory canadaCalculator = new TaxCalculatorWithoutFactory(new CanadaTaxStrategy());
System.debug('Tax in Canada: ' + canadaCalculator.calculateTax(amount));
Issues with This Implementation :
- Tightly Coupled Logic: The class depends on specific implementations.
- Scalability Challenges: Adding new regions requires modifying the existing logic.
- Code Duplication: Object creation logic is repeated whenever a tax strategy is needed.
- Difficult Maintenance: Changes in one part of the logic can impact others.
The Solution: Factory Pattern
By implementing the Factory pattern, we encapsulate the object creation logic into a dedicated factory class (TaxStrategyFactory). This approach decouples client code from specific strategy implementations. It adheres to the Open/Closed Principle for extensibility. It also centralizes creation logic for maintainability and scalability.
With Factory Pattern:
1. Strategy Interface and Concrete Strategies
public interface TaxCalculationStrategy {
Decimal calculateTax(Decimal amount);
}
public class USATaxStrategy implements TaxCalculationStrategy {
public Decimal calculateTax(Decimal amount) {
return amount * 0.07; // 7% tax
}
}
public class CanadaTaxStrategy implements TaxCalculationStrategy {
public Decimal calculateTax(Decimal amount) {
return amount * 0.05; // 5% tax
}
}
2. Factory Class
public class TaxCalculatorWithFactory {
public static TaxCalculationStrategy getTaxStrategy(String region) {
if (region == 'USA') {
return new USATaxStrategy();
} else if (region == 'Canada') {
return new CanadaTaxStrategy();
} else {
throw new IllegalArgumentException('Unsupported region');
}
}
}
Running via anonymous window:
Decimal amount = 10000;
String region = 'USA';
TaxCalculationStrategy strategy = TaxCalculatorWithFactory.getTaxStrategy(region);
System.debug('Tax in '+region+': ' + strategy.calculateTax(amount));
Code Explanation:
- TaxCalculationStrategy Interface: Defines the contract for all tax strategies.
- Concrete Strategies: Each region’s tax logic is implemented in separate classes.
- TaxStrategyFactory: Encapsulates object creation logic and dynamically provides the appropriate strategy.
Advantages of This Implementation :
- Centralized Object Creation: The factory class centralizes object instantiation, reducing duplication.
- Decoupling: The client code (TaxCalculatorWithFactory) is decoupled from specific implementations.
- Scalability: Adding new strategies (e.g., EUTaxStrategy) does not require changes to client code; only the factory needs an update.
Some of the Use Cases for Factory Pattern in Salesforce :
- Dynamic Record Creation: It simplifies the creation of records like Leads, Opportunities, or Cases. This is based on specific conditions. It avoids complex conditional logic.
- Custom Applications: Ensures a consistent approach for creating user-defined components or object structures in custom frameworks.
- Integration with External Systems: Dynamically creates objects for data mapping and processing during integrations, improving scalability and adaptability.
- Switching Implementations: Easily selects and instantiates varying implementations, such as tax calculations or discount rules, without hardcoding logic.
4. Facade Pattern
This pattern simplifies complex systems by providing a unified interface.
Examples to be continued…
5. Decorator Pattern
The decorator pattern allows the introduction of new temporary fields for processing without altering the object structure
Examples to be continued…
How to Start Using Design Patterns
- Understand the Problem: Choose a pattern that matches your specific requirements.
- Keep It Simple: Avoid over-engineering. Use patterns only when they add value.
- Test Your Code: Always write test cases to ensure your implementation works as intended.


Leave a comment