Apex Design Patterns in Salesforce — Singleton & Factory Explained

A practical guide to Apex design patterns — learn Singleton and Factory patterns with real-world examples, code snippets, and best practices for scalable Salesforce development.

What are Apex Design Patterns?

Apex design patterns are reusable solutions to common problems in Salesforce development. They provide a consistent structure for organizing code so it’s easier to maintain, scale, and test. While these patterns originate from object-oriented programming, they are adapted for Apex to respect Salesforce’s governor limits and multi-tenant architecture.

Why use design patterns?

Using design patterns in Apex helps teams avoid duplicate logic, reduce SOQL/DML usage, improve testability, and make codebases easier to extend. Patterns also promote reusability and clarity across triggers, classes, and integrations.

Common Apex Design Patterns Covered

1. Singleton Pattern

The Singleton Pattern ensures a class has only one instance during a request and provides a global point of access. In Apex, this is useful for loading configuration or reference data once per transaction and reusing it across triggers, classes, and controllers.

Example scenario: applying region-based discounts stored in a custom object Region_Discount__c across Cases, Opportunities, or custom objects.

Benefits:

  • Single instance per transaction
  • Improved performance by avoiding repeated SOQL queries
  • Helps stay within governor limits during bulk operations

Singleton example (Apex)

public class RegionDiscountManager {

    // Singleton instance
    private static RegionDiscountManager instance;

    // Map to hold Region → Discount values
    private Map regionDiscountMap;

    // Private constructor to prevent direct instantiation
    private RegionDiscountManager() {
        regionDiscountMap = new Map();
        for (Region_Discount__c rd : [SELECT Region__c, Discount__c FROM Region_Discount__c]) {
            if (!String.isBlank(rd.Region__c)) {
                regionDiscountMap.put(rd.Region__c.trim().toLowerCase(), rd.Discount__c);
            }
        }
    }

    // Public method to access singleton instance
    public static RegionDiscountManager getInstance() {
        if (instance == null) {
            instance = new RegionDiscountManager();
        }
        return instance;
    }

    // Method to get discount for a region
    public Decimal getDiscountForRegion(String region) {
        if (String.isBlank(region)) return 0;
        String key = region.trim().toLowerCase();
        return regionDiscountMap.get(key);
    }
}

Trigger example using Singleton

trigger CaseTrigger on Case (before insert, before update) {
    if (Trigger.isBefore && Trigger.isInsert) {
        RegionDiscountManager rdm = RegionDiscountManager.getInstance();
        for (Case c : Trigger.new) {
            if (!String.isBlank(c.Region__c)) {
                c.Discount__c = rdm.getDiscountForRegion(c.Region__c);
            }
        }
    }
}

2. Factory Pattern

The Factory Pattern centralizes object creation so you return the correct implementation without hardcoding class names. This is ideal when business logic varies by type or context — for example, applying different discount strategies based on Account.Type.

Scenario: Account Types — Customer (percentage discount), Partner (flat discount), Internal (no discount).

Implementation steps

  • Define a common interface (e.g., IDiscountStrategy)
  • Create concrete implementations (CustomerDiscount, PartnerDiscount, InternalDiscount)
  • Create a factory class (DiscountStrategyFactory) that returns the right implementation
  • Use the factory from triggers, services, or controllers

Factory example (Apex)

public interface IDiscountStrategy {
    Decimal calculateDiscount(Decimal originalAmount);
}

public class CustomerDiscount implements IDiscountStrategy {
    public Decimal calculateDiscount(Decimal originalAmount) {
        return originalAmount * 0.10; // 10% discount
    }
}

public class PartnerDiscount implements IDiscountStrategy {
    public Decimal calculateDiscount(Decimal originalAmount) {
        return 500; // Flat $500 off
    }
}

public class InternalDiscount implements IDiscountStrategy {
    public Decimal calculateDiscount(Decimal originalAmount) {
        return 0; // No discount
    }
}

public class DiscountStrategyFactory {
    public static IDiscountStrategy getStrategy(String accountType) {
        if (accountType == 'Customer') {
            return new CustomerDiscount();
        } else if (accountType == 'Partner') {
            return new PartnerDiscount();
        } else {
            return new InternalDiscount(); // default fallback
        }
    }
}

// Usage in an Opportunity trigger
trigger OpportunityTrigger on Opportunity (before insert, before update) {
    if (Trigger.isBefore && Trigger.isInsert) {
        for (Opportunity opp : Trigger.new) {
            String accType = opp.Account.Type;
            IDiscountStrategy strategy = DiscountStrategyFactory.getStrategy(accType);
            opp.Discount__c = strategy.calculateDiscount(opp.Amount);
        }
    }
}

Key takeaways

  • Singleton: Use for transaction-wide cached data (settings/reference data)
  • Factory: Use to centralize creation of behavior-specific objects
  • Patterns improve scalability, maintainability, and guard against governor limit issues

By thoughtfully applying these patterns, Salesforce developers can build cleaner, more testable, and extensible applications.

Why this matters for Salesforce teams

Design patterns give development teams a shared vocabulary and structure for solving recurring problems. For admins, developers, and architects, adopting patterns reduces technical debt, speeds up onboarding, and ensures solutions behave predictably as systems grow.