Apex Design Patterns Every Salesforce Developer Should Know

When you first start coding in Apex, it’s easy to fall into the trap of writing everything directly inside triggers or controllers. While this may work for small projects, as your Salesforce org grows, the complexity of your codebase can quickly become unmanageable. That’s where design patterns come in.

Design patterns are proven solutions to common programming challenges. By applying them in your Apex development, you can write code that is cleaner, reusable, easier to maintain, and more scalable. Salesforce itself encourages developers to embrace design patterns to ensure enterprise-grade implementations.

In this blog, we’ll explore 10 Apex design patterns that every Salesforce developer should know. For each, we’ll cover what it is, why it’s important, and show an Apex code example to help you apply it in your own projects.

Let's level up your development game!


Why Should a Salesforce Developer Care About Design Patterns?


Before we dive in, let's address the "why." On the Salesforce platform, with its governor limits and multi-tenant architecture, well-structured code isn't a luxury—it's a necessity.

  • Maintainability: Patterns create a consistent structure. Any developer on your team can look at the code and understand how it's organized, making onboarding and bug-fixing faster.
  • Testability: Patterns like Service Layer and Trigger Handler make it incredibly easy to write granular, focused unit tests, leading to higher code coverage and more reliable deployments.
  • Reusability: Avoid reinventing the wheel. Write logic once and use it across multiple triggers, Visualforce pages, Lightning Web Components, and REST APIs.
  • Separation of Concerns (SoC): Patterns help you decouple your business logic from your database logic (DML) and your UI logic. This makes your code more flexible and less brittle when requirements change.
  • Governor Limit Management: Properly structured code, especially using patterns like Unit of Work, helps you bulkify operations and manage DML and SOQL queries efficiently, preventing nasty limit exceptions.


1. Trigger Handler Pattern


Problem: Business logic often ends up directly inside triggers, making them bulky and difficult to maintain.

Solution: The Trigger Handler pattern moves all business logic out of the trigger and into a dedicated handler class.

Benefits:

  • Keeps triggers clean and simple.
  • Encourages code reuse.
  • Makes testing easier.

Example:


// Trigger
trigger AccountTrigger on Account (before insert, after insert, before update, after update) {
    AccountTriggerHandler handler = new AccountTriggerHandler();
    if(Trigger.isBefore && Trigger.isInsert) {
        handler.beforeInsert(Trigger.new);
    }
    if(Trigger.isAfter && Trigger.isInsert) {
        handler.afterInsert(Trigger.new);
    }
}

// Handler
public class AccountTriggerHandler {
    public void beforeInsert(List newAccounts) {
        for(Account acc : newAccounts) {
            acc.Name = acc.Name + ' - Verified';
        }
    }
    public void afterInsert(List newAccounts) {
        System.debug('Accounts created: ' + newAccounts.size());
    }
}

 MIND IT !

No SOQL/DML inside the trigger body. Always push it into handlers or services.


2. Service Layer Pattern


Problem: Mixing business logic across controllers, triggers, and utilities leads to scattered and duplicated code.

Solution: Place business logic inside a service layer class, which acts as a single entry point for core operations.

Benefits:

  • Centralized logic.
  • Improves code organization.
  • Easier to reuse across triggers, controllers, and APIs.

Example:


public class AccountService {
    public static void updateAccountStatus(List accounts) {
        for(Account acc : accounts) {
            acc.Status__c = 'Active';
        }
        update accounts;
    }
}

// Usage
List accs = [SELECT Id FROM Account LIMIT 5];
AccountService.updateAccountStatus(accs);

 MIND IT !

Perform CRUD/FLS checks here. That way, all callers inherit secure behavior.


3. Unit of Work Pattern


Problem: Complex transactions may involve multiple objects and DML statements, leading to governor limit issues.

Solution: The Unit of Work pattern batches DML operations and commits them together at the end.

Benefits:

  • Prevents hitting governor limits.
  • Provides transactional consistency.
  • Cleaner rollback management.

Example (simplified):


public class UnitOfWork {
    private List accountsToInsert = new List();
    private List contactsToInsert = new List();
    
    public void registerAccount(Account acc) {
        accountsToInsert.add(acc);
    }
    public void registerContact(Contact con) {
        contactsToInsert.add(con);
    }
    public void commitWork() {
        insert accountsToInsert;
        insert contactsToInsert;
    }
}

// Usage
UnitOfWork uow = new UnitOfWork();
uow.registerAccount(new Account(Name='ABC Corp'));
uow.registerContact(new Contact(LastName='Smith'));
uow.commitWork();

4. Factory Pattern


Problem: Creating objects directly inside methods couples the code to specific implementations.

Solution: The Factory pattern centralizes object creation.

Benefits:

  • Decouples object creation from usage.
  • Makes it easier to add or change implementations.

Example:


public interface PaymentProcessor {
    void processPayment(Decimal amount);
}

public class CreditCardProcessor implements PaymentProcessor {
    public void processPayment(Decimal amount) {
        System.debug('Processing credit card payment: ' + amount);
    }
}

public class PaymentFactory {
    public static PaymentProcessor getProcessor(String type) {
        if(type == 'CreditCard') {
            return new CreditCardProcessor();
        }
        return null;
    }
}

// Usage
PaymentProcessor processor = PaymentFactory.getProcessor('CreditCard');
processor.processPayment(1000);

5. Singleton Pattern


Problem: Sometimes you only want a single instance of a class throughout the transaction.

Solution: Use the Singleton pattern to ensure only one object instance is created.

Benefits:

  • Reduces resource usage.
  • Ensures consistency of shared objects.

Example:


public class SettingsSingleton {
    private static SettingsSingleton instance;
    public String settingValue;
    
    private SettingsSingleton() {
        settingValue = 'Global Value';
    }
    
    public static SettingsSingleton getInstance() {
        if(instance == null) {
            instance = new SettingsSingleton();
        }
        return instance;
    }
}

// Usage
SettingsSingleton s = SettingsSingleton.getInstance();
System.debug(s.settingValue);

6. Strategy Pattern


Problem: Hardcoding multiple algorithms in one class makes it inflexible and messy.

Solution: The Strategy pattern defines a family of algorithms and makes them interchangeable.

Benefits:

  • Flexible and extensible.
  • Eliminates conditional logic.

Example:


public interface DiscountStrategy {
    Decimal applyDiscount(Decimal amount);
}

public class NoDiscount implements DiscountStrategy {
    public Decimal applyDiscount(Decimal amount) {
        return amount;
    }
}

public class PercentageDiscount implements DiscountStrategy {
    public Decimal applyDiscount(Decimal amount) {
        return amount * 0.9;
    }
}

// Usage
DiscountStrategy strategy = new PercentageDiscount();
Decimal price = strategy.applyDiscount(1000);
System.debug('Final Price: ' + price);

7. Decorator Pattern


Problem: Sometimes you want to add extra behavior to objects without modifying their core class.

Solution: The Decorator pattern lets you “wrap” objects to extend functionality.

Benefits:

  • Promotes flexible enhancements.
  • Avoids creating bloated classes.

Example:


public interface Notifier {
    void send(String message);
}

public class EmailNotifier implements Notifier {
    public void send(String message) {
        System.debug('Email: ' + message);
    }
}

public class SMSNotifier implements Notifier {
    private Notifier notifier;
    public SMSNotifier(Notifier notifier) {
        this.notifier = notifier;
    }
    public void send(String message) {
        notifier.send(message);
        System.debug('SMS: ' + message);
    }
}

// Usage
Notifier notifier = new SMSNotifier(new EmailNotifier());
notifier.send('Your account is active!');

8. Observer Pattern


Problem: When one object changes, other objects need to be notified.

Solution: The Observer pattern sets up a publisher-subscriber model.

Benefits:

  • Promotes loose coupling.
  • Useful for event-driven designs.

Example:


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

public class AuditLogger implements Observer {
    public void update(String message) {
        System.debug('Audit: ' + message);
    }
}

public class Publisher {
    private List observers = new List();
    
    public void addObserver(Observer obs) {
        observers.add(obs);
    }
    
    public void notifyObservers(String message) {
        for(Observer obs : observers) {
            obs.update(message);
        }
    }
}

// Usage
Publisher pub = new Publisher();
pub.addObserver(new AuditLogger());
pub.notifyObservers('Account created');

9. Command Pattern


Problem:Directly invoking methods makes it difficult to queue, log, or undo actions.

Solution: The Command pattern encapsulates a request as an object.

Benefits:

  • Useful for batch jobs, queuing, or undo logic.
  • Decouples invoker and receiver.

Example:


public interface Command {
    void execute();
}

public class CreateAccountCommand implements Command {
    public void execute() {
        insert new Account(Name='New Account');
    }
}

public class Invoker {
    public void runCommand(Command cmd) {
        cmd.execute();
    }
}

// Usage
Invoker inv = new Invoker();
inv.runCommand(new CreateAccountCommand());

10. Facade Pattern


Problem:Complex subsystems confuse clients and lead to messy calls.

Solution: Provide a simple facade class that hides internal complexity.

Benefits:

  • Simplifies client usage.
  • Provides a single entry point.

Example:


public class AccountFacade {
    public static void createAccountWithContact(String accName, String contactName) {
        Account acc = new Account(Name=accName);
        insert acc;
        
        Contact con = new Contact(LastName=contactName, AccountId=acc.Id);
        insert con;
    }
}

// Usage
AccountFacade.createAccountWithContact('Tech Corp', 'Saurabh Samir');

Conclusion: Patterns are Your Blueprint for Success


Learning and implementing these design patterns is not about being academically clever. It's about writing professional-grade code that stands the test of time. It's about making your life, and the lives of your teammates, easier.

Start by mastering the Trigger Handler and Service Layer patterns. These are non-negotiable for any serious development. Then, gradually incorporate others like the Unit of Work and Singleton as your applications grow in complexity.

Don't try to force a pattern where it doesn't fit. Use them as tools in your toolbox. The true skill is in recognizing which problem calls for which solution.

What's your favorite Apex design pattern? Did we miss any crucial ones? Share your thoughts in the comments below!

Share This Post:

About The Author

Hey, my name is Saurabh Samir, and I am a Salesforce Developer with a passion for helping you elevate your knowledge in Salesforce, Lightning Web Components (LWC), Salesforce TPM (CG Cloud), Salesforce triggers, and Apex. I aim to simplify complex concepts and share valuable insights to enhance your Salesforce journey. Do comment below if you have any questions or feedback—I'd love to hear from you!