Salesforce Design Patterns: Comprehensive Guide

Introduction

Design patterns are proven solutions to recurring software design problems. In Salesforce development, implementing appropriate design patterns helps create maintainable, scalable, and robust applications. This guide covers the most important design patterns used in Salesforce development, their implementations, benefits, and specific use cases.

MVC Pattern

Overview

The Model-View-Controller (MVC) pattern separates an application into three interconnected components, providing a clear separation of concerns.

Components

  • Model: Represents the data and business logic
  • View: Represents the user interface
  • Controller: Handles user input and updates the model and view accordingly

Salesforce Implementation

  • Model: Apex classes representing data structures and business logic
  • View: Visualforce pages or Lightning Web Components
  • Controller: Apex controllers or JavaScript controllers for LWC

Example

// Model
public class AccountModel {
    private Account acc;
    
    public AccountModel(Account acc) {
        this.acc = acc;
    }
    
    public String getAccountName() {
        return acc.Name;
    }
    
    public void updateAccountName(String name) {
        acc.Name = name;
    }
}

// Controller
public class AccountController {
    private AccountModel model;
    
    public AccountController() {
        this.model = new AccountModel([SELECT Id, Name FROM Account LIMIT 1]);
    }
    
    public String getAccountName() {
        return model.getAccountName();
    }
    
    public void saveAccount(String name) {
        model.updateAccountName(name);
    }
}
<!-- View (Visualforce) -->
<apex:page controller="AccountController">
    <apex:form>
        <apex:outputLabel value="Account Name: {!accountName}" />
        <apex:inputText value="{!accountName}" />
        <apex:commandButton value="Save" action="{!saveAccount}" />
    </apex:form>
</apex:page>

Benefits

  • Clear separation of concerns
  • Improved code organization and maintainability
  • Easier testing and debugging
  • Reusability of components

Factory Pattern

Overview

The Factory pattern provides an interface for creating objects without specifying their concrete classes, delegating instantiation to subclasses.

Components

  • Factory Interface/Class: Defines the factory method
  • Concrete Factories: Implement the factory method
  • Product Interface: Common interface for products
  • Concrete Products: Specific implementations of products

Salesforce Implementation

// Product Interface
public interface NotificationService {
    void sendNotification(String message, String recipient);
}

// Concrete Products
public class EmailNotification implements NotificationService {
    public void sendNotification(String message, String recipient) {
        // Email implementation
    }
}

public class SMSNotification implements NotificationService {
    public void sendNotification(String message, String recipient) {
        // SMS implementation
    }
}

// Factory Class
public class NotificationFactory {
    public static NotificationService createNotificationService(String type) {
        if (type == 'Email') {
            return new EmailNotification();
        } else if (type == 'SMS') {
            return new SMSNotification();
        } else {
            throw new UnsupportedOperationException('Notification type not supported');
        }
    }
}

// Client Usage
NotificationService service = NotificationFactory.createNotificationService('Email');
service.sendNotification('Hello', '[email protected]');

Benefits

  • Encapsulates object creation logic
  • Promotes loose coupling
  • Makes code more maintainable and adaptable to change
  • Supports dependency inversion principle

Singleton Pattern

Overview

The Singleton pattern ensures a class has only one instance and provides a global point of access to it.

Components

  • Private Constructor: Prevents direct instantiation
  • Static Instance: The single instance of the class
  • Static Access Method: Returns the instance

Salesforce Implementation

public class Logger {
    // Private static instance variable
    private static Logger instance;
    
    // Private constructor to prevent direct instantiation
    private Logger() {
        // Initialization code
    }
    
    // Public static method to get the instance
    public static Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return instance;
    }
    
    // Logger methods
    public void log(String message) {
        System.debug(message);
    }
}

// Usage
Logger logger = Logger.getInstance();
logger.log('This is a log message');

Benefits

  • Controlled access to the sole instance
  • Reduced memory footprint
  • Global access point
  • Prevents multiple instantiations that could cause inconsistencies

Observer Pattern

Overview

The Observer pattern defines a one-to-many dependency between objects, so when one object changes state, all its dependents are notified and updated automatically.

Components

  • Subject/Observable: Maintains a list of observers and notifies them
  • Observer: Interface defining update method
  • Concrete Observers: Classes that respond to notifications

Salesforce Implementation

// Observer Interface
public interface Observer {
    void update(String message);
}

// Concrete Observer
public class CustomerObserver implements Observer {
    public void update(String message) {
        System.debug('Customer notified: ' + message);
    }
}

// Subject
public class OrderSubject {
    private List<Observer> observers = new List<Observer>();
    
    public void attach(Observer observer) {
        observers.add(observer);
    }
    
    public void detach(Observer observer) {
        observers.remove(observers.indexOf(observer));
    }
    
    public void notifyObservers(String message) {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }
    
    public void processOrder(String orderNumber) {
        // Process order logic
        notifyObservers('Order ' + orderNumber + ' has been processed');
    }
}

// Usage
OrderSubject orderSubject = new OrderSubject();
Observer customerObserver = new CustomerObserver();
orderSubject.attach(customerObserver);
orderSubject.processOrder('12345');

Benefits

  • Loose coupling between objects
  • Support for broadcast communication
  • Dynamic relationships between objects
  • Enables event-driven architecture

Strategy Pattern

Overview

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable, allowing the algorithm to vary independently from clients using it.

Components

  • Strategy Interface: Defines a common interface for all algorithms
  • Concrete Strategies: Specific algorithm implementations
  • Context: Uses a Strategy to execute an algorithm

Salesforce Implementation

// Strategy Interface
public interface PricingStrategy {
    Decimal calculatePrice(Decimal basePrice);
}

// Concrete Strategies
public class RegularPricingStrategy implements PricingStrategy {
    public Decimal calculatePrice(Decimal basePrice) {
        return basePrice;
    }
}

public class DiscountPricingStrategy implements PricingStrategy {
    private Decimal discountPercent;
    
    public DiscountPricingStrategy(Decimal discountPercent) {
        this.discountPercent = discountPercent;
    }
    
    public Decimal calculatePrice(Decimal basePrice) {
        return basePrice * (1 - discountPercent / 100);
    }
}

// Context
public class PriceCalculator {
    private PricingStrategy strategy;
    
    public void setStrategy(PricingStrategy strategy) {
        this.strategy = strategy;
    }
    
    public Decimal calculateFinalPrice(Decimal basePrice) {
        return strategy.calculatePrice(basePrice);
    }
}

// Usage
PriceCalculator calculator = new PriceCalculator();
calculator.setStrategy(new RegularPricingStrategy());
Decimal regularPrice = calculator.calculateFinalPrice(100);

calculator.setStrategy(new DiscountPricingStrategy(10)); // 10% discount
Decimal discountedPrice = calculator.calculateFinalPrice(100);

Benefits

  • Algorithms can be switched at runtime
  • Isolates algorithm implementation from the code that uses it
  • Eliminates conditional statements
  • Promotes the open/closed principle

Composite Pattern

Overview

The Composite pattern lets you compose objects into tree structures to represent part-whole hierarchies, allowing clients to treat individual objects and compositions uniformly.

Components

  • Component: Interface for all objects in the composition
  • Leaf: Individual objects with no children
  • Composite: Contains child components

Salesforce Implementation

// Component
public interface OrgStructure {
    void display(Integer depth);
    Decimal calculateBudget();
}

// Leaf
public class Department implements OrgStructure {
    private String name;
    private Decimal budget;
    
    public Department(String name, Decimal budget) {
        this.name = name;
        this.budget = budget;
    }
    
    public void display(Integer depth) {
        String indent = '';
        for (Integer i = 0; i < depth; i++) {
            indent += '--';
        }
        System.debug(indent + name);
    }
    
    public Decimal calculateBudget() {
        return budget;
    }
}

// Composite
public class Organization implements OrgStructure {
    private String name;
    private List<OrgStructure> children = new List<OrgStructure>();
    
    public Organization(String name) {
        this.name = name;
    }
    
    public void add(OrgStructure component) {
        children.add(component);
    }
    
    public void remove(OrgStructure component) {
        children.remove(children.indexOf(component));
    }
    
    public void display(Integer depth) {
        String indent = '';
        for (Integer i = 0; i < depth; i++) {
            indent += '--';
        }
        System.debug(indent + name);
        
        for (OrgStructure child : children) {
            child.display(depth + 1);
        }
    }
    
    public Decimal calculateBudget() {
        Decimal totalBudget = 0;
        for (OrgStructure child : children) {
            totalBudget += child.calculateBudget();
        }
        return totalBudget;
    }
}

// Usage
Organization company = new Organization('Acme Corp');
Organization sales = new Organization('Sales Division');
Organization marketing = new Organization('Marketing Division');

sales.add(new Department('Enterprise Sales', 1000000));
sales.add(new Department('SMB Sales', 500000));
marketing.add(new Department('Digital Marketing', 250000));

company.add(sales);
company.add(marketing);
company.add(new Department('R&D', 2000000));

company.display(0);
System.debug('Total Budget: ' + company.calculateBudget());

Benefits

  • Works with complex hierarchies
  • Treats individual objects and compositions uniformly
  • Makes it easier to add new component types
  • Simplifies client code

Service Layer Pattern

Overview

The Service Layer pattern defines an application’s boundary and its set of available operations from the perspective of interfacing client layers.

Components

  • Service Interface: Defines operations available to clients
  • Service Implementation: Implements the operations
  • Domain Layer: Business logic and entities

Salesforce Implementation

// Service Interface
public interface AccountService {
    List<Account> getAccounts();
    Account getAccountById(Id accountId);
    void saveAccount(Account account);
}

// Service Implementation
public class AccountServiceImpl implements AccountService {
    public List<Account> getAccounts() {
        return [SELECT Id, Name, Industry FROM Account LIMIT 100];
    }
    
    public Account getAccountById(Id accountId) {
        return [SELECT Id, Name, Industry FROM Account WHERE Id = :accountId];
    }
    
    public void saveAccount(Account account) {
        upsert account;
    }
}

// Usage in Controller
public class AccountController {
    private AccountService service;
    
    public AccountController() {
        this.service = new AccountServiceImpl();
    }
    
    public List<Account> getAccounts() {
        return service.getAccounts();
    }
}

Benefits

  • Defines clear application boundary
  • Improves testability through interface-based design
  • Reduces duplication of business logic
  • Simplifies complex operations

Repository Pattern

Overview

The Repository pattern mediates between the domain and data mapping layers, acting like an in-memory collection of domain objects.

Components

  • Repository Interface: Defines CRUD operations
  • Repository Implementation: Implements operations against data store
  • Domain Entity: The entity being persisted

Salesforce Implementation

// Repository Interface
public interface ContactRepository {
    List<Contact> getByAccountId(Id accountId);
    Contact getById(Id contactId);
    void save(Contact contact);
    void delete(Id contactId);
}

// Repository Implementation
public class ContactRepositoryImpl implements ContactRepository {
    public List<Contact> getByAccountId(Id accountId) {
        return [SELECT Id, FirstName, LastName, Email FROM Contact 
                WHERE AccountId = :accountId];
    }
    
    public Contact getById(Id contactId) {
        return [SELECT Id, FirstName, LastName, Email, AccountId 
                FROM Contact WHERE Id = :contactId];
    }
    
    public void save(Contact contact) {
        upsert contact;
    }
    
    public void delete(Id contactId) {
        delete [SELECT Id FROM Contact WHERE Id = :contactId];
    }
}

// Usage in Service
public class ContactService {
    private ContactRepository repository;
    
    public ContactService() {
        this.repository = new ContactRepositoryImpl();
    }
    
    public List<Contact> getContactsByAccount(Id accountId) {
        return repository.getByAccountId(accountId);
    }
}

Benefits

  • Centralizes data access logic
  • Provides a consistent interface for data operations
  • Simplifies unit testing with mock repositories
  • Decouples application from data storage details

Selector Pattern

Overview

The Selector pattern centralizes SOQL queries to provide a single point of access for retrieving records.

Components

  • Selector Class: Contains methods for retrieving data

Salesforce Implementation

public class AccountSelector {
    public static List<Account> getAccountsByIds(Set<Id> accountIds) {
        return [SELECT Id, Name, Industry, BillingCity, BillingCountry
                FROM Account 
                WHERE Id IN :accountIds];
    }
    
    public static List<Account> getAccountsWithContacts() {
        return [SELECT Id, Name, 
                (SELECT Id, FirstName, LastName FROM Contacts)
                FROM Account 
                WHERE Id IN (SELECT AccountId FROM Contact)];
    }
    
    public static List<Account> getAccountsByIndustry(String industry) {
        return [SELECT Id, Name, Industry 
                FROM Account 
                WHERE Industry = :industry];
    }
}

// Usage
List<Account> techAccounts = AccountSelector.getAccountsByIndustry('Technology');

Benefits

  • Centralizes query logic
  • Prevents duplication of queries
  • Simplifies query maintenance
  • Improves field selection consistency

Domain Layer Pattern

Overview

The Domain Layer pattern encapsulates business logic related to a specific domain entity.

Components

  • Domain Class: Contains business logic for the entity

Salesforce Implementation

public class AccountDomain {
    private List<Account> accounts;
    
    public AccountDomain(List<Account> accounts) {
        this.accounts = accounts;
    }
    
    public void applyDiscount(Decimal discountPercent) {
        for (Account acc : accounts) {
            // Apply discount logic to accounts
            applyDiscountToOpportunities(acc.Id, discountPercent);
        }
    }
    
    private void applyDiscountToOpportunities(Id accountId, Decimal discountPercent) {
        List<Opportunity> opps = [SELECT Id, Amount FROM Opportunity WHERE AccountId = :accountId];
        for (Opportunity opp : opps) {
            opp.Amount = opp.Amount * (1 - discountPercent / 100);
        }
        update opps;
    }
    
    public void assignTerritories() {
        // Territory assignment logic
    }
}

// Usage
List<Account> accounts = [SELECT Id FROM Account WHERE Industry = 'Retail'];
AccountDomain domain = new AccountDomain(accounts);
domain.applyDiscount(10);

Benefits

  • Centralizes business logic related to domain objects
  • Improves maintainability and Reusability
  • Separates business logic from data access
  • Creates a rich domain model

Enterprise Application Architecture

Salesforce applications can leverage a combination of patterns to create a comprehensive architecture. A common approach is:

Layers

  1. Presentation Layer: Visualforce pages, Lightning components
  2. Controller Layer: Apex controllers, JavaScript controllers
  3. Service Layer: Service interfaces and implementations
  4. Domain Layer: Business logic encapsulation
  5. Selector Layer: Data access and queries
  6. Trigger Framework: Consistent trigger handling

Implementation Example

// Trigger
trigger AccountTrigger on Account (before insert, before update) {
    AccountTriggerHandler handler = new AccountTriggerHandler();
    
    if (Trigger.isBefore) {
        if (Trigger.isInsert) {
            handler.onBeforeInsert(Trigger.new);
        }
        else if (Trigger.isUpdate) {
            handler.onBeforeUpdate(Trigger.new, Trigger.oldMap);
        }
    }
}

// Trigger Handler
public class AccountTriggerHandler {
    private AccountService service;
    
    public AccountTriggerHandler() {
        this.service = new AccountServiceImpl();
    }
    
    public void onBeforeInsert(List<Account> newAccounts) {
        service.validateAccounts(newAccounts);
        service.enrichAccounts(newAccounts);
    }
    
    public void onBeforeUpdate(List<Account> newAccounts, Map<Id, Account> oldAccountMap) {
        service.validateUpdates(newAccounts, oldAccountMap);
    }
}

// Service Layer
public class AccountServiceImpl implements AccountService {
    private AccountDomain domain;
    private AccountSelector selector;
    
    public AccountServiceImpl() {
        this.selector = new AccountSelector();
    }
    
    public void validateAccounts(List<Account> accounts) {
        domain = new AccountDomain(accounts);
        domain.validate();
    }
    
    public void enrichAccounts(List<Account> accounts) {
        domain = new AccountDomain(accounts);
        domain.enrichData();
    }
}

// Domain Layer
public class AccountDomain {
    private List<Account> accounts;
    
    public AccountDomain(List<Account> accounts) {
        this.accounts = accounts;
    }
    
    public void validate() {
        // Validation logic
    }
    
    public void enrichData() {
        // Data enrichment logic
    }
}

// Selector Layer
public class AccountSelector {
    public List<Account> getAccountsWithRelatedData(Set<Id> accountIds) {
        return [SELECT Id, Name, Industry,
                (SELECT Id FROM Contacts),
                (SELECT Id FROM Opportunities)
                FROM Account
                WHERE Id IN :accountIds];
    }
}

Best Practices

When to Use Each Pattern

  • MVC Pattern: For user interfaces and interactions
  • Factory Pattern: When object creation is complex or should be abstracted
  • Singleton Pattern: For shared resources like logging or configuration
  • Observer Pattern: For event-driven scenarios and notifications
  • Strategy Pattern: When multiple algorithms need to be interchangeable
  • Composite Pattern: For hierarchical data structures
  • Service Layer: To define application boundaries and operations
  • Repository Pattern: To abstract data access
  • Selector Pattern: To centralize and reuse queries
  • Domain Layer: To encapsulate business logic

Implementation Guidelines

  1. Start Simple: Don’t over-engineer for small applications
  2. Consider Maintenance: Choose patterns that make maintenance easier
  3. Document Your Architecture: Make sure other developers understand the patterns used
  4. Consistency Is Key: Apply patterns consistently throughout the application
  5. Test Thoroughly: Design patterns should make testing easier, not harder
  6. Performance Matters: Consider Salesforce governor limits when implementing patterns
  7. Combine Patterns: Use multiple patterns together to solve complex problems
  8. Evolve Gradually: Refactor toward patterns over time rather than all at once

References

Amit Singh
Amit Singh

Amit Singh aka @sfdcpanther/pantherschools, a Salesforce Technical Architect, Consultant with over 8+ years of experience in Salesforce technology. 21x Certified. Blogger, Speaker, and Instructor. DevSecOps Champion

Articles: 299

Newsletter Updates

Enter your email address below and subscribe to our newsletter

Leave a Reply

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

Discover more from Panther Schools

Subscribe now to keep reading and get access to the full archive.

Continue reading