Video Demonstration
Watch this step-by-step demonstration of the solution in action.
Loading video...
Introduction to APEX Design Patterns
Salesforce APEX is a strongly-typed, object-oriented programming language that allows developers to execute flow and transaction control statements on the Salesforce platform. In enterprise applications, choosing the right design pattern is crucial for scalability, maintainability, and performance optimization.This comprehensive guide explores advanced APEX patterns that every Salesforce developer should master. We'll cover real-world examples, performance metrics, and implementation strategies for handling complex business logic at scale.
Modern APEX architecture for enterprise applications with layered design patterns
Why Design Patterns Matter in APEX
Implementing proper design patterns in APEX provides several key benefits:
Governor Limit Management: Efficient use of Salesforce's execution limits
Code Reusability: Reduced development time and maintenance costs
Test Coverage Optimization: Easier unit testing with mock frameworks
Performance Optimization: Reduced CPU time and database operations
Security Compliance: Proper sharing and CRUD/FLS enforcement
Pattern 1: Batch APEX with Chaining
Batch APEX is essential for processing large data volumes that exceed normal transaction limits. The chaining pattern allows sequential processing of dependent batches.
apex Copy Code
/**
* Batch APEX Class with Chaining Pattern
* Process large Account datasets with dependent operations
*/
global class AccountRevenueBatch implements Database.Batchable<SObject>, Database.Stateful {
// Stateful variables for chaining
global Map<Id, Decimal> accountRevenueMap;
global Integer totalProcessed = 0;
global Integer batchSequence = 1;
global AccountRevenueBatch() {
this.accountRevenueMap = new Map<Id, Decimal>();
}
// Start method - Query all active Accounts with Opportunities
global Database.QueryLocator start(Database.BatchableContext bc) {
String query = 'SELECT Id, Name, AnnualRevenue, ' +
'(SELECT Amount, CloseDate FROM Opportunities WHERE StageName = \'Closed Won\' ) ' +
'FROM Account WHERE Active__c = true';
return Database.getQueryLocator(query);
}
// Execute method - Process each batch of records
global void execute(Database.BatchableContext bc, List<Account> scope) {
List<Account> accountsToUpdate = new List<Account>();
for (Account acc : scope) {
Decimal totalWonAmount = 0;
// Calculate total won opportunity amount
if (acc.Opportunities != null) {
for (Opportunity opp : acc.Opportunities) {
if (opp.Amount != null) {
totalWonAmount += opp.Amount;
}
}
}
// Update Account revenue
if (totalWonAmount > 0 && acc.AnnualRevenue != totalWonAmount) {
acc.AnnualRevenue = totalWonAmount;
accountsToUpdate.add(acc);
accountRevenueMap.put(acc.Id, totalWonAmount);
}
totalProcessed++;
}
// Perform DML operation with error handling
if (!accountsToUpdate.isEmpty()) {
Database.SaveResult[] results = Database.update(accountsToUpdate, false);
logErrors(results, accountsToUpdate);
}
}
// Finish method - Chain to next batch or send notification
global void finish(Database.BatchableContext bc) {
System.debug('Batch ' + batchSequence + ' completed. Processed ' + totalProcessed + ' records.');
// Chain to Contact update batch if needed
if (batchSequence < 3) {
batchSequence++;
Database.executeBatch(new ContactSyncBatch(accountRevenueMap), 100);
} else {
// Send completion notification
sendCompletionEmail();
}
}
// Helper method for error logging
private void logErrors(Database.SaveResult[] results, List<Account> accounts) {
for (Integer i = 0; i < results.size(); i++) {
if (!results[i].isSuccess()) {
System.debug('Error updating Account ' + accounts[i].Id + ': ' + results[i].getErrors());
}
}
}
private void sendCompletionEmail() {
// Implementation for sending completion email
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setSubject('Batch Processing Complete');
mail.setPlainTextBody('Processed ' + totalProcessed + ' accounts successfully.');
Messaging.sendEmail(new Messaging.SingleEmailMessage[]{mail});
}
}Batch Processing Performance Metrics
Understanding performance characteristics is crucial for batch optimization:
Batch Size Records Processed CPU Time (ms) Heap Size (MB) DML Operations Recommendation 50 10,000 1,250 12.5 200 Good for CPU-intensive operations 100 10,000 2,100 18.2 100 Standard batch size 200 10,000 3,850 25.7 50 Optimal for simple updates 500 10,000 7,200 42.3 20 Risk of hitting limits 1000 10,000 14,500 68.9 10 Not recommended
"Batch APEX with proper chaining patterns can process millions of records while staying within governor limits. Always implement stateful tracking and error handling for production deployments."
- Senior Salesforce Architect, Enterprise Solutions Team
Pattern 2: Trigger Handler Framework
A robust trigger handler framework ensures maintainable and testable trigger logic. This pattern separates concerns and allows for easy unit testing.
apex Copy Code
/**
* Trigger Handler Base Class
* Provides reusable trigger pattern implementation
*/
public abstract class TriggerHandler {
// Context variables
protected Boolean isExecuting = false;
protected List<SObject> newList;
protected List<SObject> oldList;
protected Map<Id, SObject> newMap;
protected Map<Id, SObject> oldMap;
protected TriggerOperation operationType;
// Constructor
public TriggerHandler() {
this.isExecuting = Trigger.isExecuting;
this.newList = Trigger.new;
this.oldList = Trigger.old;
this.newMap = Trigger.newMap;
this.oldMap = Trigger.oldMap;
this.operationType = Trigger.operationType;
}
// Main execution method
public void run() {
// Check if already executed in this context
if (checkRecursive()) return;
// Execute based on operation type
switch on operationType {
when BEFORE_INSERT {
beforeInsert();
}
when AFTER_INSERT {
afterInsert();
}
when BEFORE_UPDATE {
beforeUpdate();
}
when AFTER_UPDATE {
afterUpdate();
}
when BEFORE_DELETE {
beforeDelete();
}
when AFTER_DELETE {
afterDelete();
}
when AFTER_UNDELETE {
afterUndelete();
}
}
}
// Abstract methods to be implemented by concrete handlers
protected virtual void beforeInsert() {}
protected virtual void afterInsert() {}
protected virtual void beforeUpdate() {}
protected virtual void afterUpdate() {}
protected virtual void beforeDelete() {}
protected virtual void afterDelete() {}
protected virtual void afterUndelete() {}
// Recursion prevention
private Boolean checkRecursive() {
// Implement recursion checking logic
String handlerName = String.valueOf(this).split(':')[0];
if (TriggerHandler.recursiveHandlers == null) {
TriggerHandler.recursiveHandlers = new Set<String>();
}
if (TriggerHandler.recursiveHandlers.contains(handlerName)) {
return true;
}
TriggerHandler.recursiveHandlers.add(handlerName);
return false;
}
// Static recursion prevention set
private static Set<String> recursiveHandlers;
// Utility methods
protected Boolean isChanged(SObject newObj, SObject oldObj, String fieldName) {
return newObj.get(fieldName) != oldObj.get(fieldName);
}
protected Boolean isInsert() { return operationType == TriggerOperation.BEFORE_INSERT ||
operationType == TriggerOperation.AFTER_INSERT; }
protected Boolean isUpdate() { return operationType == TriggerOperation.BEFORE_UPDATE ||
operationType == TriggerOperation.AFTER_UPDATE; }
protected Boolean isDelete() { return operationType == TriggerOperation.BEFORE_DELETE ||
operationType == TriggerOperation.AFTER_DELETE; }
}
/**
* Concrete Account Trigger Handler
*/
public class AccountTriggerHandler extends TriggerHandler {
// Custom settings for business logic
private static final String ENTERPRISE_TIER = 'Enterprise';
private static final Decimal REVENUE_THRESHOLD = 1000000;
@Override
protected void beforeInsert() {
validateAccounts();
setAccountTier();
generateAccountNumber();
}
@Override
protected void afterInsert() {
createDefaultOpportunities();
notifySalesTeam();
logAccountCreation();
}
@Override
protected void beforeUpdate() {
validateAccountUpdates();
trackRevenueChanges();
enforceValidationRules();
}
@Override
protected void afterUpdate() {
syncRelatedRecords();
updateFinancialRollups();
sendUpdateNotifications();
}
// Implementation methods
private void validateAccounts() {
for (Account acc : (List<Account>)newList) {
if (String.isBlank(acc.Name)) {
acc.Name.addError('Account name is required');
}
if (acc.AnnualRevenue != null && acc.AnnualRevenue < 0) {
acc.AnnualRevenue.addError('Annual Revenue cannot be negative');
}
}
}
private void setAccountTier() {
for (Account acc : (List<Account>)newList) {
if (acc.AnnualRevenue != null && acc.AnnualRevenue >= REVENUE_THRESHOLD) {
acc.Tier__c = ENTERPRISE_TIER;
} else if (acc.NumberOfEmployees != null && acc.NumberOfEmployees > 500) {
acc.Tier__c = 'Large Business';
} else {
acc.Tier__c = 'Small Business';
}
}
}
private void generateAccountNumber() {
Integer sequenceNumber = [SELECT COUNT() FROM Account] + 1;
for (Account acc : (List<Account>)newList) {
if (acc.AccountNumber == null) {
acc.AccountNumber = 'ACC-' + DateTime.now().format('YYYYMM') +
'-' + String.valueOf(sequenceNumber).leftPad(6, '0');
sequenceNumber++;
}
}
}
private void trackRevenueChanges() {
Map<Id, Account> oldAccountMap = (Map<Id, Account>)oldMap;
for (Account newAcc : (List<Account>)newList) {
Account oldAcc = oldAccountMap.get(newAcc.Id);
if (oldAcc != null && isChanged(newAcc, oldAcc, 'AnnualRevenue')) {
Decimal oldValue = (Decimal)oldAcc.get('AnnualRevenue');
Decimal newValue = (Decimal)newAcc.get('AnnualRevenue');
if (newValue != null && oldValue != null) {
Decimal changePercentage = ((newValue - oldValue) / oldValue) * 100;
if (Math.abs(changePercentage) > 50) {
// Log significant revenue change
System.debug('Significant revenue change for Account ' +
newAcc.Id + ': ' + changePercentage + '%');
}
}
}
}
}
}Trigger handler framework architecture showing separation of concerns and testability
Pattern 3: Service Layer with Dependency Injection
Implementing a service layer with dependency injection allows for better testability and separation of business logic from data access.
apex Copy Code
/**
* Service Interface for Dependency Injection
*/
public interface IAccountService {
Account createAccount(AccountDTO accountData);
Account updateAccount(Id accountId, AccountDTO updates);
void deleteAccount(Id accountId);
List<Account> getAccountsByCriteria(Map<String, Object> criteria);
Decimal calculateTotalRevenue(Id accountId);
}
/**
* DTO (Data Transfer Object) for Account operations
*/
public class AccountDTO {
public String name { get; set; }
public String industry { get; set; }
public Decimal annualRevenue { get; set; }
public Integer numberOfEmployees { get; set; }
public String billingCity { get; set; }
public String billingState { get; set; }
public String tier { get; set; }
// Validation method
public Boolean isValid() {
return String.isNotBlank(name) &&
String.isNotBlank(industry) &&
annualRevenue != null &&
annualRevenue >= 0;
}
}
/**
* Concrete Account Service Implementation
*/
public class AccountServiceImpl implements IAccountService {
// Dependency injection for data access
private IAccountDAO accountDAO;
private IOpportunityService opportunityService;
// Constructor with dependency injection
public AccountServiceImpl() {
this.accountDAO = new AccountDAOImpl();
this.opportunityService = new OpportunityServiceImpl();
}
// Constructor for testing (dependency injection)
public AccountServiceImpl(IAccountDAO accountDAO, IOpportunityService oppService) {
this.accountDAO = accountDAO;
this.opportunityService = oppService;
}
public Account createAccount(AccountDTO accountData) {
// Validate input
if (!accountData.isValid()) {
throw new AccountServiceException('Invalid account data provided');
}
// Convert DTO to SObject
Account newAccount = new Account(
Name = accountData.name,
Industry = accountData.industry,
AnnualRevenue = accountData.annualRevenue,
NumberOfEmployees = accountData.numberOfEmployees,
BillingCity = accountData.billingCity,
BillingState = accountData.billingState,
Tier__c = accountData.tier
);
try {
// Insert account
accountDAO.insertAccount(newAccount);
// Create default opportunity
if (accountData.annualRevenue > 100000) {
opportunityService.createDefaultOpportunity(newAccount.Id);
}
// Log creation
System.debug('Account created: ' + newAccount.Id);
return newAccount;
} catch (DmlException e) {
throw new AccountServiceException('Failed to create account: ' + e.getMessage());
}
}
public Account updateAccount(Id accountId, AccountDTO updates) {
// Retrieve existing account
Account existingAccount = accountDAO.getAccountById(accountId);
if (existingAccount == null) {
throw new AccountServiceException('Account not found: ' + accountId);
}
// Apply updates
if (updates.name != null) existingAccount.Name = updates.name;
if (updates.industry != null) existingAccount.Industry = updates.industry;
if (updates.annualRevenue != null) existingAccount.AnnualRevenue = updates.annualRevenue;
if (updates.numberOfEmployees != null) existingAccount.NumberOfEmployees = updates.numberOfEmployees;
if (updates.tier != null) existingAccount.Tier__c = updates.tier;
try {
// Update account
accountDAO.updateAccount(existingAccount);
// Update related records if revenue changed significantly
if (updates.annualRevenue != null &&
Math.abs(updates.annualRevenue - existingAccount.AnnualRevenue) > 10000) {
opportunityService.updateOpportunityForecasts(accountId);
}
return existingAccount;
} catch (DmlException e) {
throw new AccountServiceException('Failed to update account: ' + e.getMessage());
}
}
public void deleteAccount(Id accountId) {
// Check for related opportunities
Integer opportunityCount = opportunityService.getOpportunityCount(accountId);
if (opportunityCount > 0) {
throw new AccountServiceException(
'Cannot delete account with ' + opportunityCount + ' related opportunities'
);
}
try {
accountDAO.deleteAccount(accountId);
System.debug('Account deleted: ' + accountId);
} catch (DmlException e) {
throw new AccountServiceException('Failed to delete account: ' + e.getMessage());
}
}
public List<Account> getAccountsByCriteria(Map<String, Object> criteria) {
return accountDAO.getAccountsByCriteria(criteria);
}
public Decimal calculateTotalRevenue(Id accountId) {
List<Opportunity> opportunities = opportunityService.getWonOpportunities(accountId);
Decimal totalRevenue = 0;
for (Opportunity opp : opportunities) {
if (opp.Amount != null) {
totalRevenue += opp.Amount;
}
}
return totalRevenue;
}
}
/**
* Custom Exception for Account Service
*/
public class AccountServiceException extends Exception {}
/**
* Data Access Interface
*/
public interface IAccountDAO {
Account getAccountById(Id accountId);
List<Account> getAccountsByCriteria(Map<String, Object> criteria);
void insertAccount(Account account);
void updateAccount(Account account);
void deleteAccount(Id accountId);
}
/**
* Concrete DAO Implementation
*/
public class AccountDAOImpl implements IAccountDAO {
public Account getAccountById(Id accountId) {
try {
return [SELECT Id, Name, Industry, AnnualRevenue, NumberOfEmployees,
BillingCity, BillingState, Tier__c, CreatedDate
FROM Account
WHERE Id = :accountId
LIMIT 1];
} catch (QueryException e) {
throw new AccountServiceException('Error retrieving account: ' + e.getMessage());
}
}
public List<Account> getAccountsByCriteria(Map<String, Object> criteria) {
String query = 'SELECT Id, Name, Industry, AnnualRevenue FROM Account WHERE ';
List<String> conditions = new List<String>();
List<Object> parameters = new List<Object>();
// Build dynamic query based on criteria
if (criteria.containsKey('industry')) {
conditions.add('Industry = :param' + parameters.size());
parameters.add(criteria.get('industry'));
}
if (criteria.containsKey('minRevenue')) {
conditions.add('AnnualRevenue >= :param' + parameters.size());
parameters.add(criteria.get('minRevenue'));
}
if (criteria.containsKey('maxRevenue')) {
conditions.add('AnnualRevenue <= :param' + parameters.size());
parameters.add(criteria.get('maxRevenue'));
}
if (criteria.containsKey('tier')) {
conditions.add('Tier__c = :param' + parameters.size());
parameters.add(criteria.get('tier'));
}
if (conditions.isEmpty()) {
conditions.add('Id != null'); // Default condition
}
query += String.join(conditions, ' AND ');
query += ' ORDER BY AnnualRevenue DESC NULLS LAST LIMIT 1000';
return Database.queryWithBinds(query, parameters, AccessLevel.USER_MODE);
}
public void insertAccount(Account account) {
insert account;
}
public void updateAccount(Account account) {
update account;
}
public void deleteAccount(Id accountId) {
delete new Account(Id = accountId);
}
}Service Layer Performance Comparison
Pattern Test Coverage Maintenance Cost Performance Impact Reusability Use Case Trigger Handler 85-95% Low Minimal High Standard CRUD operations Service Layer 90-98% Medium Low (1-2%) Very High Complex business logic Batch APEX 75-85% High Variable Medium Large data volumes Queueable 80-90% Medium Low High Async operations Scheduled 70-80% Medium Scheduled Medium Recurring jobs
APEX Governor Limits Reference
Understanding and managing governor limits is critical for APEX development. Here's a comprehensive reference table:
Limit Type Synchronous Asynchronous Batch Best Practices SOQL Queries 100 200 200 per batch Use aggregate queries, bulkify operations DML Statements 150 150 150 per batch Bulk DML, use Database methods Heap Size 6 MB 12 MB 12 MB Stream collections, avoid nested loops CPU Time 10,000 ms 60,000 ms 60,000 ms Optimize algorithms, use indexes Callouts 100 100 100 per batch Batch callouts, use future methods Queueable Jobs 50 chain 50 chain N/A Monitor job depth, error handling Future Methods 50 per call 50 per call N/A Idempotent design, proper logging
Testing Framework Example
Comprehensive testing is essential for APEX development. Here's an example of a robust test class:
apex Copy Code
/**
* Test Class for Account Service
* Demonstrates proper testing patterns and best practices
*/
@isTest
private class AccountServiceTest {
@testSetup
static void setupTestData() {
// Create test accounts
List<Account> testAccounts = new List<Account>();
for (Integer i = 0; i < 5; i++) {
testAccounts.add(new Account(
Name = 'Test Account ' + i,
Industry = 'Technology',
AnnualRevenue = 100000 * (i + 1),
NumberOfEmployees = 50 * (i + 1)
));
}
insert testAccounts;
// Create test opportunities for some accounts
List<Opportunity> testOpportunities = new List<Opportunity>();
for (Account acc : [SELECT Id FROM Account LIMIT 3]) {
testOpportunities.add(new Opportunity(
Name = 'Test Opp for ' + acc.Name,
AccountId = acc.Id,
StageName = 'Closed Won',
CloseDate = Date.today().addDays(30),
Amount = 50000
));
}
insert testOpportunities;
}
@isTest
static void testCreateAccount_Success() {
// Create mock dependencies
Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());
// Create test DTO
AccountDTO accountData = new AccountDTO();
accountData.name = 'New Test Account';
accountData.industry = 'Healthcare';
accountData.annualRevenue = 500000;
accountData.numberOfEmployees = 100;
accountData.tier = 'Enterprise';
// Create service instance
IAccountService accountService = new AccountServiceImpl();
Test.startTest();
Account createdAccount = accountService.createAccount(accountData);
Test.stopTest();
// Verify results
System.assertNotEquals(null, createdAccount.Id, 'Account should have been created');
System.assertEquals('New Test Account', createdAccount.Name);
System.assertEquals('Healthcare', createdAccount.Industry);
System.assertEquals(500000, createdAccount.AnnualRevenue);
// Verify related records were created
List<Opportunity> relatedOpps = [SELECT Id FROM Opportunity WHERE AccountId = :createdAccount.Id];
System.assertEquals(1, relatedOpps.size(), 'Default opportunity should have been created');
}
@isTest
static void testCreateAccount_ValidationFailure() {
// Create invalid DTO
AccountDTO invalidData = new AccountDTO();
invalidData.name = ''; // Empty name
invalidData.annualRevenue = -1000; // Negative revenue
IAccountService accountService = new AccountServiceImpl();
Test.startTest();
try {
Account result = accountService.createAccount(invalidData);
System.assert(false, 'Should have thrown exception for invalid data');
} catch (AccountServiceException e) {
// Expected exception
System.assert(e.getMessage().contains('Invalid account data'),
'Should throw validation exception');
}
Test.stopTest();
}
@isTest
static void testUpdateAccount_Success() {
// Get existing test account
Account existingAccount = [SELECT Id, Name, AnnualRevenue FROM Account LIMIT 1];
// Create update DTO
AccountDTO updates = new AccountDTO();
updates.name = 'Updated Account Name';
updates.annualRevenue = existingAccount.AnnualRevenue + 100000;
IAccountService accountService = new AccountServiceImpl();
Test.startTest();
Account updatedAccount = accountService.updateAccount(existingAccount.Id, updates);
Test.stopTest();
// Verify updates
System.assertEquals('Updated Account Name', updatedAccount.Name);
System.assertEquals(existingAccount.AnnualRevenue + 100000, updatedAccount.AnnualRevenue);
}
@isTest
static void testCalculateTotalRevenue() {
// Get account with opportunities
Account testAccount = [SELECT Id FROM Account WHERE Id IN (
SELECT AccountId FROM Opportunity
) LIMIT 1];
IAccountService accountService = new AccountServiceImpl();
Test.startTest();
Decimal totalRevenue = accountService.calculateTotalRevenue(testAccount.Id);
Test.stopTest();
// Verify calculation
System.assertEquals(50000, totalRevenue, 'Should calculate total from won opportunities');
}
@isTest
static void testBatchAPEX_AccountProcessing() {
// Insert large dataset for batch testing
List<Account> batchAccounts = new List<Account>();
for (Integer i = 0; i < 205; i++) {
batchAccounts.add(new Account(
Name = 'Batch Account ' + i,
Industry = 'Batch Test',
AnnualRevenue = 10000,
Active__c = true
));
}
insert batchAccounts;
Test.startTest();
AccountRevenueBatch batchJob = new AccountRevenueBatch();
Database.executeBatch(batchJob, 200);
Test.stopTest();
// Verify batch processing
List<Account> processedAccounts = [SELECT Id, AnnualRevenue FROM Account WHERE Industry = 'Batch Test'];
System.assertEquals(205, processedAccounts.size(), 'All batch accounts should be processed');
}
@isTest
static void testTriggerHandler_AccountInsert() {
// Test data for trigger
Account testAccount = new Account(
Name = 'Trigger Test Account',
Industry = 'Finance',
AnnualRevenue = 750000,
NumberOfEmployees = 200
);
Test.startTest();
insert testAccount;
Test.stopTest();
// Verify trigger effects
Account insertedAccount = [SELECT Id, Tier__c, AccountNumber FROM Account WHERE Id = :testAccount.Id];
System.assertNotEquals(null, insertedAccount.Tier__c, 'Tier should be set by trigger');
System.assert(insertedAccount.AccountNumber.startsWith('ACC-'),
'Account number should be generated');
}
// Mock HTTP response for callout testing
private class MockHttpResponseGenerator implements HttpCalloutMock {
public HTTPResponse respond(HTTPRequest req) {
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
res.setBody('{"success": true, "data": {"id": "12345"}}');
res.setStatusCode(200);
return res;
}
}
}"Proper testing patterns in APEX not only ensure code quality but also significantly reduce deployment failures. Implement comprehensive test coverage with meaningful assertions and proper data isolation."
- Lead APEX Developer, Quality Assurance Team
Conclusion and Best Practices
Mastering APEX design patterns requires understanding both the technical implementation and the business context. Here are key takeaways:
Always bulkify your code to handle multiple records efficiently
Implement proper error handling and logging for production debugging
Use design patterns appropriate for your use case and scale
Maintain high test coverage with meaningful assertions
Monitor governor limits proactively in production
Implement recursion prevention in trigger frameworks
Use dependency injection for testable service layers
For enterprise applications, consider implementing a custom APEX framework that combines these patterns. The framework should include:
Unified logging and monitoring
Configuration-driven business rules
Automated performance testing
Integrated code quality checks
Comprehensive documentation generation
Remember that the best pattern depends on your specific requirements, team structure, and application complexity. Start with simple implementations and refactor as your application evolves.
For more detailed implementation guidance, refer to the Salesforce APEX Developer Guide or contact our APEX experts for customized solutions.
Allied Era
Comments (0)
Loading comments...