Salesforce Development Interview Questions & Answers Part 2

This comprehensive guide covers essential Apex development interview questions ranging from basic concepts to advanced scenarios.

Q16. What is recursion in triggers and how do you prevent it?

Recursion in Triggers: Recursion occurs when a trigger fires, performs DML operations that cause the same trigger to fire again, creating an infinite loop. This can lead to exceeding governor limits and runtime errors.

Common Causes:

  • Trigger updates records of the same object
  • Multiple triggers on the same object
  • Workflow rules or Process Builder that update records

Prevention Methods:

1. Static Boolean Flag (Most Common)

public class TriggerHandler {
    public static Boolean isFirstRun = true;
}

trigger AccountTrigger on Account (after update) {
    if (TriggerHandler.isFirstRun) {
        TriggerHandler.isFirstRun = false;
        // Your trigger logic here
    }
}

2. Set to Track Processed Records

public class TriggerHandler {
    public static Set<Id> processedIds = new Set<Id>();
}

trigger AccountTrigger on Account (after update) {
    for (Account acc : Trigger.new) {
        if (!TriggerHandler.processedIds.contains(acc.Id)) {
            TriggerHandler.processedIds.add(acc.Id);
            // Your trigger logic here
        }
    }
}

3. Context-Specific Flags

public class TriggerHandler {
    public static Boolean skipAccountTrigger = false;
    public static Boolean skipContactTrigger = false;
}

Q17. What is a custom exception in Apex?

Custom Exception: A custom exception is a user-defined exception class that extends the built-in Exception class. It allows developers to create specific exception types for their application’s unique error scenarios.

Creating Custom Exceptions:

public class MyCustomException extends Exception {
    // Custom exception class
}

// Another example with more detail
public class InsufficientFundsException extends Exception {
    private Decimal balance;
    
    public InsufficientFundsException(Decimal currentBalance) {
        this.balance = currentBalance;
    }
    
    public Decimal getBalance() {
        return balance;
    }
}

Using Custom Exceptions:

public class BankAccount {
    public Decimal balance;
    
    public void withdraw(Decimal amount) {
        if (amount > balance) {
            throw new InsufficientFundsException(balance);
        }
        balance -= amount;
    }
}

// Catching custom exception
try {
    BankAccount acc = new BankAccount();
    acc.balance = 100;
    acc.withdraw(150);
} catch (InsufficientFundsException e) {
    System.debug('Insufficient funds. Current balance: ' + e.getBalance());
}

Benefits:

  • Better error handling and code readability
  • Specific exception types for different scenarios
  • Easier debugging and maintenance
  • Clearer communication of error conditions

Q18. What are test classes and why are they important?

Test Classes: Test classes are special Apex classes annotated with @isTest that contain test methods to verify the functionality of your code. They simulate user interactions and validate that the code works as expected.

Importance:

  1. Deployment Requirement:
    • Minimum 75% code coverage required for production deployment
    • All triggers must have some test coverage
  2. Quality Assurance:
    • Ensures code works as intended
    • Catches bugs before deployment
    • Validates business logic
  3. Regression Testing:
    • Ensures new changes don’t break existing functionality
    • Provides confidence when refactoring code
  4. Documentation:
    • Serves as examples of how to use the code
    • Documents expected behavior

Example Test Class:

@isTest
private class AccountTriggerTest {
    
    @isTest
    static void testAccountCreation() {
        // Test data setup
        Account testAccount = new Account(
            Name = 'Test Account',
            Industry = 'Technology'
        );
        
        // Test execution
        Test.startTest();
        insert testAccount;
        Test.stopTest();
        
        // Assertions
        Account insertedAccount = [SELECT Id, Name, Industry FROM Account WHERE Id = :testAccount.Id];
        System.assertEquals('Test Account', insertedAccount.Name);
        System.assertEquals('Technology', insertedAccount.Industry);
    }
}

Best Practices:

  • Use Test.startTest() and Test.stopTest() for governor limit reset
  • Create test data in the test method or use @TestSetup
  • Test positive, negative, and bulk scenarios
  • Use assertions to validate results
  • Never use @isTest(SeeAllData=true) unless absolutely necessary

Q19. What is the @TestSetup annotation?

@TestSetup Annotation: The @TestSetup annotation is used to create test data once that can be reused by all test methods in a test class. This improves test performance by reducing the number of DML operations.

Key Features:

  • Executes before each test method
  • Creates common test data
  • Reduces test execution time
  • Data is rolled back after each test method
  • Only one @TestSetup method allowed per test class

Example:

@isTest
private class AccountServiceTest {
    
    @TestSetup
    static void setupTestData() {
        // Create test data once for all test methods
        List<Account> accounts = new List<Account>();
        for (Integer i = 0; i < 10; i++) {
            accounts.add(new Account(
                Name = 'Test Account ' + i,
                Industry = 'Technology'
            ));
        }
        insert accounts;
        
        // Create related contacts
        List<Contact> contacts = new List<Contact>();
        for (Account acc : accounts) {
            contacts.add(new Contact(
                LastName = 'Test Contact',
                AccountId = acc.Id
            ));
        }
        insert contacts;
    }
    
    @isTest
    static void testMethod1() {
        // Query the test data created in @TestSetup
        List<Account> accounts = [SELECT Id, Name FROM Account];
        System.assertEquals(10, accounts.size());
        // Test logic here
    }
    
    @isTest
    static void testMethod2() {
        // Same test data is available here
        List<Contact> contacts = [SELECT Id, LastName FROM Contact];
        System.assertEquals(10, contacts.size());
        // Different test logic here
    }
}

Benefits:

  • Better performance for test classes
  • Reduces code duplication
  • Cleaner test methods
  • Easier maintenance

Important Notes:

  • Test data created in @TestSetup is committed and visible to all test methods
  • Each test method sees the same initial state
  • Changes made by one test method don’t affect other test methods
  • @TestSetup method itself is not a test method

Q20. What is a scheduled Apex class?

Scheduled Apex: A scheduled Apex class allows you to run Apex code at specified times using the Salesforce scheduler. It implements the Schedulable interface and is useful for performing regular batch operations.

Implementation:

global class ScheduledAccountUpdate implements Schedulable {
    
    global void execute(SchedulableContext sc) {
        // Code to be executed on schedule
        List<Account> accounts = [SELECT Id, Name, Status__c 
                                 FROM Account 
                                 WHERE Status__c = 'Pending' 
                                 LIMIT 200];
        
        for (Account acc : accounts) {
            acc.Status__c = 'Active';
        }
        
        update accounts;
    }
}

Scheduling the Class:

1. Through Apex Code:

// Schedule to run daily at 2 AM
String cronExpression = '0 0 2 * * ?';
String jobName = 'Daily Account Update';
ScheduledAccountUpdate scheduleJob = new ScheduledAccountUpdate();
System.schedule(jobName, cronExpression, scheduleJob);

2. Through UI:

  • Setup → Apex Classes → Schedule Apex

CRON Expression Format:

Seconds Minutes Hours Day_of_month Month Day_of_week Optional_year
0       0       2     *            *     ?          *

Examples:
'0 0 2 * * ?' - Daily at 2 AM
'0 0 12 ? * MON-FRI' - Weekdays at noon
'0 0 0 1 * ?' - First day of every month at midnight
'0 0 8,20 * * ?' - Twice daily at 8 AM and 8 PM

Limitations:

  • Maximum 100 scheduled Apex jobs at one time
  • Cannot schedule for intervals less than 1 hour apart
  • Use Batch Apex for processing large data volumes

Best Practices:

  • Keep the execute method lightweight
  • Use Batch Apex for bulk processing
  • Implement error handling
  • Log important information
  • Monitor scheduled jobs regularly

Q21. What is the difference between Public, Private, Global, and Protected?

These are access modifiers that control the visibility and accessibility of classes, methods, and variables in Apex.

Private

  • Default access level for inner classes
  • Accessible only within the defining class
  • Not inherited by subclasses
  • Most restrictive
public class MyClass {
    private String secretData = 'Hidden';
    
    private void privateMethod() {
        // Only accessible within MyClass
    }
}

Public

  • Default access level for outer classes
  • Accessible within the namespace
  • Can be accessed by any Apex code in the same application
  • Not accessible in managed packages
public class MyClass {
    public String publicData = 'Visible in namespace';
    
    public void publicMethod() {
        // Accessible within the same namespace
    }
}

Global

  • Most permissive access level
  • Accessible across all namespaces
  • Required for web services, managed packages, and APIs
  • Cannot be used for interfaces
  • More restrictive than public in terms of changes (harder to modify in managed packages)
global class GlobalWebService {
    global String globalData = 'Accessible everywhere';
    
    webservice static String getInfo() {
        // Web service method must be global
        return 'Information';
    }
}

Protected

  • Accessible within the defining class and its subclasses
  • Cannot be used for outer classes, only for methods and variables
  • Useful for inheritance scenarios
public class ParentClass {
    protected String protectedData = 'Visible to children';
    
    protected void protectedMethod() {
        // Accessible in this class and subclasses
    }
}

public class ChildClass extends ParentClass {
    public void accessProtected() {
        // Can access protectedData and protectedMethod
        System.debug(protectedData);
        protectedMethod();
    }
}

Comparison Table:

ModifierSame ClassSame NamespaceDifferent NamespaceSubclass
Private
Protected
Public
Global

Example Combining All:

global class AccessModifierExample {
    // Different access levels
    private String privateVar = 'Private';
    protected String protectedVar = 'Protected';
    public String publicVar = 'Public';
    global String globalVar = 'Global';
    
    // Methods with different access
    private void privateMethod() {
        System.debug('Private method');
    }
    
    protected void protectedMethod() {
        System.debug('Protected method');
    }
    
    public void publicMethod() {
        System.debug('Public method');
    }
    
    global void globalMethod() {
        System.debug('Global method');
    }
}

Best Practices:

  • Use private by default for encapsulation
  • Use public for methods/variables that need to be accessed in the same namespace
  • Use global only when necessary (web services, managed packages)
  • Use protected for inheritance hierarchies
  • Follow the principle of least privilege

Q23. What is Dynamic Apex?

Dynamic Apex: Dynamic Apex enables developers to write code that can adapt to changes in an organization’s schema at runtime. It allows you to access sObject and field metadata dynamically, without hardcoding API names.

Key Components:

1. Schema Class

Provides methods to describe sObjects and fields.

2. Dynamic SOQL

Execute queries built as strings at runtime.

3. Dynamic DML

Perform DML operations on dynamically determined objects.

Use Cases and Examples:

1. Dynamic Field Access

// Get field value dynamically
Account acc = new Account(Name = 'Test');
String fieldName = 'Name';
String fieldValue = (String)acc.get(fieldName);
System.debug('Field Value: ' + fieldValue);

// Set field value dynamically
acc.put('Industry', 'Technology');

2. Dynamic SOQL

// Build query dynamically
String objectName = 'Account';
String fieldName = 'Name';
String whereCondition = 'Industry = \'Technology\'';

String query = 'SELECT Id, ' + fieldName + 
               ' FROM ' + objectName + 
               ' WHERE ' + whereCondition;

List<sObject> records = Database.query(query);

for (sObject record : records) {
    System.debug('Record: ' + record.get(fieldName));
}

3. Dynamic sObject Creation

// Create sObject dynamically
String objectName = 'Account';
Schema.SObjectType objectType = Schema.getGlobalDescribe().get(objectName);
sObject newRecord = objectType.newSObject();

newRecord.put('Name', 'Dynamic Account');
newRecord.put('Industry', 'Technology');

insert newRecord;

4. Describing Objects and Fields

// Get all objects in the org
Map<String, Schema.SObjectType> globalDescribe = Schema.getGlobalDescribe();

for (String objectName : globalDescribe.keySet()) {
    System.debug('Object: ' + objectName);
}

// Describe a specific object
Schema.SObjectType accountType = Schema.getGlobalDescribe().get('Account');
Schema.DescribeSObjectResult accountDescribe = accountType.getDescribe();

System.debug('Label: ' + accountDescribe.getLabel());
System.debug('Plural Label: ' + accountDescribe.getLabelPlural());
System.debug('Is Custom: ' + accountDescribe.isCustom());

// Get all fields of an object
Map<String, Schema.SObjectField> fieldMap = accountDescribe.fields.getMap();

for (String fieldName : fieldMap.keySet()) {
    Schema.DescribeFieldResult fieldDescribe = fieldMap.get(fieldName).getDescribe();
    System.debug('Field: ' + fieldDescribe.getLabel() + 
                 ', Type: ' + fieldDescribe.getType());
}

5. Dynamic Picklist Values

// Get picklist values dynamically
public List<String> getPicklistValues(String objectName, String fieldName) {
    List<String> picklistValues = new List<String>();
    
    Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
    Schema.DescribeSObjectResult objDescribe = objType.getDescribe();
    
    Schema.SObjectField field = objDescribe.fields.getMap().get(fieldName);
    Schema.DescribeFieldResult fieldDescribe = field.getDescribe();
    
    for (Schema.PicklistEntry entry : fieldDescribe.getPicklistValues()) {
        if (entry.isActive()) {
            picklistValues.add(entry.getValue());
        }
    }
    
    return picklistValues;
}

6. Dynamic RecordType Access

// Get RecordType Id dynamically
public Id getRecordTypeId(String objectName, String recordTypeName) {
    Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
    Schema.DescribeSObjectResult objDescribe = objType.getDescribe();
    
    Map<String, Schema.RecordTypeInfo> recordTypeMap = objDescribe.getRecordTypeInfosByName();
    
    if (recordTypeMap.containsKey(recordTypeName)) {
        return recordTypeMap.get(recordTypeName).getRecordTypeId();
    }
    
    return null;
}

7. Generic Field Update Method

public void updateField(sObject record, String fieldName, Object fieldValue) {
    record.put(fieldName, fieldValue);
    update record;
}

// Usage
Account acc = [SELECT Id FROM Account LIMIT 1];
updateField(acc, 'Industry', 'Healthcare');

Benefits:

  • Flexible and adaptable code
  • Reduces maintenance when schema changes
  • Useful for building generic/reusable components
  • Essential for managed packages
  • Enables metadata-driven development

Considerations:

  • Slightly slower than static references
  • More complex and harder to debug
  • No compile-time validation
  • Use only when necessary

Q24. What is Schema class in Apex?

Schema Class: The Schema class provides methods to access and describe your organization’s data model (schema) at runtime. It’s a key component of Dynamic Apex that allows you to work with sObjects, fields, and their metadata programmatically.

Main Components:

1. Schema.getGlobalDescribe()

Returns a map of all sObject types in the organization.

Map<String, Schema.SObjectType> globalDescribe = Schema.getGlobalDescribe();

// Iterate through all objects
for (String objectName : globalDescribe.keySet()) {
    System.debug('Object API Name: ' + objectName);
}

// Get a specific object
Schema.SObjectType accountType = globalDescribe.get('Account');

2. Schema.SObjectType

Represents an sObject type.

Schema.SObjectType accountType = Account.SObjectType;
// or
Schema.SObjectType accountType = Schema.getGlobalDescribe().get('Account');

// Get sObject describe result
Schema.DescribeSObjectResult accountDescribe = accountType.getDescribe();

3. Schema.DescribeSObjectResult

Contains metadata about an sObject.

Schema.DescribeSObjectResult accountDescribe = Account.SObjectType.getDescribe();

// Common properties
String label = accountDescribe.getLabel(); // 'Account'
String pluralLabel = accountDescribe.getLabelPlural(); // 'Accounts'
String apiName = accountDescribe.getName(); // 'Account'
Boolean isCustom = accountDescribe.isCustom(); // false
Boolean isCreateable = accountDescribe.isCreateable();
Boolean isUpdateable = accountDescribe.isUpdateable();
Boolean isDeletable = accountDescribe.isDeletable();

// Get all fields
Map<String, Schema.SObjectField> fieldMap = accountDescribe.fields.getMap();

4. Schema.SObjectField

Represents a field on an sObject.

Schema.SObjectField nameField = Account.Name;
// or
Schema.SObjectField nameField = Account.SObjectType.getDescribe()
                                       .fields.getMap().get('Name');

5. Schema.DescribeFieldResult

Contains metadata about a field.

Schema.DescribeFieldResult fieldDescribe = Account.Name.getDescribe();

// Common properties
String fieldLabel = fieldDescribe.getLabel(); // 'Account Name'
String fieldApiName = fieldDescribe.getName(); // 'Name'
Schema.DisplayType fieldType = fieldDescribe.getType(); // STRING
Integer fieldLength = fieldDescribe.getLength(); // 255
Boolean isRequired = !fieldDescribe.isNillable();
Boolean isCustomField = fieldDescribe.isCustom();
Boolean isUnique = fieldDescribe.isUnique();

Practical Examples:

1. Check Object and Field Permissions

public class PermissionChecker {
    
    public static Boolean canCreateAccount() {
        return Schema.sObjectType.Account.isCreateable();
    }
    
    public static Boolean canUpdateAccount() {
        return Schema.sObjectType.Account.isUpdateable();
    }
    
    public static Boolean canDeleteAccount() {
        return Schema.sObjectType.Account.isDeletable();
    }
    
    public static Boolean canAccessAccountName() {
        return Schema.sObjectType.Account.fields.Name.isAccessible();
    }
    
    public static Boolean canUpdateAccountName() {
        return Schema.sObjectType.Account.fields.Name.isUpdateable();
    }
}

2. Get All Updateable Fields

public List<String> getUpdateableFields(String objectName) {
    List<String> updateableFields = new List<String>();
    
    Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
    Map<String, Schema.SObjectField> fieldMap = objType.getDescribe().fields.getMap();
    
    for (String fieldName : fieldMap.keySet()) {
        Schema.DescribeFieldResult fieldDescribe = fieldMap.get(fieldName).getDescribe();
        if (fieldDescribe.isUpdateable()) {
            updateableFields.add(fieldName);
        }
    }
    
    return updateableFields;
}

3. Field Type Validation

public Boolean isFieldType(String objectName, String fieldName, Schema.DisplayType expectedType) {
    Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
    Schema.SObjectField field = objType.getDescribe().fields.getMap().get(fieldName);
    Schema.DescribeFieldResult fieldDescribe = field.getDescribe();
    
    return fieldDescribe.getType() == expectedType;
}

// Usage
Boolean isPhoneField = isFieldType('Account', 'Phone', Schema.DisplayType.PHONE);

4. Get Dependent Picklist Values

public Map<String, List<String>> getDependentPicklistValues(
    String objectName, 
    String controllingField, 
    String dependentField) {
    
    Map<String, List<String>> dependencyMap = new Map<String, List<String>>();
    
    Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
    Schema.SObjectField depField = objType.getDescribe().fields.getMap().get(dependentField);
    Schema.DescribeFieldResult fieldDescribe = depField.getDescribe();
    
    // Get controlling field values
    Schema.SObjectField ctrlField = objType.getDescribe().fields.getMap().get(controllingField);
    List<Schema.PicklistEntry> ctrlValues = ctrlField.getDescribe().getPicklistValues();
    
    // Map dependent values to controlling values
    for (Schema.PicklistEntry entry : fieldDescribe.getPicklistValues()) {
        if (entry.isActive()) {
            // Logic to map dependent to controlling values
            // This requires additional processing based on validFor property
        }
    }
    
    return dependencyMap;
}

5. Build Dynamic Query with All Fields

public String buildSelectAllQuery(String objectName) {
    Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
    Map<String, Schema.SObjectField> fieldMap = objType.getDescribe().fields.getMap();
    
    List<String> fields = new List<String>();
    for (String fieldName : fieldMap.keySet()) {
        if (fieldMap.get(fieldName).getDescribe().isAccessible()) {
            fields.add(fieldName);
        }
    }
    
    return 'SELECT ' + String.join(fields, ',') + ' FROM ' + objectName;
}

6. Get Child Relationships

public List<String> getChildRelationships(String objectName) {
    List<String> relationships = new List<String>();
    
    Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
    Schema.DescribeSObjectResult objDescribe = objType.getDescribe();
    
    for (Schema.ChildRelationship rel : objDescribe.getChildRelationships()) {
        if (rel.getRelationshipName() != null) {
            relationships.add(rel.getRelationshipName());
        }
    }
    
    return relationships;
}

Common Schema Methods Summary:

MethodPurpose
Schema.getGlobalDescribe()Get all sObjects in org
sObject.SObjectType.getDescribe()Get sObject metadata
field.getDescribe()Get field metadata
getLabel()Get user-friendly label
getName()Get API name
isCustom()Check if custom
isAccessible()Check read permission
isCreateable()Check create permission
isUpdateable()Check update permission
isDeletable()Check delete permission

Benefits:

  • Security: Check CRUD/FLS at runtime
  • Flexibility: Build dynamic solutions
  • Maintainability: Adapt to schema changes
  • Reusability: Create generic utilities

Q25. What are the best practices for writing triggers?

Triggers are powerful but can lead to performance issues and maintenance challenges if not written properly. Here are the best practices:

1. One Trigger Per Object

  • Have only one trigger per object
  • Use a handler class to manage logic
  • Easier to maintain and debug
trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
    AccountTriggerHandler.handleTrigger();
}

2. Use Trigger Handler Pattern

  • Separate business logic from trigger
  • Better code organization
  • Easier to test
// Trigger
trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
    new AccountTriggerHandler().run();
}

// Handler
public class AccountTriggerHandler {
    
    public void run() {
        if (Trigger.isBefore) {
            if (Trigger.isInsert) {
                beforeInsert(Trigger.new);
            } else if (Trigger.isUpdate) {
                beforeUpdate(Trigger.new, Trigger.oldMap);
            }
        } else if (Trigger.isAfter) {
            if (Trigger.isInsert) {
                afterInsert(Trigger.new);
            } else if (Trigger.isUpdate) {
                afterUpdate(Trigger.new, Trigger.oldMap);
            }
        }
    }
    
    private void beforeInsert(List<Account> newAccounts) {
        // Before insert logic
    }
    
    private void beforeUpdate(List<Account> newAccounts, Map<Id, Account> oldMap) {
        // Before update logic
    }
    
    private void afterInsert(List<Account> newAccounts) {
        // After insert logic
    }
    
    private void afterUpdate(List<Account> newAccounts, Map<Id, Account> oldMap) {
        // After update logic
    }
}

3. Bulkify Your Code

  • Always write triggers to handle multiple records
  • Never use SOQL or DML inside loops
  • Process records in collections
// Bad Practice
trigger AccountTrigger on Account (after insert) {
    for (Account acc : Trigger.new) {
        Contact con = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
        con.Description = 'Updated';
        update con;
    }
}

// Good Practice
trigger AccountTrigger on Account (after insert) {
    Set<Id> accountIds = new Set<Id>();
    for (Account acc : Trigger.new) {
        accountIds.add(acc.Id);
    }
    
    List<Contact> contactsToUpdate = [SELECT Id, AccountId FROM Contact 
                                      WHERE AccountId IN :accountIds];
    
    for (Contact con : contactsToUpdate) {
        con.Description = 'Updated';
    }
    
    update contactsToUpdate;
}

4. Avoid Recursion

  • Use static variables to prevent infinite loops
  • Track processed records
public class TriggerHelper {
    public static Boolean isFirstRun = true;
    public static Set<Id> processedIds = new Set<Id>();
}

trigger AccountTrigger on Account (after update) {
    if (TriggerHelper.isFirstRun) {
        TriggerHelper.isFirstRun = false;
        // Your trigger logic
    }
}

5. Use Context Variables Appropriately

  • Trigger.new – List of new records (insert, update, undelete)
  • Trigger.old – List of old records (update, delete)
  • Trigger.newMap – Map of new records (before update, after insert, after update, after undelete)
  • Trigger.oldMap – Map of old records (update, delete)
trigger AccountTrigger on Account (before update) {
    for (Account acc : Trigger.new) {
        Account oldAccount = Trigger.oldMap.get(acc.Id);
        
        if (acc.Industry != oldAccount.Industry) {
            // Industry changed
            acc.Description = 'Industry changed from ' + oldAccount.Industry;
        }
    }
}

6. Choose the Right Trigger Context

Before Triggers:

  • Validation
  • Setting default values
  • Modifying record values before saving
  • No DML needed on Trigger.new

After Triggers:

  • Creating related records
  • Updating related records
  • Sending emails
  • Callouts (use @future)
trigger AccountTrigger on Account (before insert, after insert) {
    
    if (Trigger.isBefore && Trigger.isInsert) {
        // Set default values
        for (Account acc : Trigger.new) {
            if (acc.Type == null) {
                acc.Type = 'Prospect';
            }
        }
    }
    
    if (Trigger.isAfter && Trigger.isInsert) {
        // Create related records
        List<Contact> contacts = new List<Contact>();
        for (Account acc : Trigger.new) {
            contacts.add(new Contact(
                LastName = 'Default Contact',
                AccountId = acc.Id
            ));
        }
        insert contacts;
    }
}

7. Handle Exceptions Properly

  • Use try-catch blocks
  • Log errors
  • Provide meaningful error messages
trigger AccountTrigger on Account (before insert) {
    try {
        // Your logic here
        AccountTriggerHandler.validateAccounts(Trigger.new);
    } catch (Exception e) {
        System.debug('Error in AccountTrigger: ' + e.getMessage());
        Trigger.new[0].addError('An error occurred: ' + e.getMessage());
    }
}

8. Use Maps for Efficient Processing

  • Create maps to avoid nested loops
  • Better performance
// Efficient approach
Map<Id, Account> accountMap = new Map<Id, Account>(
    [SELECT Id, Name FROM Account WHERE Id IN :accountIds]
);

for (Contact con : contactList) {
    Account relatedAccount = accountMap.get(con.AccountId);
    // Process
}

9. Write Comprehensive Test Classes

  • Test all trigger contexts
  • Test bulk operations (200+ records)
  • Test positive and negative scenarios
  • Test with different user profiles
@isTest
private class AccountTriggerTest {
    
    @isTest
    static void testBulkInsert() {
        List<Account> accounts = new List<Account>();
        for (Integer i = 0; i < 200; i++) {
            accounts.add(new Account(Name = 'Test Account ' + i));
        }
        
        Test.startTest();
        insert accounts;
        Test.stopTest();
        
        // Assertions
        List<Account> insertedAccounts = [SELECT Id FROM Account];
        System.assertEquals(200, insertedAccounts.size());
    }
}

10. Follow Governor Limits

  • Max 200 SOQL queries per transaction
  • Max 150 DML statements per transaction
  • Max 50,000 records retrieved by SOQL
  • Max 10,000 records for DML

11. Use Queueable or Batch for Complex Operations

  • For long-running processes
  • For processing large data volumes
trigger AccountTrigger on Account (after insert) {
    if (!System.isBatch() && !System.isFuture()) {
        System.enqueueJob(new AccountQueueable(Trigger.newMap.keySet()));
    }
}

12. Document Your Code

  • Add comments explaining business logic
  • Document assumptions and dependencies
/**
 * AccountTrigger
 * Purpose: Handles all DML events for Account object
 * Created: 2025-01-15
 * Modified: 2025-01-20
 */
trigger AccountTrigger on Account (before insert, after insert) {
    // Delegate to handler
    new AccountTriggerHandler().run();
}

13. Avoid Hard-Coding

  • Use Custom Settings or Custom Metadata
  • Makes code configurable
// Using Custom Settings
Trigger_Settings__c settings = Trigger_Settings__c.getInstance();
if (settings.Enable_Account_Trigger__c) {
    // Execute trigger logic
}

14. Consider Order of Execution

Understand the Salesforce order of execution:

  1. System validation rules
  2. Before triggers
  3. Custom validation rules
  4. After triggers
  5. Assignment rules
  6. Auto-response rules
  7. Workflow rules
  8. Processes and Flows
  9. Escalation rules
  10. Parent rollup summary fields
  11. Criteria-based sharing
  12. Commit to database
  13. Post-commit logic

15. Use Field-Level Security

  • Check CRUD and FLS permissions
  • Use Schema class methods
if (Schema.sObjectType.Account.fields.Industry.isUpdateable()) {
    acc.Industry = 'Technology';
}

Summary Checklist:

  • ✅ One trigger per object
  • ✅ Use handler pattern
  • ✅ Bulkify code
  • ✅ Prevent recursion
  • ✅ Choose correct context
  • ✅ Handle exceptions
  • ✅ Write test classes
  • ✅ Respect governor limits
  • ✅ Document code
  • ✅ Use custom settings
  • ✅ Check FLS
  • ✅ Avoid hard-coding

Following these best practices will result in maintainable, scalable, and performant triggers.

Q26. What are Platform Events and how do you use them?

Platform Events: Platform Events are a part of Salesforce’s event-driven architecture that enables you to deliver secure, scalable, and customizable event notifications. They use a publish-subscribe model where publishers send event messages that subscribers receive in near real-time.

Key Characteristics:

  • Event-driven architecture
  • Publish-subscribe model
  • Asynchronous communication
  • Decoupled integrations
  • Near real-time delivery
  • Can be published from Apex, Process Builder, Flows, or APIs
  • Can be subscribed using Triggers, Process Builder, Flows, or CometD

Types of Platform Events:

1. Standard Platform Events

Pre-built events provided by Salesforce (e.g., Real-Time Event Monitoring)

2. Custom Platform Events

User-defined events for custom use cases

Creating a Platform Event:

Setup → Platform Events → New Platform Event

API Name: Order_Event__e
Fields:
- Order_Number__c (Text)
- Order_Amount__c (Number)
- Status__c (Text)

Publishing Platform Events:

From Apex:

public class OrderEventPublisher {
    
    public static void publishOrderEvent(String orderNumber, Decimal amount, String status) {
        // Create event instance
        Order_Event__e orderEvent = new Order_Event__e(
            Order_Number__c = orderNumber,
            Order_Amount__c = amount,
            Status__c = status
        );
        
        // Publish event
        Database.SaveResult result = EventBus.publish(orderEvent);
        
        // Check if event was published successfully
        if (result.isSuccess()) {
            System.debug('Order event published successfully');
        } else {
            for (Database.Error error : result.getErrors()) {
                System.debug('Error publishing event: ' + error.getMessage());
            }
        }
    }
    
    // Bulk publishing
    public static void publishMultipleEvents(List<Order_Event__e> events) {
        List<Database.SaveResult> results = EventBus.publish(events);
        
        for (Database.SaveResult result : results) {
            if (!result.isSuccess()) {
                for (Database.Error error : result.getErrors()) {
                    System.debug('Error: ' + error.getMessage());
                }
            }
        }
    }
}

Subscribing to Platform Events:

Using Trigger:

trigger OrderEventTrigger on Order_Event__e (after insert) {
    for (Order_Event__e event : Trigger.new) {
        System.debug('Order Number: ' + event.Order_Number__c);
        System.debug('Order Amount: ' + event.Order_Amount__c);
        System.debug('Status: ' + event.Status__c);
        
        // Process the event
        if (event.Status__c == 'Completed') {
            // Create notification, update records, etc.
            OrderEventHandler.handleCompletedOrder(event);
        }
    }
}

Handler Class:

public class OrderEventHandler {
    
    public static void handleCompletedOrder(Order_Event__e event) {
        // Create a task for sales team
        Task followUpTask = new Task(
            Subject = 'Follow up on Order: ' + event.Order_Number__c,
            Description = 'Order amount: ' + event.Order_Amount__c,
            Status = 'Not Started',
            Priority = 'High'
        );
        
        insert followUpTask;
    }
}

Subscribing via Flow:

  • Create a Flow that starts when a Platform Event is received
  • Process → New Flow → Platform Event-Triggered Flow

Use Cases:

  1. System Integration:
    • Notify external systems of Salesforce changes
    • Send data to data warehouses
  2. Async Processing:
    • Trigger background jobs
    • Process heavy computations
  3. Real-time Notifications:
    • Alert users of critical events
    • Update dashboards
  4. Microservices Communication:
    • Decouple system components
    • Event-driven architecture

Advanced Example – Integration with External System:

public class ExternalSystemIntegration {
    
    @future(callout=true)
    public static void notifyExternalSystem(String orderNumber, Decimal amount) {
        // First publish platform event
        Order_Event__e orderEvent = new Order_Event__e(
            Order_Number__c = orderNumber,
            Order_Amount__c = amount,
            Status__c = 'Pending External Processing'
        );
        
        EventBus.publish(orderEvent);
        
        // Then make callout
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://external-system.com/api/orders');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(JSON.serialize(new Map<String, Object>{
            'orderNumber' => orderNumber,
            'amount' => amount
        }));
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        // Publish result event
        if (res.getStatusCode() == 200) {
            Order_Event__e successEvent = new Order_Event__e(
                Order_Number__c = orderNumber,
                Status__c = 'External System Notified'
            );
            EventBus.publish(successEvent);
        }
    }
}

Benefits:

  • Loose coupling between systems
  • Scalable and resilient
  • Real-time event processing
  • Simplified integrations

Limitations:

  • Events are retained for 72 hours
  • Max 250,000 events delivered per 24 hours (can be increased)
  • 2,000 event definitions per org

Q27. What is the difference between REST and SOAP web services in Salesforce?

Both REST and SOAP are protocols for exposing Apex methods as web services, but they have different characteristics and use cases.

REST (Representational State Transfer)

Characteristics:

  • Lightweight and flexible
  • Uses HTTP methods (GET, POST, PUT, DELETE, PATCH)
  • Typically uses JSON (also supports XML)
  • Stateless
  • Better performance
  • Easier to implement and consume

Creating REST Web Service:

@RestResource(urlMapping='/accounts/*')
global class AccountRestService {
    
    @HttpGet
    global static Account getAccount() {
        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;
        
        String accountId = req.requestURI.substring(req.requestURI.lastIndexOf('/')+1);
        
        Account acc = [SELECT Id, Name, Industry, AnnualRevenue 
                       FROM Account 
                       WHERE Id = :accountId];
        
        return acc;
    }
    
    @HttpPost
    global static String createAccount(String name, String industry) {
        Account acc = new Account(
            Name = name,
            Industry = industry
        );
        
        insert acc;
        
        return acc.Id;
    }
    
    @HttpPut
    global static String updateAccount(String id, String name, String industry) {
        Account acc = [SELECT Id FROM Account WHERE Id = :id];
        acc.Name = name;
        acc.Industry = industry;
        
        update acc;
        
        return 'Account updated successfully';
    }
    
    @HttpDelete
    global static String deleteAccount() {
        RestRequest req = RestContext.request;
        String accountId = req.requestURI.substring(req.requestURI.lastIndexOf('/')+1);
        
        Account acc = [SELECT Id FROM Account WHERE Id = :accountId];
        delete acc;
        
        return 'Account deleted successfully';
    }
    
    @HttpPatch
    global static String patchAccount() {
        RestRequest req = RestContext.request;
        String accountId = req.requestURI.substring(req.requestURI.lastIndexOf('/')+1);
        
        // Parse JSON from request body
        Map<String, Object> params = (Map<String, Object>)JSON.deserializeUntyped(req.requestBody.toString());
        
        Account acc = [SELECT Id FROM Account WHERE Id = :accountId];
        
        if (params.containsKey('name')) {
            acc.Name = (String)params.get('name');
        }
        if (params.containsKey('industry')) {
            acc.Industry = (String)params.get('industry');
        }
        
        update acc;
        
        return 'Account patched successfully';
    }
}

Advanced REST Example with Custom Response:

@RestResource(urlMapping='/api/v1/customers/*')
global class CustomerRestAPI {
    
    global class CustomerResponse {
        global String status;
        global String message;
        global Object data;
        global List<String> errors;
    }
    
    @HttpGet
    global static CustomerResponse getCustomer() {
        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;
        CustomerResponse response = new CustomerResponse();
        
        try {
            String customerId = req.requestURI.substring(req.requestURI.lastIndexOf('/')+1);
            
            Account customer = [SELECT Id, Name, Industry, AnnualRevenue, 
                               (SELECT Id, FirstName, LastName, Email FROM Contacts)
                               FROM Account 
                               WHERE Id = :customerId];
            
            response.status = 'success';
            response.message = 'Customer retrieved successfully';
            response.data = customer;
            res.statusCode = 200;
            
        } catch (Exception e) {
            response.status = 'error';
            response.message = 'Failed to retrieve customer';
            response.errors = new List<String>{e.getMessage()};
            res.statusCode = 500;
        }
        
        return response;
    }
    
    @HttpPost
    global static CustomerResponse createCustomer(String name, String industry, Decimal revenue) {
        CustomerResponse response = new CustomerResponse();
        
        try {
            Account newCustomer = new Account(
                Name = name,
                Industry = industry,
                AnnualRevenue = revenue
            );
            
            insert newCustomer;
            
            response.status = 'success';
            response.message = 'Customer created successfully';
            response.data = newCustomer;
            RestContext.response.statusCode = 201;
            
        } catch (Exception e) {
            response.status = 'error';
            response.message = 'Failed to create customer';
            response.errors = new List<String>{e.getMessage()};
            RestContext.response.statusCode = 400;
        }
        
        return response;
    }
}

Calling REST Endpoint:

Endpoint: https://yourinstance.salesforce.com/services/apexrest/accounts/001XXXXXXXXXXXXXXX
Method: GET
Headers: 
  Authorization: Bearer {access_token}
  Content-Type: application/json

SOAP (Simple Object Access Protocol)

Characteristics:

  • XML-based protocol
  • More structured and formal
  • Built-in security (WS-Security)
  • WSDL (Web Service Definition Language) for service description
  • Supports complex operations and transactions
  • More overhead than REST

Creating SOAP Web Service:

global class AccountSoapService {
    
    webservice static Account getAccount(String accountId) {
        Account acc = [SELECT Id, Name, Industry, AnnualRevenue 
                       FROM Account 
                       WHERE Id = :accountId];
        return acc;
    }
    
    webservice static String createAccount(String name, String industry) {
        Account acc = new Account(
            Name = name,
            Industry = industry
        );
        insert acc;
        return acc.Id;
    }
    
    webservice static Boolean updateAccount(String accountId, String name, String industry) {
        try {
            Account acc = [SELECT Id FROM Account WHERE Id = :accountId];
            acc.Name = name;
            acc.Industry = industry;
            update acc;
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    
    webservice static Boolean deleteAccount(String accountId) {
        try {
            Account acc = [SELECT Id FROM Account WHERE Id = :accountId];
            delete acc;
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

Advanced SOAP Example with Custom Types:

global class OrderSoapService {
    
    // Custom class for request
    global class OrderRequest {
        webservice String customerName;
        webservice String productName;
        webservice Decimal quantity;
        webservice Decimal unitPrice;
    }
    
    // Custom class for response
    global class OrderResponse {
        webservice String orderId;
        webservice String status;
        webservice Decimal totalAmount;
        webservice String message;
    }
    
    webservice static OrderResponse createOrder(OrderRequest request) {
        OrderResponse response = new OrderResponse();
        
        try {
            // Create Account
            Account acc = new Account(Name = request.customerName);
            insert acc;
            
            // Create Order
            Order ord = new Order(
                AccountId = acc.Id,
                Status = 'Draft',
                EffectiveDate = Date.today()
            );
            insert ord;
            
            // Calculate total
            Decimal total = request.quantity * request.unitPrice;
            
            response.orderId = ord.Id;
            response.status = 'Success';
            response.totalAmount = total;
            response.message = 'Order created successfully';
            
        } catch (Exception e) {
            response.status = 'Error';
            response.message = e.getMessage();
        }
        
        return response;
    }
    
    webservice static List<Account> getAccountsByIndustry(String industry) {
        return [SELECT Id, Name, Industry, AnnualRevenue 
                FROM Account 
                WHERE Industry = :industry 
                LIMIT 100];
    }
}

Accessing WSDL:

Setup → Apex Classes → Select Class → Generate WSDL

Comparison Table:

FeatureRESTSOAP
ProtocolArchitectural styleProtocol
FormatJSON, XMLXML only
PerformanceFaster, lightweightSlower, more overhead
SecurityHTTPS, OAuthWS-Security, SSL
StateStatelessCan be stateful
CachingCan leverage HTTP cachingNo caching
Learning CurveEasierSteeper
Error HandlingHTTP status codesSOAP faults
BandwidthLessMore
Use CaseModern web/mobile appsEnterprise integrations

When to Use REST:

  • Mobile applications
  • Web applications
  • Public APIs
  • Microservices
  • Real-time applications
  • When JSON is preferred

When to Use SOAP:

  • Enterprise applications
  • High security requirements
  • Complex transactions
  • ACID compliance needed
  • Legacy system integration
  • Formal contracts (WSDL)

Example: Making a REST Callout from Apex:

public class RestCalloutExample {
    
    @future(callout=true)
    public static void callExternalService() {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.example.com/data');
        req.setMethod('GET');
        req.setHeader('Content-Type', 'application/json');
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        if (res.getStatusCode() == 200) {
            Map<String, Object> results = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
            System.debug('Response: ' + results);
        }
    }
}

Q28. What are Integration Patterns in Salesforce?

Integration Patterns: Integration patterns are proven solutions for common integration scenarios between Salesforce and external systems. They define how systems communicate and exchange data.

Common Integration Patterns:

1. Remote Process Invocation – Request and Reply

Scenario: Salesforce invokes an external service and waits for a response synchronously.

Implementation:

public class RemoteProcessInvocation {
    
    public static String validateAddress(String street, String city, String zipCode) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.addressvalidation.com/validate');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        
        Map<String, String> addressData = new Map<String, String>{
            'street' => street,
            'city' => city,
            'zipCode' => zipCode
        };
        
        req.setBody(JSON.serialize(addressData));
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        if (res.getStatusCode() == 200) {
            Map<String, Object> result = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
            return (String)result.get('validatedAddress');
        }
        
        return null;
    }
}

// Usage in trigger
trigger AccountTrigger on Account (before insert) {
    for (Account acc : Trigger.new) {
        if (acc.BillingStreet != null) {
            String validated = RemoteProcessInvocation.validateAddress(
                acc.BillingStreet,
                acc.BillingCity,
                acc.BillingPostalCode
            );
            acc.BillingStreet = validated;
        }
    }
}

Characteristics:

  • Synchronous
  • User waits for response
  • Use @future(callout=true) or Queueable
  • Timeout considerations (max 120 seconds)

2. Remote Process Invocation – Fire and Forget

Scenario: Salesforce invokes an external service but doesn’t wait for a response.

Implementation:

public class FireAndForgetIntegration {
    
    @future(callout=true)
    public static void notifyExternalSystem(String accountId, String accountName) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.external.com/notifications');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setTimeout(10000); // 10 seconds timeout
        
        Map<String, String> data = new Map<String, String>{
            'accountId' => accountId,
            'accountName' => accountName,
            'timestamp' => String.valueOf(DateTime.now())
        };
        
        req.setBody(JSON.serialize(data));
        
        try {
            Http http = new Http();
            HttpResponse res = http.send(req);
            // Don't process response, just log
            System.debug('Notification sent, Status: ' + res.getStatusCode());
        } catch (Exception e) {
            System.debug('Error sending notification: ' + e.getMessage());
            // Optionally log to custom object
        }
    }
}

// Usage
trigger AccountTrigger on Account (after insert) {
    for (Account acc : Trigger.new) {
        FireAndForgetIntegration.notifyExternalSystem(acc.Id, acc.Name);
    }
}

Characteristics:

  • Asynchronous
  • No response expected
  • Better performance
  • Use for notifications

3. Batch Data Synchronization

Scenario: Synchronize large volumes of data between Salesforce and external systems on a scheduled basis.

Implementation:

global class BatchDataSync implements Database.Batchable<sObject>, Database.AllowsCallouts {
    
    global Database.QueryLocator start(Database.BatchableContext bc) {
        // Get records modified in last 24 hours
        DateTime yesterday = DateTime.now().addDays(-1);
        return Database.getQueryLocator(
            'SELECT Id, Name, Industry, LastModifiedDate FROM Account WHERE LastModifiedDate > :yesterday'
        );
    }
    
    global void execute(Database.BatchableContext bc, List<Account> scope) {
        // Prepare data for external system
        List<Map<String, Object>> accountData = new List<Map<String, Object>>();
        
        for (Account acc : scope) {
            accountData.add(new Map<String, Object>{
                'id' => acc.Id,
                'name' => acc.Name,
                'industry' => acc.Industry
            });
        }
        
        // Make callout
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.external.com/sync');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(JSON.serialize(accountData));
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        if (res.getStatusCode() == 200) {
            System.debug('Batch sync successful');
        } else {
            System.debug('Batch sync failed: ' + res.getBody());
        }
    }
    
    global void finish(Database.BatchableContext bc) {
        System.debug('Batch job completed');
    }
}

// Schedule the batch
global class ScheduleBatchDataSync implements Schedulable {
    global void execute(SchedulableContext sc) {
        Database.executeBatch(new BatchDataSync(), 200);
    }
}

Characteristics:

  • Asynchronous
  • Large data volumes
  • Scheduled execution
  • Bulk operations

4. Remote Call-In (Inbound Integration)

Scenario: External system calls Salesforce to create, update, or query data.

Implementation:

@RestResource(urlMapping='/api/orders/*')
global class InboundOrderAPI {
    
    global class OrderRequest {
        global String customerName;
        global String productName;
        global Decimal quantity;
        global Decimal price;
    }
    
    global class OrderResponse {
        global Boolean success;
        global String orderId;
        global String message;
    }
    
    @HttpPost
    global static OrderResponse createOrder(OrderRequest request) {
        OrderResponse response = new OrderResponse();
        
        try {
            // Find or create account
            List<Account> accounts = [SELECT Id FROM Account WHERE Name = :request.customerName LIMIT 1];
            Account acc;
            
            if (accounts.isEmpty()) {
                acc = new Account(Name = request.customerName);
                insert acc;
            } else {
                acc = accounts[0];
            }
            
            // Create order
            Order__c order = new Order__c(
                Account__c = acc.Id,
                Product_Name__c = request.productName,
                Quantity__c = request.quantity,
                Unit_Price__c = request.price,
                Total_Amount__c = request.quantity * request.price
            );
            
            insert order;
            
            response.success = true;
            response.orderId = order.Id;
            response.message = 'Order created successfully';
            
        } catch (Exception e) {
            response.success = false;
            response.message = e.getMessage();
        }
        
        return response;
    }
    
    @HttpGet
    global static List<Order__c> getOrders() {
        String accountName = RestContext.request.params.get('accountName');
        
        if (String.isNotBlank(accountName)) {
            return [SELECT Id, Product_Name__c, Quantity__c, Total_Amount__c 
                    FROM Order__c 
                    WHERE Account__r.Name = :accountName];
        }
        
        return [SELECT Id, Product_Name__c, Quantity__c, Total_Amount__c 
                FROM Order__c 
                LIMIT 100];
    }
}

Characteristics:

  • Synchronous
  • External system initiates
  • REST or SOAP
  • Authentication required

5. UI Update Based on Data Changes

Scenario: Update Salesforce UI when external data changes (using Platform Events or Streaming API).

Implementation:

// External system publishes platform event
public class ExternalDataChangePublisher {
    
    public static void publishDataChange(String entityType, String entityId, String changeType) {
        External_Data_Change__e event = new External_Data_Change__e(
            Entity_Type__c = entityType,
            Entity_Id__c = entityId,
            Change_Type__c = changeType,
            Timestamp__c = DateTime.now()
        );
        
        EventBus.publish(event);
    }
}

// Trigger to handle the event
trigger ExternalDataChangeTrigger on External_Data_Change__e (after insert) {
    for (External_Data_Change__e event : Trigger.new) {
        if (event.Entity_Type__c == 'Account' && event.Change_Type__c == 'Update') {
            // Refresh account data
            ExternalDataChangeHandler.refreshAccountData(event.Entity_Id__c);
        }
    }
}

public class ExternalDataChangeHandler {
    
    @future(callout=true)
    public static void refreshAccountData(String accountId) {
        // Call external system to get latest data
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.external.com/accounts/' + accountId);
        req.setMethod('GET');
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        if (res.getStatusCode() == 200) {
            Map<String, Object> externalData = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
            
            // Update Salesforce record
            Account acc = [SELECT Id FROM Account WHERE External_Id__c = :accountId];
            acc.Name = (String)externalData.get('name');
            acc.Industry = (String)externalData.get('industry');
            update acc;
        }
    }
}

6. Data Virtualization (Lightning Connect)

Scenario: Access external data in Salesforce without storing it locally.

Characteristics:

  • Real-time access
  • No data duplication
  • OData protocol
  • External Objects

Best Practices for Integration Patterns:

  1. Error Handling:
public class IntegrationErrorHandler {
    
    public static void logIntegrationError(String systemName, String operation, String errorMessage) {
        Integration_Log__c log = new Integration_Log__c(
            System_Name__c = systemName,
            Operation__c = operation,
            Error_Message__c = errorMessage,
            Timestamp__c = DateTime.now()
        );
        insert log;
    }
}
  1. Retry Logic:
public class RetryableCallout {
    
    public static HttpResponse makeCalloutWithRetry(HttpRequest req, Integer maxRetries) {
        Integer attempts = 0;
        HttpResponse res;
        
        while (attempts < maxRetries) {
            try {
                Http http = new Http();
                res = http.send(req);
                
                if (res.getStatusCode() == 200) {
                    return res;
                }
                
                attempts++;
                
            } catch (Exception e) {
                attempts++;
                if (attempts >= maxRetries) {
                    throw e;
                }
            }
        }
        
        return res;
    }
}
  1. Circuit Breaker Pattern:
public class CircuitBreaker {
    private static Integer failureCount = 0;
    private static final Integer FAILURE_THRESHOLD = 5;
    private static DateTime lastFailureTime;
    private static final Integer TIMEOUT_MINUTES = 5;
    
    public static Boolean isOpen() {
        if (failureCount >= FAILURE_THRESHOLD) {
            if (lastFailureTime.addMinutes(TIMEOUT_MINUTES) > DateTime.now()) {
                return true; // Circuit is open
            } else {
                failureCount = 0; // Reset after timeout
            }
        }
        return false;
    }
    
    public static void recordFailure() {
        failureCount++;
        lastFailureTime = DateTime.now();
    }
    
    public static void recordSuccess() {
        failureCount = 0;
    }
}

Q29. What is the difference between ‘with sharing’ and ‘without sharing’?

Sharing Keywords: These keywords control whether the current user’s sharing rules are enforced when executing Apex code.

with sharing

Definition: Enforces the sharing rules of the current user. Only records accessible to the user are returned or modified.

public with sharing class AccountController {
    
    public List<Account> getAccounts() {
        // Returns only accounts the current user has access to
        return [SELECT Id, Name, Industry FROM Account];
    }
    
    public void updateAccount(String accountId, String newName) {
        Account acc = [SELECT Id, Name FROM Account WHERE Id = :accountId];
        // Will throw exception if user doesn't have edit access
        acc.Name = newName;
        update acc;
    }
}

without sharing

Definition: Executes code without enforcing the current user’s sharing rules. All records are accessible regardless of user permissions.

public without sharing class AccountUtility {
    
    public List<Account> getAllAccounts() {
        // Returns ALL accounts in the system
        return [SELECT Id, Name, Industry FROM Account];
    }
    
    public void updateAccountAsSystem(String accountId, String newName) {
        Account acc = [SELECT Id, Name FROM Account WHERE Id = :accountId];
        // Updates regardless of user's access
        acc.Name = newName;
        update acc;
    }
}

inherited sharing (Since Spring ’18)

Definition: Inherits the sharing setting from the calling class. If called from a context with sharing, it uses sharing rules; otherwise, it runs without sharing.

public inherited sharing class FlexibleController {
    
    public List<Account> getAccounts() {
        // Inherits sharing context from caller
        return [SELECT Id, Name FROM Account];
    }
}

// Example usage
public with sharing class CallerWithSharing {
    public void doSomething() {
        FlexibleController controller = new FlexibleController();
        // FlexibleController will run WITH sharing
        List<Account> accounts = controller.getAccounts();
    }
}

public without sharing class CallerWithoutSharing {
    public void doSomething() {
        FlexibleController controller = new FlexibleController();
        // FlexibleController will run WITHOUT sharing
        List<Account> accounts = controller.getAccounts();
    }
}

No Keyword (Omitted)

Behavior:

  • Before Spring ’18: Runs without sharing
  • Spring ’18 and later: Same as inherited sharing for inner classes, without sharing for top-level classes
public class NoSharingKeyword {
    // Behaves as WITHOUT SHARING for top-level class
    public List<Account> getAccounts() {
        return [SELECT Id, Name FROM Account];
    }
}

Practical Examples:

Example 1: Security-Conscious Design

// Public facing controller - respects user permissions
public with sharing class AccountPublicController {
    
    public List<Account> getUserAccessibleAccounts() {
        return [SELECT Id, Name, Industry FROM Account];
    }
    
    public void createAccount(String name) {
        Account acc = new Account(Name = name);
        insert acc; // Will fail if user doesn't have create permission
    }
}

// Internal utility - system context needed
public without sharing class AccountSystemUtility {
    
    public static void createSystemAccount(String name) {
        Account acc = new Account(
            Name = name,
            RecordTypeId = getSystemRecordTypeId()
        );
        insert acc; // Always succeeds regardless of user permissions
    }
    
    private static Id getSystemRecordTypeId() {
        return Schema.SObjectType.Account.getRecordTypeInfosByName()
                     .get('System Account').getRecordTypeId();
    }
}

Example 2: Mixing Sharing Contexts

public with sharing class OpportunityController {
    
    public List<Opportunity> getUserOpportunities() {
        // Respects sharing rules
        return [SELECT Id, Name, Amount FROM Opportunity];
    }
    
    public void createOpportunityWithSystemDefaults(String name, Decimal amount) {
        // Need to access system data without sharing
        Map<String, String> systemDefaults = OpportunitySystemDefaults.getDefaults();
        
        Opportunity opp = new Opportunity(
            Name = name,
            Amount = amount,
            StageName = systemDefaults.get('defaultStage'),
            CloseDate = Date.today().addDays(30)
        );
        
        insert opp; // Respects sharing rules for insert
    }
}

public without sharing class OpportunitySystemDefaults {
    
    public static Map<String, String> getDefaults() {
        // Access system configuration that user might not have access to
        System_Config__c config = [SELECT Default_Stage__c, Default_Probability__c 
                                   FROM System_Config__c 
                                   LIMIT 1];
        
        return new Map<String, String>{
            'defaultStage' => config.Default_Stage__c,
            'defaultProbability' => String.valueOf(config.Default_Probability__c)
        };
    }
}

Example 3: Elevated Privileges for Specific Operations

public with sharing class CaseController {
    
    public List<Case> getMyCases() {
        // User sees only their cases
        return [SELECT Id, CaseNumber, Subject, Status FROM Case];
    }
    
    public void escalateCase(String caseId) {
        // Escalation requires system privileges
        CaseEscalationUtility.performEscalation(caseId);
    }
}

public without sharing class CaseEscalationUtility {
    
    public static void performEscalation(String caseId) {
        Case c = [SELECT Id, Status, Priority, OwnerId FROM Case WHERE Id = :caseId];
        
        // System can access queue even if user can't
        Group escalationQueue = [SELECT Id FROM Group 
                                WHERE Type = 'Queue' 
                                AND DeveloperName = 'Escalation_Queue' 
                                LIMIT 1];
        
        c.Status = 'Escalated';
        c.Priority = 'High';
        c.OwnerId = escalationQueue.Id;
        
        update c; // Updates as system
        
        // Send notification to queue members
        notifyQueueMembers(escalationQueue.Id, c.Id);
    }
    
    private static void notifyQueueMembers(Id queueId, Id caseId) {
        List<GroupMember> members = [SELECT UserOrGroupId FROM GroupMember WHERE GroupId = :queueId];
        // Notification logic
    }
}

Example 4: Report Generation

public with sharing class UserReportController {
    
    public String generateMyReport() {
        // User sees their own data
        List<Opportunity> myOpps = [SELECT Id, Amount FROM Opportunity];
        return ReportGenerator.generateReport(myOpps);
    }
}

public without sharing class AdminReportController {
    
    public String generateOrgWideReport() {
        // Admin sees all data
        List<Opportunity> allOpps = [SELECT Id, Amount, Owner.Name FROM Opportunity];
        return ReportGenerator.generateReport(allOpps);
    }
}

public inherited sharing class ReportGenerator {
    
    public static String generateReport(List<Opportunity> opportunities) {
        // Inherits sharing from caller
        // Process and format opportunities
        Decimal total = 0;
        for (Opportunity opp : opportunities) {
            total += opp.Amount != null ? opp.Amount : 0;
        }
        return 'Total Opportunities: ' + opportunities.size() + ', Total Amount: ' + total;
    }
}

Comparison Table:

KeywordSharing RulesUse CaseAccess Level
with sharingEnforcedUser-facing featuresUser’s access
without sharingNot enforcedSystem operationsAll records
inherited sharingInherited from callerUtility classesDepends on caller
OmittedNot enforced (top-level)Legacy codeAll records

Best Practices:

  1. Default to ‘with sharing’ for security
  2. Use ‘without sharing’ only when necessary (system operations, background jobs)
  3. Document why you’re using ‘without sharing’
  4. Use ‘inherited sharing’ for utility classes
  5. Combine both when needed for specific operations
  6. Always validate CRUD/FLS even with sharing keywords
public with sharing class SecureController {
    
    public void createAccount(String name) {
        // Check CRUD permission
        if (!Schema.sObjectType.Account.isCreateable()) {
            throw new SecurityException('User does not have permission to create Accounts');
        }
        
        // Check FLS for Name field
        if (!Schema.sObjectType.Account.fields.Name.isCreateable()) {
            throw new SecurityException('User does not have permission to set Account Name');
        }
        
        Account acc = new Account(Name = name);
        insert acc;
    }
}
Key Takeaways:
  • with sharing = User’s permissions enforced (secure)
  • without sharing = System context (powerful, use carefully)
  • inherited sharing = Flexible, inherits from caller
  • Always consider security implications when choosing sharing keywords

Q30. What are Custom Metadata Types and how are they different from Custom Settings?

Both Custom Metadata Types and Custom Settings store application configuration data, but they have different characteristics and use cases.

Custom Metadata Types

Definition: Custom Metadata Types are customizable, deployable, packageable application metadata that you can use to build customizable functionality.

Creating Custom Metadata Type:

Setup → Custom Metadata Types → New Custom Metadata Type

Label: API Configuration
Plural Label: API Configurations
Object Name: API_Configuration__mdt

Fields:
- Endpoint__c (Text)
- Timeout__c (Number)
- API_Key__c (Text Encrypted)
- Is_Active__c (Checkbox)

Accessing Custom Metadata in Apex:

public class APIIntegration {
    
    // Query Custom Metadata Type
    public static String getEndpoint(String configName) {
        API_Configuration__mdt config = [SELECT Endpoint__c, Timeout__c, API_Key__c
                                         FROM API_Configuration__mdt 
                                         WHERE DeveloperName = :configName 
                                         LIMIT 1];
        return config.Endpoint__c;
    }
    
    // Get all configurations
    public static List<API_Configuration__mdt> getAllConfigs() {
        return [SELECT DeveloperName, Endpoint__c, Timeout__c, Is_Active__c 
                FROM API_Configuration__mdt 
                WHERE Is_Active__c = true];
    }
    
    // Use in integration
    public static void makeAPICall(String configName) {
        API_Configuration__mdt config = API_Configuration__mdt.getInstance(configName);
        
        if (config != null && config.Is_Active__c) {
            HttpRequest req = new HttpRequest();
            req.setEndpoint(config.Endpoint__c);
            req.setMethod('GET');
            req.setTimeout(Integer.valueOf(config.Timeout__c));
            req.setHeader('Authorization', 'Bearer ' + config.API_Key__c);
            
            Http http = new Http();
            HttpResponse res = http.send(req);
            
            System.debug('Response: ' + res.getBody());
        }
    }
}

Advanced Example – Business Rules Engine:

// Custom Metadata Type: Validation_Rule__mdt
// Fields: Object_Name__c, Field_Name__c, Rule_Type__c, Error_Message__c, Is_Active__c

public class ValidationEngine {
    
    private static Map<String, List<Validation_Rule__mdt>> rulesCache;
    
    // Load all validation rules
    static {
        rulesCache = new Map<String, List<Validation_Rule__mdt>>();
        
        for (Validation_Rule__mdt rule : [SELECT Object_Name__c, Field_Name__c, 
                                          Rule_Type__c, Error_Message__c, 
                                          Validation_Logic__c
                                          FROM Validation_Rule__mdt 
                                          WHERE Is_Active__c = true]) {
            if (!rulesCache.containsKey(rule.Object_Name__c)) {
                rulesCache.put(rule.Object_Name__c, new List<Validation_Rule__mdt>());
            }
            rulesCache.get(rule.Object_Name__c).add(rule);
        }
    }
    
    public static Boolean validateRecord(sObject record, String objectName) {
        if (!rulesCache.containsKey(objectName)) {
            return true;
        }
        
        for (Validation_Rule__mdt rule : rulesCache.get(objectName)) {
            Object fieldValue = record.get(rule.Field_Name__c);
            
            if (rule.Rule_Type__c == 'Required' && fieldValue == null) {
                record.addError(rule.Error_Message__c);
                return false;
            }
            
            if (rule.Rule_Type__c == 'MaxLength') {
                String strValue = String.valueOf(fieldValue);
                if (strValue != null && strValue.length() > Integer.valueOf(rule.Validation_Logic__c)) {
                    record.addError(rule.Error_Message__c);
                    return false;
                }
            }
        }
        
        return true;
    }
}

// Usage in trigger
trigger AccountTrigger on Account (before insert, before update) {
    for (Account acc : Trigger.new) {
        ValidationEngine.validateRecord(acc, 'Account');
    }
}

Custom Settings

Definition: Custom Settings are similar to custom objects but provide a cached storage mechanism for configuration data.

Types of Custom Settings:

  1. Hierarchy Custom Settings
  2. List Custom Settings

Creating Custom Setting:

Setup → Custom Settings → New

Label: Integration Settings
Object Name: Integration_Settings__c
Setting Type: Hierarchy

Fields:
- API_Endpoint__c (Text)
- Retry_Count__c (Number)
- Enable_Logging__c (Checkbox)

Accessing Custom Settings in Apex:

Hierarchy Custom Settings:

public class IntegrationManager {
    
    // Get settings for current user/profile/org
    public static Integration_Settings__c getSettings() {
        // Returns setting for current user, or profile, or org default
        return Integration_Settings__c.getInstance();
    }
    
    // Get settings for specific user
    public static Integration_Settings__c getUserSettings(Id userId) {
        return Integration_Settings__c.getInstance(userId);
    }
    
    // Get settings for specific profile
    public static Integration_Settings__c getProfileSettings(Id profileId) {
        return Integration_Settings__c.getInstance(profileId);
    }
    
    // Get org-wide default settings
    public static Integration_Settings__c getOrgDefaults() {
        return Integration_Settings__c.getOrgDefaults();
    }
    
    // Usage
    public static void performIntegration() {
        Integration_Settings__c settings = Integration_Settings__c.getInstance();
        
        if (settings.Enable_Logging__c) {
            System.debug('Integration started');
        }
        
        HttpRequest req = new HttpRequest();
        req.setEndpoint(settings.API_Endpoint__c);
        req.setMethod('GET');
        
        // Retry logic based on settings
        Integer retryCount = Integer.valueOf(settings.Retry_Count__c);
        // Implementation...
    }
}

List Custom Settings:

// Custom Setting: Country_Code__c (List type)
// Fields: Country_Name__c, Code__c, Currency__c

public class CountryManager {
    
    // Get all list custom settings
    public static Map<String, Country_Code__c> getAllCountries() {
        return Country_Code__c.getAll();
    }
    
    // Get specific setting by name
    public static Country_Code__c getCountry(String countryName) {
        return Country_Code__c.getInstance(countryName);
    }
    
    // Get country code
    public static String getCountryCode(String countryName) {
        Country_Code__c country = Country_Code__c.getInstance(countryName);
        return country != null ? country.Code__c : null;
    }
    
    // Usage in validation
    public static Boolean isValidCountry(String countryName) {
        return Country_Code__c.getInstance(countryName) != null;
    }
}

// Usage
trigger AccountTrigger on Account (before insert) {
    for (Account acc : Trigger.new) {
        if (acc.BillingCountry != null) {
            if (!CountryManager.isValidCountry(acc.BillingCountry)) {
                acc.BillingCountry.addError('Invalid country name');
            }
        }
    }
}

Comprehensive Comparison:

FeatureCustom Metadata TypesCustom Settings (Hierarchy)Custom Settings (List)
Deployable✓ (via changesets/metadata API)✗ (requires data migration)✗ (requires data migration)
Packageable
Records in Package
Accessible via SOQL✗ (use getInstance)✗ (use getInstance/getAll)
DML Operations✗ (only via Metadata API)
Cached
Counts Against Limits✗ (metadata)✗ (not counted)✗ (not counted)
Supports Relationships
Hierarchy Support✓ (User/Profile/Org)
VisibilityPublic/ProtectedPublic onlyPublic only
Data Types SupportedAll standard typesAll standard typesAll standard types
Record Trigger Support
Subscriber OverrideLimitedLimited

When to Use Each:

Use Custom Metadata Types when:

  • Configuration needs to be deployed as metadata
  • Building managed packages
  • Need relationships between configuration records
  • Want version control of configuration
  • Need to query with complex SOQL
  • Configuration should be part of the application

Use Hierarchy Custom Settings when:

  • Need user/profile/org level configurations
  • Different settings for different users
  • Frequently updated configuration
  • Simple key-value storage

Use List Custom Settings when:

  • Static reference data
  • Simple lookup tables
  • Rarely changing data
  • Key-value pairs
Best Practices:
  1. Custom Metadata Types:
    • Use for application configuration
    • Deploy as part of release
    • Version control in Git
    • Use protected/public visibility appropriately
  2. Custom Settings:
    • Use for runtime configuration
    • Use hierarchy for user-specific settings
    • Use list for static data
    • Don’t overuse – consider limits
  3. General:
    • Cache frequently accessed data
    • Document configuration purpose
    • Validate configuration values
    • Handle missing configuration gracefully

Q31. What is Mixed DML Operation error and how do you resolve it?

Mixed DML Operation Error: This error occurs when you try to perform DML operations on setup objects (like User, Profile, PermissionSet) and non-setup objects (like Account, Contact) in the same transaction.

Error Message:

"MIXED_DML_OPERATION, DML operation on setup object is not permitted after you have updated a non-setup object (or vice versa)"

Setup Objects Include:

  • User
  • UserRole
  • Profile
  • PermissionSet
  • PermissionSetAssignment
  • Group (with Type = ‘Queue’)
  • GroupMember
  • QueueSObject

Non-Setup Objects:

  • Standard objects (Account, Contact, Opportunity, etc.)
  • Custom objects

Example of Mixed DML Error:

public class MixedDMLExample {
    
    public static void createAccountAndUser() {
        // This will cause Mixed DML error
        
        // DML on non-setup object
        Account acc = new Account(Name = 'Test Account');
        insert acc;
        UserRole uRole = [SELECT Id FROM UserRole WHERE Name = 'Sales User' LIMIT 1];
        // DML on setup object in same transaction - ERROR!
        User newUser = new User(
            FirstName = 'Test',
            LastName = 'User',
            Email = '[email protected]',
            Username = '[email protected]',
            Alias = 'tuser',
            TimeZoneSidKey = 'America/Los_Angeles',
            LocaleSidKey = 'en_US',
            EmailEncodingKey = 'UTF-8',
            LanguageLocaleKey = 'en_US',
            UserRoleId = uRole.Id,
            ProfileId = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1].Id
        );
        insert newUser; // MIXED_DML_OPERATION error here!
    }
}

Solutions to Mixed DML:

Solution 1: Use @future Method (Most Common)

public class MixedDMLSolution1 {
    
    public static void createAccountAndUser() {
        // DML on non-setup object first
        Account acc = new Account(Name = 'Test Account');
        insert acc;
        
        // DML on setup object in separate transaction
        createUserAsync('testuser');
    }
    
    @future
    public static void createUserAsync(String userName) {
        User newUser = new User(
            FirstName = 'Test',
            LastName = userName,
            Email = userName + '@test.com',
            Username = userName + '@test.com.test',
            Alias = userName.substring(0, 5),
            TimeZoneSidKey = 'America/Los_Angeles',
            LocaleSidKey = 'en_US',
            EmailEncodingKey = 'UTF-8',
            LanguageLocaleKey = 'en_US',
            ProfileId = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1].Id
        );
        
        insert newUser;
    }
}

Solution 2: Use Queueable

public class MixedDMLSolution2 {
    
    public static void createAccountAndUser() {
        // DML on non-setup object
        Account acc = new Account(Name = 'Test Account');
        insert acc;
        
        // Queue setup object DML
        System.enqueueJob(new UserCreationQueueable('testuser'));
    }
}

public class UserCreationQueueable implements Queueable {
    private String userName;
    
    public UserCreationQueueable(String userName) {
        this.userName = userName;
    }
    
    public void execute(QueueableContext context) {
        // Get the Profile
Profile prof = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1];

// Get the User Role
UserRole uRole = [SELECT Id FROM UserRole WHERE Name = 'Sales User' LIMIT 1];

User newUser = new User(
    FirstName = 'Test',
    LastName = userName,
    Email = userName + '@test.com',
    Username = userName + '@test.com.test',
    Alias = userName.substring(0, 5),
    TimeZoneSidKey = 'America/Los_Angeles',
    LocaleSidKey = 'en_US',
    EmailEncodingKey = 'UTF-8',
    LanguageLocaleKey = 'en_US',
    ProfileId = prof.Id,
    UserRoleId = uRole.Id
);

insert newUser;
    }
}

Best Practices:

  1. Identify Setup vs Non-Setup Objects early
  2. Use @future for async execution (most common solution)
  3. Consider Queueable for more complex scenarios
  4. Document mixed DML handling in your code
  5. Test thoroughly – mixed DML errors only occur at runtime
  6. Use System.runAs() wisely – only when setup DML is first
  7. Avoid complex transactions involving both types

Q32. What is Database.Stateful interface and when would you use it?

Database.Stateful Interface: The Database.Stateful interface is used with Batch Apex to maintain state across batch transactions. By default, batch apex doesn’t retain instance variable values between execute method calls, but implementing this interface changes that behavior.

Default Behavior (Without Stateful):

global class BatchWithoutStateful implements Database.Batchable<sObject> {
    
    global Integer recordCount = 0;  // This will NOT persist across batches
    
    global Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator('SELECT Id, Name FROM Account');
    }
    
    global void execute(Database.BatchableContext bc, List<Account> scope) {
        recordCount += scope.size();
        System.debug('Current count: ' + recordCount);
        // recordCount resets to 0 for each batch!
    }
    
    global void finish(Database.BatchableContext bc) {
        System.debug('Total records: ' + recordCount);  // Will be 0 or last batch size
    }
}

With Database.Stateful:

global class BatchWithStateful implements Database.Batchable<sObject>, Database.Stateful {
    
    global Integer recordCount = 0;  // This WILL persist across batches
    global List<String> errorMessages = new List<String>();
    
    global Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator('SELECT Id, Name, Industry FROM Account');
    }
    
    global void execute(Database.BatchableContext bc, List<Account> scope) {
        recordCount += scope.size();
        
        List<Account> accountsToUpdate = new List<Account>();
        
        for (Account acc : scope) {
            if (String.isBlank(acc.Industry)) {
                acc.Industry = 'Unknown';
                accountsToUpdate.add(acc);
            }
        }
        
        try {
            update accountsToUpdate;
        } catch (Exception e) {
            errorMessages.add('Batch error: ' + e.getMessage());
        }
        
        System.debug('Processed so far: ' + recordCount);
    }
    
    global void finish(Database.BatchableContext bc) {
        System.debug('Total records processed: ' + recordCount);
        
        if (!errorMessages.isEmpty()) {
            // Send email notification with all errors
            sendErrorNotification(errorMessages);
        }
    }
    
    private void sendErrorNotification(List<String> errors) {
        Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
        email.setToAddresses(new String[]{'[email protected]'});
        email.setSubject('Batch Job Errors');
        email.setPlainTextBody('Errors:\n' + String.join(errors, '\n'));
        Messaging.sendEmail(new Messaging.SingleEmailMessage[]{email});
    }
}

Common Use Cases:

Use Case 1: Counting and Aggregating Results

global class SalesReportBatch implements Database.Batchable<sObject>, Database.Stateful {
    
    global Decimal totalRevenue = 0;
    global Integer opportunitiesProcessed = 0;
    global Integer wonOpportunities = 0;
    global Map<String, Decimal> revenueByIndustry = new Map<String, Decimal>();
    
    global Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(
            'SELECT Id, Amount, StageName, Account.Industry FROM Opportunity WHERE CloseDate = THIS_YEAR'
        );
    }
    
    global void execute(Database.BatchableContext bc, List<Opportunity> scope) {
        for (Opportunity opp : scope) {
            opportunitiesProcessed++;
            
            if (opp.StageName == 'Closed Won' && opp.Amount != null) {
                wonOpportunities++;
                totalRevenue += opp.Amount;
                
                // Track revenue by industry
                String industry = opp.Account.Industry != null ? opp.Account.Industry : 'Unknown';
                if (!revenueByIndustry.containsKey(industry)) {
                    revenueByIndustry.put(industry, 0);
                }
                revenueByIndustry.put(industry, revenueByIndustry.get(industry) + opp.Amount);
            }
        }
    }
    
    global void finish(Database.BatchableContext bc) {
        // Create summary report
        String report = 'Sales Report Summary\n';
        report += '====================\n';
        report += 'Opportunities Processed: ' + opportunitiesProcessed + '\n';
        report += 'Won Opportunities: ' + wonOpportunities + '\n';
        report += 'Total Revenue: $' + totalRevenue.format() + '\n\n';
        report += 'Revenue by Industry:\n';
        
        for (String industry : revenueByIndustry.keySet()) {
            report += industry + ': $' + revenueByIndustry.get(industry).format() + '\n';
        }
        
        // Save report or send email
        System.debug(report);
        
        // Create a report record
        Sales_Report__c salesReport = new Sales_Report__c(
            Report_Date__c = Date.today(),
            Total_Revenue__c = totalRevenue,
            Opportunities_Count__c = wonOpportunities,
            Report_Details__c = report
        );
        insert salesReport;
    }
}

Use Case 2: Error Logging and Tracking

global class DataCleanupBatch implements Database.Batchable<sObject>, Database.Stateful {
    
    global Integer successCount = 0;
    global Integer errorCount = 0;
    global List<String> errorRecordIds = new List<String>();
    global Map<String, Integer> errorTypeCount = new Map<String, Integer>();
    
    global Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(
            'SELECT Id, Name, Phone, Email FROM Contact WHERE Email != null'
        );
    }
    
    global void execute(Database.BatchableContext bc, List<Contact> scope) {
        List<Contact> contactsToUpdate = new List<Contact>();
        
        for (Contact con : scope) {
            // Clean up phone numbers
            if (con.Phone != null) {
                con.Phone = cleanPhoneNumber(con.Phone);
            }
            
            // Validate email
            if (!isValidEmail(con.Email)) {
                con.Email = null;
            }
            
            contactsToUpdate.add(con);
        }
        
        // Update with partial success
        Database.SaveResult[] results = Database.update(contactsToUpdate, false);
        
        for (Integer i = 0; i < results.size(); i++) {
            if (results[i].isSuccess()) {
                successCount++;
            } else {
                errorCount++;
                errorRecordIds.add(contactsToUpdate[i].Id);
                
                for (Database.Error error : results[i].getErrors()) {
                    String errorType = error.getStatusCode().name();
                    if (!errorTypeCount.containsKey(errorType)) {
                        errorTypeCount.put(errorType, 0);
                    }
                    errorTypeCount.put(errorType, errorTypeCount.get(errorType) + 1);
                }
            }
        }
    }
    
    global void finish(Database.BatchableContext bc) {
        // Create comprehensive error report
        Batch_Execution_Log__c log = new Batch_Execution_Log__c(
            Batch_Name__c = 'DataCleanupBatch',
            Execution_Date__c = DateTime.now(),
            Success_Count__c = successCount,
            Error_Count__c = errorCount,
            Total_Records__c = successCount + errorCount
        );
        insert log;
        
        // Log each error type
        for (String errorType : errorTypeCount.keySet()) {
            Batch_Error_Detail__c errorDetail = new Batch_Error_Detail__c(
                Batch_Log__c = log.Id,
                Error_Type__c = errorType,
                Error_Count__c = errorTypeCount.get(errorType)
            );
            insert errorDetail;
        }
        
        // Send notification if there were errors
        if (errorCount > 0) {
            sendErrorNotification(log.Id, errorCount, errorTypeCount);
        }
    }
    
    private String cleanPhoneNumber(String phone) {
        return phone.replaceAll('[^0-9]', '');
    }
    
    private Boolean isValidEmail(String email) {
        String emailRegex = '^[a-zA-Z0-9._|\\\\%#~`=?&/$^*!}{+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$';
        Pattern pattern = Pattern.compile(emailRegex);
        Matcher matcher = pattern.matcher(email);
        return matcher.matches();
    }
    
    private void sendErrorNotification(Id logId, Integer errors, Map<String, Integer> errorTypes) {
        // Email notification logic
    }
}

Use Case 3: Multi-Step Processing

global class OrderProcessingBatch implements Database.Batchable<sObject>, Database.Stateful {
    
    global Integer ordersProcessed = 0;
    global Decimal totalOrderValue = 0;
    global Set<Id> accountsToUpdate = new Set<Id>();
    global Map<Id, Decimal> accountRevenue = new Map<Id, Decimal>();
    
    global Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(
            'SELECT Id, AccountId, TotalAmount, Status FROM Order WHERE Status = \'Pending\''
        );
    }
    
    global void execute(Database.BatchableContext bc, List<Order> scope) {
        List<Order> ordersToUpdate = new List<Order>();
        
        for (Order ord : scope) {
            // Process order
            ord.Status = 'Processed';
            ordersToUpdate.add(ord);
            
            // Track statistics
            ordersProcessed++;
            totalOrderValue += ord.TotalAmount != null ? ord.TotalAmount : 0;
            
            // Track accounts that need updating
            accountsToUpdate.add(ord.AccountId);
            
            // Calculate revenue by account
            if (!accountRevenue.containsKey(ord.AccountId)) {
                accountRevenue.put(ord.AccountId, 0);
            }
            accountRevenue.put(ord.AccountId, 
                accountRevenue.get(ord.AccountId) + (ord.TotalAmount != null ? ord.TotalAmount : 0));
        }
        
        update ordersToUpdate;
    }
    
    global void finish(Database.BatchableContext bc) {
        // Update accounts with total revenue
        List<Account> accountsToUpdateList = [SELECT Id, Total_Revenue__c 
                                               FROM Account 
                                               WHERE Id IN :accountsToUpdate];
        
        for (Account acc : accountsToUpdateList) {
            if (accountRevenue.containsKey(acc.Id)) {
                acc.Total_Revenue__c = (acc.Total_Revenue__c != null ? acc.Total_Revenue__c : 0) 
                                      + accountRevenue.get(acc.Id);
            }
        }
        
        update accountsToUpdateList;
        
        // Create summary record
        Order_Processing_Summary__c summary = new Order_Processing_Summary__c(
            Processing_Date__c = Date.today(),
            Orders_Processed__c = ordersProcessed,
            Total_Order_Value__c = totalOrderValue,
            Accounts_Updated__c = accountsToUpdate.size()
        );
        insert summary;
        
        System.debug('Batch completed: ' + ordersProcessed + ' orders processed');
    }
}

Use Case 4: Collecting Data for Post-Processing

global class DuplicateDetectionBatch implements Database.Batchable<sObject>, Database.Stateful {
    
    global Map<String, List<Id>> emailToAccountIds = new Map<String, List<Id>>();
    global List<Id> duplicateAccountIds = new List<Id>();
    
    global Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(
            'SELECT Id, Email__c FROM Account WHERE Email__c != null ORDER BY Email__c'
        );
    }
    
    global void execute(Database.BatchableContext bc, List<Account> scope) {
        for (Account acc : scope) {
            String email = acc.Email__c.toLowerCase();
            
            if (!emailToAccountIds.containsKey(email)) {
                emailToAccountIds.put(email, new List<Id>());
            }
            emailToAccountIds.get(email).add(acc.Id);
        }
    }
    
    global void finish(Database.BatchableContext bc) {
        // Find duplicates
        List<Duplicate_Record__c> duplicatesToInsert = new List<Duplicate_Record__c>();
        
        for (String email : emailToAccountIds.keySet()) {
            List<Id> accountIds = emailToAccountIds.get(email);
            
            if (accountIds.size() > 1) {
                // Found duplicates
                for (Id accId : accountIds) {
                    duplicatesToInsert.add(new Duplicate_Record__c(
                        Account__c = accId,
                        Duplicate_Email__c = email,
                        Duplicate_Count__c = accountIds.size(),
                        Detected_Date__c = Date.today()
                    ));
                    duplicateAccountIds.add(accId);
                }
            }
        }
        
        if (!duplicatesToInsert.isEmpty()) {
            insert duplicatesToInsert;
            
            // Notify admin
            System.debug('Found ' + duplicateAccountIds.size() + ' duplicate accounts');
        }
    }
}

Important Considerations:

1. Heap Size Limits:

  • Be careful with large collections
  • Stateful variables count against heap size
  • Monitor memory usage
global class MemoryEfficientBatch implements Database.Batchable<sObject>, Database.Stateful {
    
    // Good: primitive types
    global Integer recordCount = 0;
    global Decimal totalAmount = 0;
    
    // Risky: large collections
    global List<Id> allProcessedIds = new List<Id>();  // Could grow very large!
    
    // Better: use summary data
    global Map<String, Integer> summaryByType = new Map<String, Integer>();  // Limited size
}

2. Testing Stateful Batches:

@isTest
private class BatchWithStatefulTest {
    
    @testSetup
    static void setup() {
        List<Account> accounts = new List<Account>();
        for (Integer i = 0; i < 250; i++) {
            accounts.add(new Account(
                Name = 'Test Account ' + i,
                Industry = i < 100 ? 'Technology' : 'Finance'
            ));
        }
        insert accounts;
    }
    
    @isTest
    static void testStatefulBatch() {
        Test.startTest();
        SalesReportBatch batch = new SalesReportBatch();
        Database.executeBatch(batch, 200);
        Test.stopTest();
        
        // Verify stateful data persisted
        List<Sales_Report__c> reports = [SELECT Total_Revenue__c FROM Sales_Report__c];
        System.assertEquals(1, reports.size(), 'Report should be created');
    }
}

Comparison:

FeatureWithout StatefulWith Stateful
Instance VariablesReset each batchPersist across batches
Use CaseIndependent batch processingAccumulating data, reporting
MemoryLowerHigher (be careful)
ComplexitySimplerMore complex
Best ForStandard updatesAggregation, counting, tracking

Best Practices:

  1. Use sparingly – only when you need to maintain state
  2. Monitor heap size – be careful with large collections
  3. Document clearly – explain why stateful is needed
  4. Test thoroughly – with realistic data volumes
  5. Consider alternatives – sometimes you can use finish() queries instead
  6. Clean up – clear large collections when no longer needed

Q33. What are Aggregate SOQL Functions?

Aggregate Functions: Aggregate functions allow you to perform calculations on a set of records without retrieving all the individual records. They help you group, count, sum, and compute values efficiently.

Available Aggregate Functions:

  1. COUNT() – Count records
  2. COUNT_DISTINCT() – Count unique values
  3. SUM() – Sum numeric values
  4. AVG() – Calculate average
  5. MIN() – Find minimum value
  6. MAX() – Find maximum value

Basic Usage:

COUNT() – Counting Records:

public class AggregateExamples {
    
    // Count all accounts
    public static Integer getTotalAccounts() {
        AggregateResult result = [SELECT COUNT() totalAccounts FROM Account];
        return (Integer)result.get('totalAccounts');
    }
    
    // Count accounts with specific criteria
    public static Integer getActiveAccounts() {
        AggregateResult result = [SELECT COUNT() total 
                                  FROM Account 
                                  WHERE IsActive__c = true];
        return (Integer)result.get('total');
    }
    
    // Count with alias
    public static void countByIndustry() {
        List<AggregateResult> results = [SELECT Industry, COUNT(Id) accountCount 
                                         FROM Account 
                                         GROUP BY Industry];
        
        for (AggregateResult ar : results) {
            String industry = (String)ar.get('Industry');
            Integer count = (Integer)ar.get('accountCount');
            System.debug(industry + ': ' + count);
        }
    }
}

COUNT_DISTINCT() – Unique Values:

public class UniqueValueCounter {
    
    // Count unique industries
    public static Integer getUniqueIndustries() {
        AggregateResult result = [SELECT COUNT_DISTINCT(Industry) uniqueIndustries 
                                  FROM Account];
        return (Integer)result.get('uniqueIndustries');
    }
    
    // Count unique owners
    public static Integer getUniqueAccountOwners() {
        AggregateResult result = [SELECT COUNT_DISTINCT(OwnerId) uniqueOwners 
                                  FROM Account];
        return (Integer)result.get('uniqueOwners');
    }
}

SUM() – Total Values:

public class RevenueCalculator {
    
    // Total annual revenue
    public static Decimal getTotalRevenue() {
        AggregateResult result = [SELECT SUM(AnnualRevenue) totalRevenue 
                                  FROM Account];
        return (Decimal)result.get('totalRevenue');
    }
    
    // Sum by industry
    public static Map<String, Decimal> getRevenueByIndustry() {
        Map<String, Decimal> revenueMap = new Map<String, Decimal>();
        
        List<AggregateResult> results = [SELECT Industry, SUM(AnnualRevenue) revenue
                                         FROM Account 
                                         GROUP BY Industry];
        
        for (AggregateResult ar : results) {
            String industry = (String)ar.get('Industry');
            Decimal revenue = (Decimal)ar.get('revenue');
            revenueMap.put(industry, revenue);
        }
        
        return revenueMap;
    }
    
    // Total opportunity amount by stage
    public static void getOpportunityTotalsByStage() {
        List<AggregateResult> results = [SELECT StageName, SUM(Amount) total
                                         FROM Opportunity 
                                         GROUP BY StageName];
        
        for (AggregateResult ar : results) {
            System.debug(ar.get('StageName') + ': $' + ar.get('total'));
        }
    }
}

AVG() – Average Values:

public class AverageCalculator {
    
    // Average annual revenue
    public static Decimal getAverageRevenue() {
        AggregateResult result = [SELECT AVG(AnnualRevenue) avgRevenue 
                                  FROM Account];
        return (Decimal)result.get('avgRevenue');
    }
    
    // Average by industry
    public static Map<String, Decimal> getAverageRevenueByIndustry() {
        Map<String, Decimal> avgMap = new Map<String, Decimal>();
        
        List<AggregateResult> results = [SELECT Industry, AVG(AnnualRevenue) average
                                         FROM Account 
                                         GROUP BY Industry];
        
        for (AggregateResult ar : results) {
            avgMap.put((String)ar.get('Industry'), (Decimal)ar.get('average'));
        }
        
        return avgMap;
    }
    
    // Average opportunity amount
    public static Decimal getAverageOpportunityAmount() {
        AggregateResult result = [SELECT AVG(Amount) avgAmount 
                                  FROM Opportunity 
                                  WHERE StageName = 'Closed Won'];
        return (Decimal)result.get('avgAmount');
    }
}

MIN() and MAX():

public class MinMaxCalculator {
    
    // Find date range of opportunities
    public static Map<String, Date> getOpportunityDateRange() {
        AggregateResult result = [SELECT MIN(CloseDate) minDate, MAX(CloseDate) maxDate
                                  FROM Opportunity];
        
        return new Map<String, Date>{
            'earliest' => (Date)result.get('minDate'),
            'latest' => (Date)result.get('maxDate')
        };
    }
    
    // Get revenue range by industry
    public static void getRevenueRangeByIndustry() {
        List<AggregateResult> results = [SELECT Industry, 
                                         MIN(AnnualRevenue) minRevenue,
                                         MAX(AnnualRevenue) maxRevenue
                                         FROM Account 
                                         GROUP BY Industry];
        
        for (AggregateResult ar : results) {
            System.debug('Industry: ' + ar.get('Industry'));
            System.debug('Min: $' + ar.get('minRevenue'));
            System.debug('Max: $' + ar.get('maxRevenue'));
        }
    }
}

Advanced Usage:

Multiple GROUP BY:

public class AdvancedGrouping {
    
    // Group by multiple fields
    public static void getOpportunitiesByStageAndOwner() {
        List<AggregateResult> results = [SELECT StageName, Owner.Name, 
                                         COUNT(Id) oppCount,
                                         SUM(Amount) totalAmount
                                         FROM Opportunity 
                                         GROUP BY StageName, Owner.Name
                                         ORDER BY StageName, Owner.Name];
        
        for (AggregateResult ar : results) {
            System.debug('Stage: ' + ar.get('StageName'));
            System.debug('Owner: ' + ar.get('Name'));
            System.debug('Count: ' + ar.get('oppCount'));
            System.debug('Total: $' + ar.get('totalAmount'));
        }
    }
    
    // Group by date functions
    public static void getOpportunitiesByMonthAndYear() {
        List<AggregateResult> results = [SELECT CALENDAR_YEAR(CloseDate) year,
                                         CALENDAR_MONTH(CloseDate) month,
                                         COUNT(Id) count,
                                         SUM(Amount) total
                                         FROM Opportunity 
                                         GROUP BY CALENDAR_YEAR(CloseDate), 
                                                 CALENDAR_MONTH(CloseDate)
                                         ORDER BY CALENDAR_YEAR(CloseDate), 
                                                 CALENDAR_MONTH(CloseDate)];
        
        for (AggregateResult ar : results) {
            System.debug('Year: ' + ar.get('year') + ', Month: ' + ar.get('month'));
            System.debug('Count: ' + ar.get('count') + ', Total: $' + ar.get('total'));
        }
    }
}

HAVING Clause:

public class HavingClauseExamples {
    
    // Find industries with more than 10 accounts
    public static List<String> getLargeIndustries() {
        List<String> industries = new List<String>();
        
        List<AggregateResult> results = [SELECT Industry, COUNT(Id) accountCount
                                         FROM Account 
                                         GROUP BY Industry 
                                         HAVING COUNT(Id) > 10];
        
        for (AggregateResult ar : results) {
            industries.add((String)ar.get('Industry'));
        }
        
        return industries;
    }
    
    // Find accounts with high total opportunity value
    public static List<Id> getHighValueAccounts(Decimal minTotalValue) {
        List<Id> accountIds = new List<Id>();
        
        List<AggregateResult> results = [SELECT AccountId, SUM(Amount) total
                                         FROM Opportunity 
                                         WHERE AccountId != null 
                                         GROUP BY AccountId 
                                         HAVING SUM(Amount) > :minTotalValue];
        
        for (AggregateResult ar : results) {
            accountIds.add((Id)ar.get('AccountId'));
        }
        
        return accountIds;
    }
    
    // Find owners with average deal size over threshold
    public static List<Id> getTopPerformingOwners(Decimal avgThreshold) {
        List<Id> ownerIds = new List<Id>();
        
        List<AggregateResult> results = [SELECT OwnerId, AVG(Amount) avgAmount
                                         FROM Opportunity 
                                         WHERE StageName = 'Closed Won' 
                                         GROUP BY OwnerId 
                                         HAVING AVG(Amount) > :avgThreshold];
        
        for (AggregateResult ar : results) {
            ownerIds.add((Id)ar.get('OwnerId'));
        }
        
        return ownerIds;
    }
}

Date Functions with Aggregates:

public class DateAggregation {
    
    // Get opportunities by fiscal quarter
    public static void getOpportunitiesByFiscalQuarter() {
        List<AggregateResult> results = [SELECT FISCAL_QUARTER(CloseDate) quarter,
                                         FISCAL_YEAR(CloseDate) year,
                                         COUNT(Id) count,
                                         SUM(Amount) total
                                         FROM Opportunity 
                                         GROUP BY FISCAL_QUARTER(CloseDate), 
                                                 FISCAL_YEAR(CloseDate)];
        
        for (AggregateResult ar : results) {
            System.debug('FY' + ar.get('year') + ' Q' + ar.get('quarter'));
            System.debug('Count: ' + ar.get('count') + ', Total: $' + ar.get('total'));
        }
    }
    
    // Get cases created per day
    public static void getCasesByDay() {
        List<AggregateResult> results = [SELECT DAY_ONLY(CreatedDate) day,
                                         COUNT(Id) caseCount
                                         FROM Case 
                                         WHERE CreatedDate = LAST_N_DAYS:30
                                         GROUP BY DAY_ONLY(CreatedDate)
                                         ORDER BY DAY_ONLY(CreatedDate)];
        
        for (AggregateResult ar : results) {
            System.debug('Date: ' + ar.get('day') + ', Cases: ' + ar.get('caseCount'));
        }
    }
}

Complex Business Analytics:

public class SalesAnalytics {
    
    // Comprehensive sales dashboard data
    public class DashboardData {
        public Decimal totalRevenue;
        public Decimal averageDealSize;
        public Integer totalDeals;
        public Integer wonDeals;
        public Decimal winRate;
        public Map<String, Decimal> revenueByStage;
        public Map<String, Integer> dealsByIndustry;
    }
    
    public static DashboardData getDashboardData() {
        DashboardData data = new DashboardData();
        
        // Get overall statistics
        AggregateResult overall = [SELECT COUNT(Id) total,
                                   SUM(Amount) revenue,
                                   AVG(Amount) avgDeal
                                   FROM Opportunity 
                                   WHERE CloseDate = THIS_YEAR][0];
        
        data.totalDeals = (Integer)overall.get('total');
        data.totalRevenue = (Decimal)overall.get('revenue');
        data.averageDealSize = (Decimal)overall.get('avgDeal');
        
        // Get won deals
        AggregateResult won = [SELECT COUNT(Id) wonCount
                               FROM Opportunity 
                               WHERE StageName = 'Closed Won' 
                               AND CloseDate = THIS_YEAR][0];
        
        data.wonDeals = (Integer)won.get('wonCount');
        data.winRate = (Decimal)data.wonDeals / data.totalDeals * 100;
        
        // Revenue by stage
        data.revenueByStage = new Map<String, Decimal>();
        List<AggregateResult> byStage = [SELECT StageName, SUM(Amount) stageRevenue
                                         FROM Opportunity 
                                         WHERE CloseDate = THIS_YEAR 
                                         GROUP BY StageName];
        
        for (AggregateResult ar : byStage) {
            data.revenueByStage.put((String)ar.get('StageName'), 
                                    (Decimal)ar.get('stageRevenue'));
        }
        
        // Deals by industry
        data.dealsByIndustry = new Map<String, Integer>();
        List<AggregateResult> byIndustry = [SELECT Account.Industry, COUNT(Id) dealCount
                                            FROM Opportunity 
                                            WHERE CloseDate = THIS_YEAR 
                                            GROUP BY Account.Industry];
        
        for (AggregateResult ar : byIndustry) {
            data.dealsByIndustry.put((String)ar.get('Industry'), 
                                     (Integer)ar.get('dealCount'));
        }
        
        return data;
    }
}

Performance Optimization Example:

public class OptimizedReporting {
    
    // Efficient: Use aggregates instead of processing all records
    public static Decimal getTotalRevenueEfficient() {
        // One query with aggregate
        AggregateResult result = [SELECT SUM(Amount) total 
                                  FROM Opportunity 
                                  WHERE StageName = 'Closed Won'];
        return (Decimal)result.get('total');
    }
    
    // Inefficient: Don't do this
    public static Decimal getTotalRevenueInefficient() {
        // Retrieves all records, processes in memory
        List<Opportunity> opps = [SELECT Amount 
                                  FROM Opportunity 
                                  WHERE StageName = 'Closed Won'];
        
        Decimal total = 0;
        for (Opportunity opp : opps) {
            total += opp.Amount != null ? opp.Amount : 0;
        }
        return total;
    }
}

Best Practices:

  1. Use aggregates instead of processing in Apex when possible
  2. Always use aliases for clarity
  3. Limit results with WHERE and HAVING
  4. Use LIMIT to prevent hitting governor limits
  5. Handle nulls properly
  6. Test with large datasets

Complete Example – Sales Performance Report:

public class SalesPerformanceReport {
    
    public class PerformanceMetrics {
        public String ownerName;
        public Integer totalOpportunities;
        public Integer wonOpportunities;
        public Decimal winRate;
        public Decimal totalRevenue;
        public Decimal averageDealSize;
        public Decimal largestDeal;
    }
    
    public static List<PerformanceMetrics> getTopPerformers(Integer topN) {
        List<PerformanceMetrics> metrics = new List<PerformanceMetrics>();
        
        List<AggregateResult> results = [
            SELECT Owner.Name ownerName,
                   COUNT(Id) totalOpps,
                   COUNT_DISTINCT(CASE WHEN StageName = 'Closed Won' THEN Id ELSE null END) wonOpps,
                   SUM(CASE WHEN StageName = 'Closed Won' THEN Amount ELSE 0 END) revenue,
                   AVG(CASE WHEN StageName = 'Closed Won' THEN Amount ELSE null END) avgDeal,
                   MAX(CASE WHEN StageName = 'Closed Won' THEN Amount ELSE 0 END) maxDeal
            FROM Opportunity 
            WHERE CloseDate = THIS_YEAR
            GROUP BY Owner.Name
            ORDER BY SUM(CASE WHEN StageName = 'Closed Won' THEN Amount ELSE 0 END) DESC
            LIMIT :topN
        ];
        
        for (AggregateResult ar : results) {
            PerformanceMetrics pm = new PerformanceMetrics();
            pm.ownerName = (String)ar.get('ownerName');
            pm.totalOpportunities = (Integer)ar.get('totalOpps');
            pm.wonOpportunities = (Integer)ar.get('wonOpps');
            pm.totalRevenue = (Decimal)ar.get('revenue');
            pm.averageDealSize = (Decimal)ar.get('avgDeal');
            pm.largestDeal = (Decimal)ar.get('maxDeal');
            pm.winRate = pm.totalOpportunities > 0 ? 
                        (Decimal)pm.wonOpportunities / pm.totalOpportunities * 100 : 0;
            
            metrics.add(pm);
        }
        
        return metrics;
    }
}

Governor Limits:

  • Aggregate queries count as one SOQL query
  • Result limited to 2000 rows without LIMIT clause
  • Use LIMIT to control number of grouped results

Q34. How do you handle governor limits in integrations?

Governor Limits in Integrations: When integrating Salesforce with external systems, you must be mindful of various governor limits, especially:

  • SOQL queries (100 synchronous, 200 asynchronous)
  • DML statements (150)
  • Callout limits (100 per transaction)
  • Heap size (6MB synchronous, 12MB asynchronous)
  • CPU time (10,000ms synchronous, 60,000ms asynchronous)

Strategies to Handle Governor Limits:

1. Use Asynchronous Processing

@future for Callouts:

public class AsyncIntegration {
    
    // Trigger or synchronous method
    public static void processRecords(List<Account> accounts) {
        Set<Id> accountIds = new Set<Id>();
        for (Account acc : accounts) {
            accountIds.add(acc.Id);
        }
        
        // Call asynchronous method
        syncAccountsAsync(accountIds);
    }
    
    @future(callout=true)
    public static void syncAccountsAsync(Set<Id> accountIds) {
        List<Account> accounts = [SELECT Id, Name, Industry, AnnualRevenue 
                                  FROM Account 
                                  WHERE Id IN :accountIds];
        
        for (Account acc : accounts) {
            syncSingleAccount(acc);
        }
    }
    
    private static void syncSingleAccount(Account acc) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.external.com/accounts');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(JSON.serialize(acc));
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        if (res.getStatusCode() == 200) {
            System.debug('Account synced: ' + acc.Name);
        }
    }
}

Queueable for Complex Processing:

public class IntegrationQueueable implements Queueable, Database.AllowsCallouts {
    
    private List<Account> accounts;
    private Integer startIndex;
    private static final Integer BATCH_SIZE = 10;
    
    public IntegrationQueueable(List<Account> accounts, Integer startIndex) {
        this.accounts = accounts;
        this.startIndex = startIndex;
    }
    
    public void execute(QueueableContext context) {
        Integer endIndex = Math.min(startIndex + BATCH_SIZE, accounts.size());
        
        // Process batch
        for (Integer i = startIndex; i < endIndex; i++) {
            Account acc = accounts[i];
            callExternalSystem(acc);
        }
        
        // Chain next batch if more records exist
        if (endIndex < accounts.size()) {
            System.enqueueJob(new IntegrationQueueable(accounts, endIndex));
        }
    }
    
    private void callExternalSystem(Account acc) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.external.com/accounts/' + acc.Id);
        req.setMethod('PUT');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(JSON.serialize(new Map<String, Object>{
            'name' => acc.Name,
            'industry' => acc.Industry
        }));
        
        try {
            Http http = new Http();
            HttpResponse res = http.send(req);
            
            if (res.getStatusCode() == 200) {
                // Log success
                System.debug('Synced: ' + acc.Name);
            } else {
                // Log error
                System.debug('Error syncing: ' + acc.Name + ', Status: ' + res.getStatusCode());
            }
        } catch (Exception e) {
            System.debug('Exception: ' + e.getMessage());
        }
    }
}

// Usage
public class AccountTriggerHandler {
    public static void syncAccounts(List<Account> accounts) {
        if (!System.isFuture() && !System.isBatch()) {
            System.enqueueJob(new IntegrationQueueable(accounts, 0));
        }
    }
}

2. Batch Apex for Large Data Volumes

global class BulkSyncBatch implements Database.Batchable<sObject>, Database.AllowsCallouts, Database.Stateful {
    
    global Integer successCount = 0;
    global Integer errorCount = 0;
    
    global Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(
            'SELECT Id, Name, Industry, Website FROM Account WHERE LastModifiedDate = TODAY'
        );
    }
    
    global void execute(Database.BatchableContext bc, List<Account> scope) {
        // Make one callout per batch (or a few)
        String jsonData = JSON.serialize(scope);
        
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.external.com/accounts/bulk');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(jsonData);
        req.setTimeout(120000); // 120 seconds
        
        try {
            Http http = new Http();
            HttpResponse res = http.send(req);
            
            if (res.getStatusCode() == 200) {
                successCount += scope.size();
                
                // Parse response and update records if needed
                Map<String, Object> responseData = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
                // Process response...
                
            } else {
                errorCount += scope.size();
                // Log errors
            }
        } catch (Exception e) {
            errorCount += scope.size();
            System.debug('Batch error: ' + e.getMessage());
        }
    }
    
    global void finish(Database.BatchableContext bc) {
        // Send summary email
        System.debug('Sync complete. Success: ' + successCount + ', Errors: ' + errorCount);
    }
}

3. Bulkify Callouts

Bad: Individual Callouts

// DON'T DO THIS - Hits callout limit quickly
public static void syncAccountsIndividually(List<Account> accounts) {
    for (Account acc : accounts) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.external.com/accounts');
        req.setMethod('POST');
        req.setBody(JSON.serialize(acc));
        
        Http http = new Http();
        HttpResponse res = http.send(req);  // One callout per account!
    }
}

Good: Bulk Callout

public static void syncAccountsBulk(List<Account> accounts) {
    // Single callout with all accounts
    HttpRequest req = new HttpRequest();
    req.setEndpoint('https://api.external.com/accounts/bulk');
    req.setMethod('POST');
    req.setHeader('Content-Type', 'application/json');
    req.setBody(JSON.serialize(accounts));  // Send all at once
    req.setTimeout(120000);
    
    Http http = new Http();
    HttpResponse res = http.send(req);
    
    if (res.getStatusCode() == 200) {
        // Process bulk response
        Map<String, Object> results = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
        // Handle results...
    }
}

4. Platform Events for Decoupling

// Publisher - doesn't wait for integration
public class AccountEventPublisher {
    
    public static void publishAccountChanges(List<Account> accounts) {
        List<Account_Event__e> events = new List<Account_Event__e>();
        
        for (Account acc : accounts) {
            events.add(new Account_Event__e(
                Account_Id__c = acc.Id,
                Account_Name__c = acc.Name,
                Industry__c = acc.Industry,
                Event_Type__c = 'UPDATE'
            ));
        }
        
        // Publish events (doesn't count against callout limits)
        List<Database.SaveResult> results = EventBus.publish(events);
        
        for (Database.SaveResult result : results) {
            if (!result.isSuccess()) {
                System.debug('Error publishing event');
            }
        }
    }
}

// Subscriber - processes events asynchronously
trigger AccountEventTrigger on Account_Event__e (after insert) {
    List<String> accountIds = new List<String>();
    
    for (Account_Event__e event : Trigger.new) {
        accountIds.add(event.Account_Id__c);
    }
    
    // Process in queueable for callouts
    System.enqueueJob(new AccountSyncQueueable(accountIds));
}

5. Continuation for Long-Running Callouts

public class LongRunningCallout {
    
    @future(callout=true)
    public static void processLargeDataset(String datasetId) {
        // Initiate long-running process
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.external.com/process');
        req.setMethod('POST');
        req.setBody(JSON.serialize(new Map<String, String>{
            'datasetId' => datasetId,
            'callbackUrl' => 'https://yourorg.my.salesforce.com/services/apexrest/callback'
        }));
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        if (res.getStatusCode() == 202) {
            // Process accepted, will receive callback later
            System.debug('Process initiated');
        }
    }
}

// Callback endpoint
@RestResource(urlMapping='/callback/*')
global class CallbackHandler {
    
    @HttpPost
    global static void handleCallback() {
        RestRequest req = RestContext.request;
        Map<String, Object> payload = (Map<String, Object>)JSON.deserializeUntyped(req.requestBody.toString());
        
        String status = (String)payload.get('status');
        String datasetId = (String)payload.get('datasetId');
        
        if (status == 'completed') {
            // Process results
            System.debug('Processing complete for: ' + datasetId);
        }
    }
}

6. Caching to Reduce Queries

public class CachedIntegration {
    
    private static Map<String, API_Config__mdt> configCache;
    private static Map<Id, Account> accountCache;
    
    static {
        // Load configuration once
        configCache = new Map<String, API_Config__mdt>();
        for (API_Config__mdt config : [SELECT DeveloperName, Endpoint__c, API_Key__c 
                                       FROM API_Config__mdt]) {
            configCache.put(config.DeveloperName, config);
        }
    }
    
    public static API_Config__mdt getConfig(String configName) {
        return configCache.get(configName);
    }
    
    public static void bulkSyncAccounts(Set<Id> accountIds) {
        // Query once, cache results
        accountCache = new Map<Id, Account>(
            [SELECT Id, Name, Industry, Website 
             FROM Account 
             WHERE Id IN :accountIds]
        );
        
        // Use cached accounts for processing
        for (Id accId : accountIds) {
            Account acc = accountCache.get(accId);
            if (acc != null) {
                syncAccount(acc);
            }
        }
    }
    
    private static void syncAccount(Account acc) {
        API_Config__mdt config = getConfig('Default');
        
        HttpRequest req = new HttpRequest();
        req.setEndpoint(config.Endpoint__c + '/accounts');
        req.setMethod('POST');
        req.setHeader('Authorization', 'Bearer ' + config.API_Key__c);
        req.setBody(JSON.serialize(acc));
        
        // Make callout...
    }
}

7. Monitoring and Error Handling

public class IntegrationMonitor {
    
    public class IntegrationMetrics {
        public Integer calloutCount = 0;
        public Integer soqlCount = 0;
        public Integer dmlCount = 0;
        public Long cpuTime = 0;
        public Integer heapSize = 0;
    }
    
    public static IntegrationMetrics getMetrics() {
        IntegrationMetrics metrics = new IntegrationMetrics();
        
        metrics.calloutCount = Limits.getCallouts();
        metrics.soqlCount = Limits.getQueries();
        metrics.dmlCount = Limits.getDMLStatements();
        metrics.cpuTime = Limits.getCpuTime();
        metrics.heapSize = Limits.getHeapSize();
        
        return metrics;
    }
    
    public static Boolean canMakeCallout() {
        return Limits.getCallouts() < Limits.getLimitCallouts();
    }
    
    public static void logLimitWarning(String context) {
        if (Limits.getCallouts() > Limits.getLimitCallouts() * 0.8) {
            System.debug('WARNING: Approaching callout limit in ' + context);
        }
        
        if (Limits.getQueries() > Limits.getLimitQueries() * 0.8) {
            System.debug('WARNING: Approaching SOQL limit in ' + context);
        }
    }
}

// Usage
public class SafeIntegration {
    
    public static void syncAccounts(List<Account> accounts) {
        if (!IntegrationMonitor.canMakeCallout()) {
            System.debug('Cannot make callout - limit reached');
            // Queue for later or use platform events
            return;
        }
        
        // Make callout
        HttpRequest req = new HttpRequest();
        // ... setup request
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        IntegrationMonitor.logLimitWarning('Account Sync');
    }
}

8. Composite API for Multiple Operations

public class CompositeAPIIntegration {
    
    public static void performMultipleOperations() {
        // Use Composite API to reduce callout count
        HttpRequest req = new HttpRequest();
        req.setEndpoint(URL.getSalesforceBaseUrl().toExternalForm() + 
                       '/services/data/v59.0/composite');
        req.setMethod('POST');
        req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId());
        req.setHeader('Content-Type', 'application/json');
        
        // Multiple operations in one callout
        Map<String, Object> compositeRequest = new Map<String, Object>{
            'allOrNone' => false,
            'compositeRequest' => new List<Object>{
                new Map<String, Object>{
                    'method' => 'POST',
                    'url' => '/services/data/v59.0/sobjects/Account',
                    'referenceId' => 'NewAccount',
                    'body' => new Map<String, String>{'Name' => 'Test Account'}
                },
                new Map<String, Object>{
                    'method' => 'GET',
                    'url' => '/services/data/v59.0/sobjects/Account/001xx000003DGb0AAG',
                    'referenceId' => 'GetAccount'
                }
            }
        };
        
        req.setBody(JSON.serialize(compositeRequest));
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        System.debug('Composite response: ' + res.getBody());
    }
}

Best Practices Summary:

  1. Use asynchronous processing (@future, Queueable, Batch)
  2. Bulkify callouts – send multiple records in one request
  3. Use Platform Events for decoupling
  4. Implement caching to reduce queries
  5. Monitor limits proactively
  6. Handle errors gracefully
  7. Use Composite/Batch APIs when possible
  8. Test with realistic data volumes
  9. Document integration patterns
  10. Implement retry logic for failed callouts

Further Learning

Uday Bheemarpu
Uday Bheemarpu
Articles: 9

Newsletter Updates

Enter your email address below and subscribe to our newsletter

Leave a Reply

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

Discover more from Panther Schools

Subscribe now to keep reading and get access to the full archive.

Continue reading