afv-library/skills/generating-apex/references/design-patterns.md

30 KiB

Apex Design Patterns

Factory Pattern

Purpose

Centralize object creation, enable dependency injection, simplify substitution/mocking.

Implementation

public virtual class Factory {
    private static Factory instance;

    public static Factory getInstance() {
        if (instance == null) {
            instance = new Factory();
        }
        return instance;
    }

    @TestVisible
    private static void setInstance(Factory mockFactory) {
        instance = mockFactory;
    }

    // Service getters - virtual for mocking
    public virtual AccountService getAccountService() {
        return new AccountService();
    }

    public virtual ContactService getContactService() {
        return new ContactService();
    }

    public virtual PaymentGateway getPaymentGateway() {
        return new StripePaymentGateway();
    }
}

Usage

public class OrderProcessor {
    private AccountService accountService;
    private PaymentGateway gateway;

    public OrderProcessor() {
        this(Factory.getInstance());
    }

    @TestVisible
    private OrderProcessor(Factory factory) {
        this.accountService = factory.getAccountService();
        this.gateway = factory.getPaymentGateway();
    }

    public void process(Order__c order) {
        Account acc = accountService.getAccount(order.Account__c);
        gateway.charge(order.Total__c);
    }
}

Repository Pattern

Purpose

Abstract data access, provide strongly-typed queries, enable DML mocking.

Implementation

public virtual class AccountRepository {

    public virtual List<Account> getByIds(Set<Id> accountIds) {
        return [
            SELECT Id, Name, Industry, AnnualRevenue
            FROM Account
            WHERE Id IN :accountIds
            WITH USER_MODE
        ];
    }

    public virtual List<Account> getByIndustry(String industry) {
        return [
            SELECT Id, Name, AnnualRevenue
            FROM Account
            WHERE Industry = :industry
            WITH USER_MODE
        ];
    }

    public virtual Account getById(Id accountId) {
        List<Account> accounts = getByIds(new Set<Id>{accountId});
        return accounts.isEmpty() ? null : accounts[0];
    }

    public virtual void save(List<Account> accounts) {
        upsert accounts;
    }

    public virtual void remove(List<Account> accounts) {
        delete accounts;
    }
}

Usage

public class AccountService {
    private AccountRepository repo;

    public AccountService() {
        this.repo = new AccountRepository();
    }

    @TestVisible
    private AccountService(AccountRepository repo) {
        this.repo = repo;
    }

    public List<Account> getTechnologyAccounts() {
        return repo.getByIndustry('Technology');
    }

    public void updateAccounts(List<Account> accounts) {
        repo.save(accounts);
    }
}

Testing with Mock Repository

@isTest
private class AccountServiceTest {


---

## Selector Pattern

### Purpose
Centralize SOQL queries per object, enforce security, enable reuse.

### Implementation

```apex
public inherited sharing class AccountSelector {

    public List<Account> selectById(Set<Id> ids) {
        return [
            SELECT Id, Name, Industry, AnnualRevenue, BillingCity
            FROM Account
            WHERE Id IN :ids
            WITH USER_MODE
        ];
    }

    public List<Account> selectByIdWithContacts(Set<Id> ids) {
        return [
            SELECT Id, Name, Industry,
                (SELECT Id, FirstName, LastName, Email FROM Contacts)
            FROM Account
            WHERE Id IN :ids
            WITH USER_MODE
        ];
    }

    public List<Account> selectByName(String name) {
        return [
            SELECT Id, Name, Industry
            FROM Account
            WHERE Name LIKE :('%' + name + '%')
            WITH USER_MODE
            LIMIT 100
        ];
    }

    public List<Account> selectActiveByIndustry(String industry) {
        return [
            SELECT Id, Name, AnnualRevenue
            FROM Account
            WHERE Industry = :industry
            AND Status__c = 'Active'
            WITH USER_MODE
        ];
    }
}

Usage

public class AccountService {
    private AccountSelector selector = new AccountSelector();

    public Map<Id, Account> getAccountsMap(Set<Id> ids) {
        return new Map<Id, Account>(selector.selectById(ids));
    }
}

Builder Pattern

Purpose

Construct complex objects step-by-step, improve readability.

Implementation

public class AccountBuilder {
    private Account record;

    public AccountBuilder() {
        this.record = new Account();
    }

    public AccountBuilder withName(String name) {
        this.record.Name = name;
        return this;
    }

    public AccountBuilder withIndustry(String industry) {
        this.record.Industry = industry;
        return this;
    }

    public AccountBuilder withAnnualRevenue(Decimal revenue) {
        this.record.AnnualRevenue = revenue;
        return this;
    }

    public AccountBuilder withBillingAddress(String city, String state, String country) {
        this.record.BillingCity = city;
        this.record.BillingState = state;
        this.record.BillingCountry = country;
        return this;
    }

    public AccountBuilder withParent(Id parentId) {
        this.record.ParentId = parentId;
        return this;
    }

    public Account build() {
        return this.record;
    }

    public Account buildAndInsert() {
        insert this.record;
        return this.record;
    }
}

Usage

// Fluent interface for building objects
Account acc = new AccountBuilder()
    .withName('Acme Corporation')
    .withIndustry('Technology')
    .withAnnualRevenue(1000000)
    .withBillingAddress('San Francisco', 'CA', 'USA')
    .buildAndInsert();

// In tests
Account testAccount = new AccountBuilder()
    .withName('Test Account')
    .build();  // Don't insert for unit tests

Singleton Pattern

Purpose

Ensure single instance, cache expensive operations.

Implementation

public class ConfigurationService {
    private static ConfigurationService instance;
    private Map<String, String> settings;

    private ConfigurationService() {
        // Load settings once
        this.settings = new Map<String, String>();
        for (Configuration__mdt config : [SELECT DeveloperName, Value__c FROM Configuration__mdt]) {
            settings.put(config.DeveloperName, config.Value__c);
        }
    }

    public static ConfigurationService getInstance() {
        if (instance == null) {
            instance = new ConfigurationService();
        }
        return instance;
    }

    public String getSetting(String key) {
        return settings.get(key);
    }

    public String getSetting(String key, String defaultValue) {
        return settings.containsKey(key) ? settings.get(key) : defaultValue;
    }

    // For testing
    @TestVisible
    private static void reset() {
        instance = null;
    }
}

Usage

String apiUrl = ConfigurationService.getInstance().getSetting('API_URL');
String timeout = ConfigurationService.getInstance().getSetting('TIMEOUT', '30000');

Strategy Pattern

Purpose

Define family of algorithms, make them interchangeable.

Implementation

public interface DiscountStrategy {
    Decimal calculate(Decimal amount);
    String getDescription();
}

public class PercentageDiscount implements DiscountStrategy {
    private Decimal percentage;

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

    public Decimal calculate(Decimal amount) {
        return amount * (percentage / 100);
    }

    public String getDescription() {
        return percentage + '% off';
    }
}

public class FixedAmountDiscount implements DiscountStrategy {
    private Decimal fixedAmount;

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

    public Decimal calculate(Decimal amount) {
        return Math.min(fixedAmount, amount);
    }

    public String getDescription() {
        return '$' + fixedAmount + ' off';
    }
}

public class TieredDiscount implements DiscountStrategy {
    public Decimal calculate(Decimal amount) {
        if (amount > 1000) return amount * 0.15;
        if (amount > 500) return amount * 0.10;
        if (amount > 100) return amount * 0.05;
        return 0;
    }

    public String getDescription() {
        return 'Tiered discount based on amount';
    }
}

Usage

public class PricingService {
    private Map<String, DiscountStrategy> strategies;

    public PricingService() {
        strategies = new Map<String, DiscountStrategy>{
            'PERCENTAGE_10' => new PercentageDiscount(10),
            'FIXED_50' => new FixedAmountDiscount(50),
            'TIERED' => new TieredDiscount()
        };
    }

    public Decimal applyDiscount(String discountType, Decimal amount) {
        DiscountStrategy strategy = strategies.get(discountType);
        if (strategy == null) {
            return 0;
        }
        return strategy.calculate(amount);
    }
}

Unit of Work Pattern

Purpose

Manage DML as single transaction, track changes, enable rollback.

Basic Implementation

public class UnitOfWork {
    private List<SObject> newRecords = new List<SObject>();
    private List<SObject> dirtyRecords = new List<SObject>();
    private List<SObject> deletedRecords = new List<SObject>();

    public void registerNew(SObject record) {
        newRecords.add(record);
    }

    public void registerNew(List<SObject> records) {
        newRecords.addAll(records);
    }

    public void registerDirty(SObject record) {
        dirtyRecords.add(record);
    }

    public void registerDeleted(SObject record) {
        deletedRecords.add(record);
    }

    public void commitWork() {
        Savepoint sp = Database.setSavepoint();
        try {
            insert newRecords;
            update dirtyRecords;
            delete deletedRecords;
        } catch (Exception e) {
            Database.rollback(sp);
            throw e;
        }
    }
}

Usage

public class OrderService {
    public void processOrder(Order__c order, List<OrderItem__c> items) {
        UnitOfWork uow = new UnitOfWork();

        // Register all changes
        uow.registerNew(order);
        uow.registerNew(items);

        Account acc = [SELECT Id, Order_Count__c FROM Account WHERE Id = :order.Account__c];
        acc.Order_Count__c = (acc.Order_Count__c ?? 0) + 1;
        uow.registerDirty(acc);

        // Single commit - all or nothing
        uow.commitWork();
    }
}

Decorator Pattern

Purpose

Add functionality dynamically without modifying original class. Stack behaviors flexibly.

Implementation

// Base interface
public interface NotificationService {
    void send(String message, Id recipientId);
}

// Core implementation
public class EmailNotification implements NotificationService {
    public void send(String message, Id recipientId) {
        Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
        email.setTargetObjectId(recipientId);
        email.setPlainTextBody(message);
        Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{ email });
    }
}

// Decorator base - wraps another NotificationService
public virtual class NotificationDecorator implements NotificationService {
    protected NotificationService wrapped;

    public NotificationDecorator(NotificationService service) {
        this.wrapped = service;
    }

    public virtual void send(String message, Id recipientId) {
        wrapped.send(message, recipientId);
    }
}

// Concrete decorator: Add logging
public class LoggingNotificationDecorator extends NotificationDecorator {
    public LoggingNotificationDecorator(NotificationService service) {
        super(service);
    }

    public override void send(String message, Id recipientId) {
        System.debug('Sending notification to: ' + recipientId);
        super.send(message, recipientId);
        System.debug('Notification sent successfully');
    }
}

// Concrete decorator: Add retry logic
public class RetryNotificationDecorator extends NotificationDecorator {
    private Integer maxRetries;

    public RetryNotificationDecorator(NotificationService service, Integer maxRetries) {
        super(service);
        this.maxRetries = maxRetries;
    }

    public override void send(String message, Id recipientId) {
        Integer attempts = 0;
        while (attempts < maxRetries) {
            try {
                super.send(message, recipientId);
                return;
            } catch (Exception e) {
                attempts++;
                if (attempts >= maxRetries) throw e;
            }
        }
    }
}

Usage

// Stack decorators as needed
NotificationService service = new EmailNotification();
service = new LoggingNotificationDecorator(service);
service = new RetryNotificationDecorator(service, 3);

// Now sends with logging + retry + email
service.send('Your order shipped!', userId);

When to Use

  • Adding cross-cutting concerns (logging, caching, validation)
  • When inheritance leads to class explosion
  • Stacking behaviors that can be combined independently

Observer Pattern

Purpose

Define one-to-many dependency where observers are notified of state changes automatically.

Implementation

// Observer interface
public interface AccountObserver {
    void onAccountUpdated(Account oldAccount, Account newAccount);
}

// Subject that notifies observers
public class AccountSubject {
    private static List<AccountObserver> observers = new List<AccountObserver>();

    public static void attach(AccountObserver observer) {
        observers.add(observer);
    }

    public static void detach(AccountObserver observer) {
        Integer index = observers.indexOf(observer);
        if (index >= 0) observers.remove(index);
    }

    public static void notifyObservers(Account oldAccount, Account newAccount) {
        for (AccountObserver observer : observers) {
            observer.onAccountUpdated(oldAccount, newAccount);
        }
    }
}

// Concrete observers
public class SalesNotificationObserver implements AccountObserver {
    public void onAccountUpdated(Account oldAcc, Account newAcc) {
        if (newAcc.AnnualRevenue > 1000000 && (oldAcc.AnnualRevenue == null || oldAcc.AnnualRevenue <= 1000000)) {
            // Notify sales team about new enterprise account
            createTask(newAcc.OwnerId, 'New Enterprise Account: ' + newAcc.Name);
        }
    }

    private void createTask(Id ownerId, String subject) {
        insert new Task(OwnerId = ownerId, Subject = subject, Status = 'Open');
    }
}

public class IntegrationSyncObserver implements AccountObserver {
    public void onAccountUpdated(Account oldAcc, Account newAcc) {
        // Queue sync to external system
        System.enqueueJob(new AccountSyncQueueable(newAcc.Id));
    }
}

Usage in Trigger

// TriggerHandler or Action class
public class AccountTriggerHandler {

    static {
        // Register observers once
        AccountSubject.attach(new SalesNotificationObserver());
        AccountSubject.attach(new IntegrationSyncObserver());
    }

    public void afterUpdate(List<Account> newList, Map<Id, Account> oldMap) {
        for (Account acc : newList) {
            AccountSubject.notifyObservers(oldMap.get(acc.Id), acc);
        }
    }
}

Platform Events Alternative

For decoupled, async observers, use Platform Events:

// Publish event
EventBus.publish(new Account_Updated__e(Account_Id__c = acc.Id, Field_Changed__c = 'Status'));

// Subscribe via trigger on platform event
trigger AccountUpdatedSubscriber on Account_Updated__e (after insert) {
    // Handle event
}

Command Pattern

Purpose

Encapsulate requests as objects, enabling queuing, logging, undo, and parameterized execution.

Implementation

// Command interface
public interface Command {
    void execute();
    void undo();
    String getDescription();
}

// Concrete command: Update Field
public class UpdateFieldCommand implements Command {
    private Id recordId;
    private String fieldName;
    private Object newValue;
    private Object oldValue;
    private SObjectType objectType;

    public UpdateFieldCommand(Id recordId, String fieldName, Object newValue) {
        this.recordId = recordId;
        this.fieldName = fieldName;
        this.newValue = newValue;
        this.objectType = recordId.getSObjectType();
    }

    public void execute() {
        // Store old value for undo
        SObject record = Database.query(
            'SELECT ' + fieldName + ' FROM ' + objectType + ' WHERE Id = :recordId'
        );
        this.oldValue = record.get(fieldName);

        // Apply new value
        record.put(fieldName, newValue);
        update record;
    }

    public void undo() {
        SObject record = objectType.newSObject(recordId);
        record.put(fieldName, oldValue);
        update record;
    }

    public String getDescription() {
        return 'Update ' + objectType + '.' + fieldName + ' to ' + newValue;
    }
}

// Command invoker with history
public class CommandInvoker {
    private List<Command> history = new List<Command>();
    private List<Command> queue = new List<Command>();

    public void addToQueue(Command cmd) {
        queue.add(cmd);
    }

    public void executeQueue() {
        for (Command cmd : queue) {
            cmd.execute();
            history.add(cmd);
            // Log for audit trail
            System.debug('Executed: ' + cmd.getDescription());
        }
        queue.clear();
    }

    public void undoLast() {
        if (!history.isEmpty()) {
            Command lastCommand = history.remove(history.size() - 1);
            lastCommand.undo();
        }
    }
}

Usage

CommandInvoker invoker = new CommandInvoker();

// Queue multiple field updates
invoker.addToQueue(new UpdateFieldCommand(accountId, 'Status__c', 'Active'));
invoker.addToQueue(new UpdateFieldCommand(accountId, 'Priority__c', 'High'));

// Execute all
invoker.executeQueue();

// Undo last operation
invoker.undoLast();

Use Cases

  • Wizard/multi-step processes with undo
  • Audit trail with replayable operations
  • Batch processing with deferred execution
  • Macro recording and playback

Facade Pattern

Purpose

Provide simplified interface to complex subsystems. Reduce coupling between client and implementation details.

Implementation

// Complex subsystems
public class CustomerVerificationService {
    public Boolean verifyIdentity(String customerId) {
        // Complex identity verification logic
        return true;
    }
}

public class CreditCheckService {
    public Integer getCreditScore(String customerId) {
        // Call external credit bureau
        return 720;
    }

    public Decimal getAvailableCredit(String customerId) {
        return 50000.00;
    }
}

public class RiskAssessmentService {
    public String assessRisk(Integer creditScore, Decimal requestedAmount) {
        if (creditScore > 700 && requestedAmount < 25000) return 'LOW';
        if (creditScore > 600) return 'MEDIUM';
        return 'HIGH';
    }
}

public class LoanApplicationService {
    public Id createApplication(Id accountId, Decimal amount) {
        Loan_Application__c app = new Loan_Application__c(
            Account__c = accountId,
            Amount__c = amount,
            Status__c = 'Pending'
        );
        insert app;
        return app.Id;
    }
}

// FACADE: Simplified interface
public class LoanFacade {
    private CustomerVerificationService verificationService;
    private CreditCheckService creditService;
    private RiskAssessmentService riskService;
    private LoanApplicationService applicationService;

    public LoanFacade() {
        this.verificationService = new CustomerVerificationService();
        this.creditService = new CreditCheckService();
        this.riskService = new RiskAssessmentService();
        this.applicationService = new LoanApplicationService();
    }

    // Single method hides all complexity
    public LoanResult applyForLoan(Id accountId, String customerId, Decimal amount) {
        LoanResult result = new LoanResult();

        // Step 1: Verify customer
        if (!verificationService.verifyIdentity(customerId)) {
            result.success = false;
            result.message = 'Identity verification failed';
            return result;
        }

        // Step 2: Check credit
        Integer creditScore = creditService.getCreditScore(customerId);
        Decimal availableCredit = creditService.getAvailableCredit(customerId);

        if (amount > availableCredit) {
            result.success = false;
            result.message = 'Requested amount exceeds available credit';
            return result;
        }

        // Step 3: Assess risk
        String riskLevel = riskService.assessRisk(creditScore, amount);

        // Step 4: Create application
        result.applicationId = applicationService.createApplication(accountId, amount);
        result.success = true;
        result.riskLevel = riskLevel;
        result.message = 'Loan application submitted successfully';

        return result;
    }

    public class LoanResult {
        public Boolean success;
        public Id applicationId;
        public String riskLevel;
        public String message;
    }
}

Usage

// Client code is simple - no knowledge of subsystems
LoanFacade facade = new LoanFacade();
LoanFacade.LoanResult result = facade.applyForLoan(accountId, 'CUST-123', 15000.00);

if (result.success) {
    System.debug('Loan approved with risk level: ' + result.riskLevel);
} else {
    System.debug('Loan denied: ' + result.message);
}

When to Use

  • Simplifying access to complex subsystems
  • Creating API layers for external integrations
  • Reducing dependencies on multiple services
  • Providing entry points for different client needs

Domain Class Pattern

Purpose

Encapsulate business rules in domain-specific classes, making code read like plain English and enabling reuse across the application.

Implementation

/**
 * Domain class encapsulating Account business rules
 * Rules live here, not scattered across triggers/services
 */
public class AccountRules {

    public static Boolean isStrategicAccount(Account account) {
        return isEnterpriseCustomer(account) &&
               isHighValue(account) &&
               isInTargetMarket(account);
    }

    public static Boolean isEnterpriseCustomer(Account account) {
        return account.Type == 'Enterprise' &&
               account.NumberOfEmployees > 500;
    }

    public static Boolean isHighValue(Account account) {
        return account.AnnualRevenue != null &&
               account.AnnualRevenue > 1000000;
    }

    public static Boolean isInTargetMarket(Account account) {
        Set<String> targetIndustries = new Set<String>{
            'Technology', 'Finance', 'Healthcare'
        };
        Set<String> targetCountries = new Set<String>{
            'United States', 'Canada', 'United Kingdom'
        };

        return targetIndustries.contains(account.Industry) &&
               targetCountries.contains(account.BillingCountry);
    }

    public static Boolean requiresExecutiveApproval(Account account, Decimal dealValue) {
        return isStrategicAccount(account) && dealValue > 500000;
    }

    public static Boolean isEligibleForDiscount(Account account) {
        return account.Customer_Since__c != null &&
               account.Customer_Since__c.monthsBetween(Date.today()) > 24 &&
               isHighValue(account);
    }
}

Usage

// Reads like plain English
public void processOpportunity(Opportunity opp, Account account) {
    if (AccountRules.isStrategicAccount(account)) {
        assignToEnterpriseTeam(opp);
    }

    if (AccountRules.requiresExecutiveApproval(account, opp.Amount)) {
        routeForApproval(opp);
    }

    if (AccountRules.isEligibleForDiscount(account)) {
        applyLoyaltyDiscount(opp);
    }
}

When to Use

  • Business rules are reused across multiple classes
  • Complex boolean logic needs to be readable
  • Rules change frequently (centralized = easier updates)
  • You want trigger/service code to read like business requirements

Relationship to Other Patterns

Pattern Relationship
Selector Domain class uses Selector for data access
Service Service orchestrates, Domain validates
Repository Domain class is data-agnostic
Strategy Domain rules can use Strategy for variations

Abstraction Level Management

Purpose

Ensure each method operates at a consistent level of abstraction. Don't mix high-level orchestration with low-level implementation details.

The Problem

// BAD: Mixed abstraction levels
public void processNewCustomer(Account account) {
    // HIGH-LEVEL: Validation
    validateAccount(account);

    // LOW-LEVEL: String manipulation (doesn't belong here)
    String sanitizedPhone = account.Phone.replaceAll('[^0-9]', '');
    if (sanitizedPhone.length() == 10) {
        sanitizedPhone = '1' + sanitizedPhone;
    }
    account.Phone = '+' + sanitizedPhone;

    // HIGH-LEVEL: Save
    insert account;

    // LOW-LEVEL: HTTP details (doesn't belong here)
    HttpRequest req = new HttpRequest();
    req.setEndpoint('https://api.crm.com/customers');
    req.setMethod('POST');
    req.setHeader('Content-Type', 'application/json');
    req.setBody(JSON.serialize(account));
    Http http = new Http();
    HttpResponse res = http.send(req);

    // HIGH-LEVEL: Notification
    sendWelcomeEmail(account);
}

The Solution

// GOOD: Consistent high-level abstraction
public void processNewCustomer(Account account) {
    validateAccount(account);
    normalizePhoneNumber(account);
    insert account;
    syncToExternalCRM(account);
    sendWelcomeEmail(account);
}

// Low-level details extracted to focused methods
private void normalizePhoneNumber(Account account) {
    if (String.isBlank(account.Phone)) return;

    String digitsOnly = account.Phone.replaceAll('[^0-9]', '');
    if (digitsOnly.length() == 10) {
        digitsOnly = '1' + digitsOnly;
    }
    account.Phone = '+' + digitsOnly;
}

private void syncToExternalCRM(Account account) {
    CRMIntegrationService.syncCustomer(account);
}

Abstraction Layers in Apex

┌─────────────────────────────────────────────────────────────┐
│  TRIGGER LAYER                                              │
│  - Routes events to handlers                                │
│  - No business logic                                        │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  HANDLER/SERVICE LAYER (High-level)                         │
│  - Orchestrates business operations                         │
│  - Coordinates between components                           │
│  - Each step is a method call, not implementation           │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  DOMAIN LAYER (Business rules)                              │
│  - Encapsulates business logic                              │
│  - AccountRules, OpportunityRules, etc.                     │
│  - Pure logic, no infrastructure                            │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  DATA ACCESS LAYER (Low-level)                              │
│  - Selectors for SOQL                                       │
│  - Repositories for DML                                     │
│  - Integration services for external calls                  │
└─────────────────────────────────────────────────────────────┘

Guidelines

Level Should Contain Should NOT Contain
High (Orchestration) Method calls, flow control SOQL, DML, string parsing
Mid (Domain) Business rules, validation HTTP calls, database queries
Low (Data Access) SOQL, DML, HTTP Business decisions

Signs of Mixed Abstraction

  • A method has both [SELECT ...] and business logic
  • HTTP request building next to email sending
  • String manipulation in a method that also updates records
  • Governor limit checks scattered among business rules

Benefits

  • Each method is easier to understand in isolation
  • Methods at the same level can be tested with similar techniques
  • Changes to implementation don't affect orchestration
  • Code reads like a high-level description of the process

Pattern Selection Guide

Need Pattern
Centralize object creation Factory
Abstract data access Repository / Selector
Build complex objects Builder
Single cached instance Singleton
Interchangeable algorithms Strategy
Transactional DML Unit of Work
Add behavior without modification Decorator
React to state changes Observer
Queue/undo operations Command
Simplify complex systems Facade
Encapsulate business rules Domain Class
Consistent method structure Abstraction Levels