Apex Design Patterns

Apex Design Patterns: Your Guide to Smarter Salesforce Code

Apex Design Patterns: Building Robust Salesforce Solutions

Your Guide to Smarter, Scalable Apex Code

πŸ’‘ What are Apex Design Patterns?

Imagine you’re building a complex LEGO castle. You wouldn’t just throw bricks together randomly, right? You’d use proven techniques for building strong walls, sturdy towers, and clever drawbridges. Design patterns in Apex are exactly like those proven techniques, but for writing code!

In a nutshell, **Apex Design Patterns are reusable solutions to common problems that Salesforce developers face when writing Apex code.** They aren’t finished pieces of code you can just copy-paste, but rather templates or blueprints that show you *how* to structure your code to solve specific problems in an efficient, maintainable, and scalable way. Think of them as a collection of “best practices” codified into a repeatable structure.

They help us create code that is:

  • Maintainable: Easier to understand and fix.
  • Scalable: Can handle growth and more complex requirements.
  • Readable: Other developers (and your future self!) can quickly grasp its purpose.
  • Testable: Simpler to write unit tests for.
  • Flexible: Easier to modify or extend without breaking existing functionality.

🌟 Why are They Important? (Business Value & Use Cases)

Using design patterns isn’t just a technical nicety; it brings significant business value to your Salesforce implementation:

  • Reduces Technical Debt: Without patterns, code can become a tangled mess (often called “spaghetti code”). This makes future changes expensive and risky. Patterns help keep your codebase clean, saving time and money in the long run.
  • Faster Development: Instead of reinventing the wheel every time, you can apply a known pattern, which speeds up development and reduces the chances of introducing bugs.
  • Improved Collaboration: When developers use common patterns, they speak a shared language. It’s like everyone following the same architectural standards – new team members can onboard faster, and code reviews are more efficient.
  • Better Performance & Resource Management: Many patterns inherently promote efficient use of Salesforce’s governor limits by centralizing logic, preventing redundant operations, and optimizing DML.
  • Easier Evolution of Features: Business requirements change constantly. Patterns build flexibility into your code, allowing you to adapt to new needs without major refactoring efforts. For example, adding a new validation rule is trivial with a well-designed trigger handler.

Common Use Cases:

  • Managing complex trigger logic to avoid recursion and ensure proper order of execution.
  • Integrating with external systems in a standardized way.
  • Creating reusable utility classes that ensure only one instance exists (e.g., for logging or configuration).
  • Handling different types of calculations or processes based on specific criteria.
  • Decoupling business logic from user interface or data layer interactions.

πŸ› οΈ Key Concepts and Components

Before diving into specific patterns, let’s touch upon some fundamental programming concepts that underpin them:

  • Encapsulation: Hiding the internal complexity of an object and exposing only what’s necessary. Think of a car: you interact with the steering wheel and pedals (public interface), but don’t need to know how the engine (internal complexity) works.
  • Abstraction: Focusing on essential details while ignoring less important ones. An abstract class defines a common interface for related classes, allowing you to treat them uniformly.
  • Inheritance: A mechanism where one class (subclass/child) inherits properties and behaviors from another class (superclass/parent). This promotes code reuse.
  • Polymorphism: The ability of an object to take on many forms. In Apex, this often means an object of a child class can be treated as an object of its parent class, allowing for flexible code that can handle different types uniformly.
  • Composition: Building complex objects by combining simpler objects. Instead of inheriting behavior, you delegate it to another object. This is often preferred over inheritance for flexibility.
  • Loose Coupling: Components of a system are designed to be as independent as possible. Changes in one component have minimal impact on others. This makes systems more flexible and easier to maintain.
  • High Cohesion: The elements within a single component (e.g., a class or method) belong together because they contribute to a single, well-defined purpose. This makes components easier to understand and manage.

πŸš€ How Does It Work? (Examples of Key Patterns)

Let’s explore some common and highly useful design patterns in Apex with simple examples. These patterns are foundational for building robust Salesforce applications.

1. Singleton Pattern

Concept: Ensures that a class has only one instance and provides a global point of access to that instance. Think of it like the “Master Key” to a building – there’s only one, and everyone knows where to get it.

Why use it? When you need to manage a single, shared resource (like a logger, a configuration manager, or a governor limit utility) across your application. It prevents multiple instances from consuming excessive resources or causing inconsistent behavior.

Singleton Instance
Only One
β†’
Class A
,
Class B
,
Class C
… all access the same instance

How it works:

  1. Make the constructor private to prevent direct instantiation.
  2. Create a static method that returns the single instance of the class.
  3. Inside this method, check if an instance already exists. If not, create it; otherwise, return the existing one.

Apex Example: A simple Logger class.


public class MyLogger {
    private static MyLogger instance;
    private List<String> logs;

    // 1. Private constructor
    private MyLogger() {
        logs = new List<String>();
    }

    // 2. Static method to get the instance
    public static MyLogger getInstance() {
        if (instance == null) {
            instance = new MyLogger();
        }
        return instance;
    }

    public void log(String message) {
        logs.add(System.now() + ': ' + message);
        System.debug('LOG: ' + message);
    }

    public List<String> getAllLogs() {
        return logs;
    }
}

// How to use it:
// MyLogger.getInstance().log('User logged in.');
// MyLogger.getInstance().log('Account created: ' + newAccount.Id);
// System.debug('All logs: ' + MyLogger.getInstance().getAllLogs());
        

2. Strategy Pattern

Concept: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern lets the algorithm vary independently from clients that use it. Imagine having different recipes (strategies) for making coffee (algorithm) – Espresso, Latte, Americano. You choose which one to use based on your mood, but the coffee machine (client) knows how to execute any of them.

Why use it? When you have a single operation that can be performed in several different ways, and you want to switch between these ways at runtime. This avoids bulky conditional statements (if-else if) and makes it easy to add new algorithms without modifying existing code.

Context (e.g., Order Processor)
Uses Strategy
β†’
Strategy A
Strategy B
Strategy C
… all implement a common interface

How it works:

  1. Define an interface (the Strategy) for all algorithms.
  2. Create concrete classes (Concrete Strategies) that implement this interface, each providing a different algorithm.
  3. Create a Context class that holds a reference to a Strategy object and uses it to execute the algorithm. The Context doesn’t know the specific implementation of the algorithm, only that it conforms to the interface.

Apex Example: Different discount calculation methods for an order.


// 1. Define the Strategy Interface
public interface IDiscountStrategy {
    Decimal calculateDiscount(Decimal originalAmount);
}

// 2. Concrete Strategy A: Percentage Discount
public class PercentageDiscount implements IDiscountStrategy {
    private Decimal percentage; // e.g., 0.10 for 10%

    public PercentageDiscount(Decimal percentage) {
        this.percentage = percentage;
    }

    public Decimal calculateDiscount(Decimal originalAmount) {
        return originalAmount * percentage;
    }
}

// 2. Concrete Strategy B: Fixed Amount Discount
public class FixedAmountDiscount implements IDiscountStrategy {
    private Decimal fixedAmount; // e.g., 25.00

    public FixedAmountDiscount(Decimal fixedAmount) {
        this.fixedAmount = fixedAmount;
    }

    public Decimal calculateDiscount(Decimal originalAmount) {
        return fixedAmount;
    }
}

// 3. Context Class: Order Processor
public class OrderProcessor {
    private IDiscountStrategy discountStrategy;

    public OrderProcessor(IDiscountStrategy strategy) {
        this.discountStrategy = strategy;
    }

    public Decimal processOrder(Decimal orderTotal) {
        Decimal discount = discountStrategy.calculateDiscount(orderTotal);
        return orderTotal - discount;
    }
}

// How to use it:
// // Apply a 10% discount
// OrderProcessor processor1 = new OrderProcessor(new PercentageDiscount(0.10));
// Decimal finalPrice1 = processor1.processOrder(100.00); // 90.00

// // Apply a fixed $15 discount
// OrderProcessor processor2 = new OrderProcessor(new FixedAmountDiscount(15.00));
// Decimal finalPrice2 = processor2.processOrder(100.00); // 85.00
        

3. Factory Pattern

Concept: Provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. It’s like a specialized vending machine: you press a button (request a product), and it dispenses a specific item (creates an object) without you needing to know the complex mechanics inside.

Why use it? When your code needs to create objects of different types that share a common interface, but you want to decouple the client code from the concrete classes being instantiated. This is great for managing complex object creation logic.

Product Factory
Creates Objects
β†’
Product A
Product B
Product C

How it works:

  1. Define an interface (or abstract class) for the “product” objects that will be created.
  2. Create concrete “product” classes that implement this interface.
  3. Create a “factory” class with a method that takes a parameter (e.g., a String type) and returns an instance of the appropriate concrete product class.

Apex Example: Creating different types of notifications (Email, SMS).


// 1. Product Interface
public interface INotification {
    void send(String recipient, String message);
}

// 2. Concrete Product A: Email Notification
public class EmailNotification implements INotification {
    public void send(String recipient, String message) {
        System.debug('Sending Email to ' + recipient + ': ' + message);
        // Add actual Email send logic here (e.g., Messaging.SingleEmailMessage)
    }
}

// 2. Concrete Product B: SMS Notification
public class SMSNotification implements INotification {
    public void send(String recipient, String message) {
        System.debug('Sending SMS to ' + recipient + ': ' + message);
        // Add actual SMS send logic here (e.g., call out to an SMS service)
    }
}

// 3. Factory Class
public class NotificationFactory {
    public static INotification createNotification(String type) {
        if (type == 'EMAIL') {
            return new EmailNotification();
        } else if (type == 'SMS') {
            return new SMSNotification();
        } else {
            throw new NotificationException('Unsupported notification type: ' + type);
        }
    }

    public class NotificationException extends Exception {}
}

// How to use it:
// INotification emailSender = NotificationFactory.createNotification('EMAIL');
// emailSender.send('admin@example.com', 'Your order has shipped!');

// INotification smsSender = NotificationFactory.createNotification('SMS');
// smsSender.send('+15551234567', 'Your order is on its way!');
        

4. Facade Pattern

Concept: Provides a simplified interface to a complex subsystem. It’s like the dashboard of your car – instead of interacting with the engine, brakes, and transmission directly, you use simple controls (pedals, steering wheel) that internally orchestrate multiple complex actions.

Why use it? To make a complex system easier to use by providing a single, straightforward entry point. It reduces coupling between clients and the subsystem, making the system more robust and easier to refactor.

Facade
Simplified Interface
β†’
Subsystem A
Subsystem B
Subsystem C
… hides complexity

How it works:

  1. Identify a complex subsystem (e.g., multiple service classes, DML operations, callouts).
  2. Create a new class (the Facade) that wraps these complex interactions.
  3. The Facade provides simple, high-level methods that internally call and coordinate the various components of the subsystem.

Apex Example: A service layer facade for creating an Account and its related Contact.


// Complex Subsystem Components (imagine these are separate service classes)
public class AccountService {
    public Account createAccount(String accName) {
        Account acc = new Account(Name = accName);
        insert acc;
        System.debug('Account created: ' + acc.Id);
        return acc;
    }
}

public class ContactService {
    public Contact createContact(Id accountId, String firstName, String lastName) {
        Contact con = new Contact(AccountId = accountId, FirstName = firstName, LastName = lastName);
        insert con;
        System.debug('Contact created: ' + con.Id);
        return con;
    }
}

// Facade Class
public class AccountContactFacade {
    private AccountService accService;
    private ContactService conService;

    public AccountContactFacade() {
        this.accService = new AccountService();
        this.conService = new ContactService();
    }

    // Simplified method to create both Account and Contact
    public void createAccountWithContact(String accountName, String contactFirstName, String contactLastName) {
        try {
            Account newAcc = accService.createAccount(accountName);
            conService.createContact(newAcc.Id, contactFirstName, contactLastName);
            System.debug('Successfully created Account and Contact for ' + accountName);
        } catch (DmlException e) {
            System.debug(LoggingLevel.ERROR, 'Error creating Account/Contact: ' + e.getMessage());
            // Potentially re-throw a custom exception
        }
    }
}

// How to use it:
// AccountContactFacade facade = new AccountContactFacade();
// facade.createAccountWithContact('Acme Corp', 'John', 'Doe');
        

5. Trigger Handler Pattern (Salesforce Specific)

Concept: Decouples the business logic from the trigger definition itself. A trigger should be “skinny,” meaning it should only contain the bare minimum logic to delegate control to a handler class. This is a crucial Salesforce-specific pattern to manage trigger complexity and governor limits.

Why use it? Triggers can easily become large, unmanageable, and difficult to test if all logic resides within them. The Trigger Handler pattern ensures:

  • Single Point of Control: All logic for an object is in one handler.
  • Order of Execution: Easier to manage the sequence of operations across different trigger contexts (before insert, after update, etc.).
  • Prevent Recursion: Handlers provide an easy way to prevent trigger re-entry.
  • Testability: Business logic in a handler class is much easier to unit test than logic directly in a trigger.
  • Bypass Logic: Can easily enable/disable specific trigger logic for data loads or tests.
Account Trigger
Skinny Trigger
β†’
AccountHandler
Manages Logic
β†’
Service A
Service B
… delegates to specific methods

How it works:

  1. Create a single trigger per object (e.g., AccountTrigger).
  2. This trigger’s only job is to instantiate a “handler” class and call the appropriate method on it based on Trigger.isBefore, Trigger.isAfter, Trigger.isInsert, etc.
  3. The handler class contains methods for each trigger event (e.g., onBeforeInsert, onAfterUpdate) that encapsulate the actual business logic, often delegating to further service or utility classes.
  4. Include a static variable in the handler to prevent recursion.

Apex Example: Account Trigger Handler.


// AccountTrigger.trigger (The "Skinny Trigger")
/*
trigger AccountTrigger on Account (before insert, before update, after insert, after update, before delete, after delete, after undelete) {
    if (TriggerHandler.bypassTrigger('AccountTrigger')) { // Optional bypass mechanism
        return;
    }

    AccountTriggerHandler handler = new AccountTriggerHandler();

    if (Trigger.isBefore) {
        if (Trigger.isInsert) {
            handler.onBeforeInsert(Trigger.new);
        } else if (Trigger.isUpdate) {
            handler.onBeforeUpdate(Trigger.new, Trigger.oldMap);
        } else if (Trigger.isDelete) {
            handler.onBeforeDelete(Trigger.old);
        }
    } else if (Trigger.isAfter) {
        if (Trigger.isInsert) {
            handler.onAfterInsert(Trigger.new);
        } else if (Trigger.isUpdate) {
            handler.onAfterUpdate(Trigger.new, Trigger.oldMap);
        } else if (Trigger.isDelete) {
            handler.onAfterDelete(Trigger.old);
        } else if (Trigger.isUndelete) {
            handler.onAfterUndelete(Trigger.new);
        }
    }
}
*/

// AccountTriggerHandler.cls
public class AccountTriggerHandler {
    // Static variable to prevent recursion
    private static Boolean hasRun = false;

    public void onBeforeInsert(List<Account> newAccounts) {
        if (!hasRun) {
            hasRun = true;
            // Example: Set default field values
            for (Account acc : newAccounts) {
                if (acc.Industry == null) {
                    acc.Industry = 'Other';
                }
            }
            // Delegate to other service classes if needed
            // AccountValidationService.validateName(newAccounts);
        }
    }

    public void onAfterInsert(List<Account> newAccounts) {
        if (!hasRun) { // Recursion prevention applies per event, or can be global
            hasRun = true;
            // Example: Create related Contacts or send notifications
            // RelatedContactCreator.createDefaultContacts(newAccounts);
            // NotificationService.sendAccountCreationAlert(newAccounts);
        }
    }

    public void onBeforeUpdate(List<Account> newAccounts, Map<Id, Account> oldAccountMap) {
        // Implement logic here, checking changed fields
        // if (!hasRun) { hasRun = true; ... }
    }

    // ... other trigger event methods (onAfterUpdate, onBeforeDelete, etc.)
}

// Global Trigger Bypass Utility (optional, but very useful)
public class TriggerHandler {
    private static Map<String, Boolean> bypassMap = new Map<String, Boolean>();

    public static void bypassTrigger(String triggerName) {
        bypassMap.put(triggerName, true);
    }

    public static void clearBypass(String triggerName) {
        bypassMap.put(triggerName, false);
    }

    public static Boolean bypassTrigger(String triggerName) {
        return bypassMap.containsKey(triggerName) && bypassMap.get(triggerName);
    }
}
        

βœ… Best Practices & Common Pitfalls

Using design patterns effectively requires discipline and adherence to best practices, while also being aware of potential issues:

Best Practices:

  • Don’t Over-Engineer: Use patterns when they genuinely solve a problem, not just because they exist. Start simple and refactor to a pattern when complexity arises.
  • Keep it Contextual to Salesforce: Adapt general design patterns to fit Salesforce’s unique architecture, governor limits, and best practices (e.g., bulkification, DML operations, security).
  • Write Thorough Tests: Every pattern implementation should be fully covered by unit tests. This is where loose coupling really shines, making testing easier.
  • Document Your Code: Explain *why* a particular pattern was used and how it functions. This aids future maintenance.
  • Use Interfaces: Prefer coding to interfaces rather than concrete implementations. This significantly increases flexibility and testability.
  • Bulkify Everything: Always design patterns with Salesforce governor limits in mind. Avoid DML or SOQL inside loops. Patterns like Trigger Handlers are excellent for bulkification.
  • Separate Concerns: Adhere to the Single Responsibility Principle. Each class and method should have one clear job.

Common Pitfalls:

  • “Patternitis” or Over-Engineering: Applying patterns unnecessarily, making simple solutions overly complex. This adds overhead without providing value.
  • Ignoring Governor Limits: Implementing a pattern from another language verbatim without adapting it for Salesforce’s resource constraints can lead to unexpected `LimitException` errors.
  • Tight Coupling: Failing to use interfaces or proper dependency injection, leading to components that are still too tightly linked, defeating the purpose of many patterns.
  • Lack of Standardization: If different developers use different variations of patterns, the benefits of shared understanding diminish. Establish coding guidelines.
  • Poor Error Handling: Complex patterns can sometimes make error handling more convoluted if not designed carefully. Ensure robust try-catch blocks and meaningful exception messages.

✨ What’s New in Salesforce Releases?

Salesforce is continuously evolving Apex and its underlying platform. While I cannot predict specific features for Winter ’26 or Spring ’26, here’s a look at *types* of recent enhancements (drawing from recent actual releases like Spring ’24, Summer ’24) that can influence how we apply or optimize Apex design patterns:

  • Enhanced Async Apex Capabilities (e.g., Queueable Chaining Limits in Spring ’24): Increases the maximum number of jobs that can be chained in a single transaction. This directly impacts patterns involving `Queueable` for complex, long-running processes (e.g., `Chain of Responsibility` with async steps, or complex data processing pipelines). Developers can now design more extensive asynchronous flows without hitting immediate limits.
  • New `System.AsyncException` Class (Spring ’24): Provides a dedicated exception type for more specific error handling in asynchronous operations. This allows for more granular error management within patterns that involve async processing, enabling better retry mechanisms or clearer logging strategies.
  • New `Database` Methods and DML Options (e.g., `Database.batchUpdate` beta in Summer ’24): Introduces new ways to perform DML operations, offering more control over partial success or error handling. Patterns like Unit of Work or Transaction Script can leverage these new methods for more robust and efficient data management, especially when integrating with external systems or processing large datasets.
  • Improved Platform Event Capabilities (Continuous enhancements): Ongoing improvements to Platform Events and Change Data Capture (CDC) enhance event-driven architecture. This strengthens patterns like Observer/Publisher-Subscriber, allowing for more scalable and decoupled communication between different parts of the Salesforce platform or external systems.
  • Apex Code Analyzer / Linting Enhancements (Continuous development): While not a direct Apex feature, improvements in static code analysis tools help enforce coding standards and identify anti-patterns earlier. This aids in maintaining the quality and adherence to design patterns within a team.

Always refer to the official Salesforce Release Notes for the most accurate and up-to-date information on new features and enhancements in specific releases.

We don’t spam! Read our privacy policy for more info.

Leave a Reply

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