Salesforce Development Interview Questions & Answers Part 1

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

Introduction

This comprehensive guide covers essential Apex development interview questions ranging from basic concepts to advanced scenarios. Each question includes detailed explanations, practical code examples, and real-world applications to help you prepare thoroughly for Salesforce developer interviews.

Whether you’re a beginner preparing for your first Apex interview or an experienced developer looking to refresh your knowledge, this guide provides in-depth coverage of:

  • Core Apex concepts and syntax
  • Trigger development and best practices
  • Asynchronous processing patterns
  • Integration techniques
  • Testing strategies
  • Real-world scenario implementations

Section 1: Basic Apex Questions

Q1. What is Apex and what are its key features?

Answer:

Apex is a strongly typed, object-oriented programming language that allows developers to execute flow and transaction control statements on the Salesforce platform’s server. It is syntactically similar to Java and acts like database stored procedures.

Key Features:

  • Cloud-Based and Multitenant Architecture: Runs on Salesforce servers in a multitenant environment
  • Integrated with Database: Built-in DML operations and SOQL/SOSL for database interactions
  • Governor Limits: Enforces runtime limits to ensure efficient resource usage
  • Built-in Testing Framework: Provides robust testing capabilities with @isTest annotation
  • Versioned and Upgradeable: API versioning allows code to run on specific platform versions
  • Strongly Typed: All variables and expressions have a type known at compile time
  • Case-Insensitive: Apex is not case-sensitive (e.g., String and string are equivalent)
  • Automatic Memory Management: Garbage collection is automatic
  • Easy to Use: Similar syntax to Java makes it accessible to Java developers

Example:

public class HelloWorld {
    public static void sayHello() {
        System.debug('Hello, Salesforce World!');
    }
}

When to Use Apex:

  • Complex business logic that cannot be handled by declarative tools
  • Web services integrations
  • Email services
  • Complex validation rules
  • Custom transactional logic

Q2. What are the different types of collections in Apex?

Answer:

Apex supports three main types of collections, each with specific use cases and characteristics:

1. List (unOrdered Collection)

  • Definition: An ordered collection of elements that allows duplicates
  • Access: Elements accessed by index (0-based)
  • Use Case: When order matters or duplicates are allowed
// Declaration and initialization
List<String> names = new List<String>();
names.add('John');
names.add('Jane');
names.add('John'); // Duplicates allowed

// Alternative syntax
List<String> colors = new List<String>{'Red', 'Blue', 'Green'};

// Access by index
String firstName = names[0]; // Returns 'John'
Integer size = names.size(); // Returns 3

// Iteration
for(String name : names) {
    System.debug(name);
}

// SOQL directly to List
List<Account> accounts = [SELECT Id, Name FROM Account LIMIT 10];

2. Set (ordered Unique Collection)

  • Definition: An unordered collection that does not allow duplicates
  • Access: Cannot access by index
  • Use Case: When uniqueness is required or checking membership
// Declaration and initialization
Set<String> uniqueNames = new Set<String>();
uniqueNames.add('John');
uniqueNames.add('Jane');
uniqueNames.add('John'); // Will not be added (duplicate)

// Alternative syntax
Set<Integer> numbers = new Set<Integer>{1, 2, 3, 4, 5};

// Check membership
Boolean hasJohn = uniqueNames.contains('John'); // Returns true

// Set operations
Set<String> set1 = new Set<String>{'A', 'B', 'C'};
Set<String> set2 = new Set<String>{'B', 'C', 'D'};

set1.addAll(set2); // Union
set1.retainAll(set2); // Intersection
set1.removeAll(set2); // Difference

// Common use case: Collecting unique IDs
Set<Id> accountIds = new Set<Id>();
for(Contact con : contacts) {
    accountIds.add(con.AccountId);
}

3. Map (Key-Value Pairs)

  • Definition: Collection of key-value pairs where each unique key maps to a single value
  • Access: Fast lookup by key
  • Use Case: When you need efficient lookups or grouping
// Declaration and initialization
Map<String, Integer> scoreMap = new Map<String, Integer>();
scoreMap.put('John', 95);
scoreMap.put('Jane', 87);

// Alternative syntax
Map<String, String> countryCapital = new Map<String, String>{
    'USA' => 'Washington DC',
    'UK' => 'London',
    'India' => 'New Delhi'
};

// Access by key
Integer johnScore = scoreMap.get('John'); // Returns 95

// Check if key exists
Boolean hasJane = scoreMap.containsKey('Jane'); // Returns true

// Iterate over keys
for(String name : scoreMap.keySet()) {
    System.debug(name + ': ' + scoreMap.get(name));
}

// Common pattern: Create Map from SOQL
Map<Id, Account> accountMap = new Map<Id, Account>(
    [SELECT Id, Name FROM Account WHERE Industry = 'Technology']
);

// Grouping pattern
Map<Id, List<Contact>> accountToContacts = new Map<Id, List<Contact>>();
for(Contact con : contacts) {
    if(!accountToContacts.containsKey(con.AccountId)) {
        accountToContacts.put(con.AccountId, new List<Contact>());
    }
    accountToContacts.get(con.AccountId).add(con);
}

Collection Best Practices:

  1. Use Lists when order matters or duplicates are needed
  2. Use Sets for uniqueness and membership testing
  3. Use Maps for fast lookups and grouping
  4. Always initialize collections before use
  5. Use appropriate collection methods for better performance

Q3. Explain the difference between DML and Database methods?

Answer:

Both DML statements and Database methods perform database operations, but they differ in error handling and flexibility.

FeatureDML StatementsDatabase Methods
SyntaxinsertupdateupsertdeleteundeleteDatabase.insert()Database.update(), etc.
Error HandlingThrows exception on any errorReturns result object with error details
Partial SuccessNot supported (all-or-nothing)Supported with allOrNone parameter
Transaction RollbackAutomatic on errorOptional based on allOrNone flag
Return ValueNoneReturns SaveResultUpsertResult, etc.
Use CaseWhen all records must succeedWhen partial success is acceptable
Error InformationException message onlyDetailed error per record

DML Statements Example:

// All-or-nothing approach
try {
    List<Account> accounts = new List<Account>{
        new Account(Name = 'Account 1'),
        new Account(Name = 'Account 2'),
        new Account() // This will fail - no Name
    };
    
    insert accounts; // Entire operation fails
    System.debug('All accounts inserted'); // Won't execute
    
} catch(DmlException e) {
    System.debug('Error: ' + e.getMessage());
    System.debug('Failed at index: ' + e.getDmlIndex(0));
    // All records are rolled back
}

Database Methods Example:

// Partial success approach
List<Account> accounts = new List<Account>{
    new Account(Name = 'Account 1'),
    new Account(Name = 'Account 2'),
    new Account() // This will fail - no Name
};

// allOrNone = false allows partial success
Database.SaveResult[] results = Database.insert(accounts, false);

// Process results
for(Integer i = 0; i < results.size(); i++) {
    if(results[i].isSuccess()) {
        System.debug('Record ' + i + ' inserted successfully: ' + results[i].getId());
    } else {
        // Get errors for failed record
        for(Database.Error error : results[i].getErrors()) {
            System.debug('Record ' + i + ' failed');
            System.debug('Error: ' + error.getMessage());
            System.debug('Fields: ' + error.getFields());
            System.debug('Status Code: ' + error.getStatusCode());
        }
    }
}
// Records 1 and 2 are inserted, Record 3 fails but doesn't rollback others

Database.upsert with External ID:

// Upsert using external ID field
List<Account> accounts = new List<Account>{
    new Account(External_ID__c = 'EXT001', Name = 'Account 1'),
    new Account(External_ID__c = 'EXT002', Name = 'Account 2')
};

Schema.SObjectField extIdField = Account.Fields.External_ID__c;
Database.UpsertResult[] results = Database.upsert(accounts, extIdField, false);

for(Database.UpsertResult result : results) {
    if(result.isSuccess()) {
        System.debug('Record ID: ' + result.getId());
        System.debug('Was created: ' + result.isCreated());
    }
}

When to Use Each:

  • Use DML Statements: When all records must succeed or fail together
  • Use Database Methods: When you need partial success or detailed error handling

Q4. What are Governor Limits in Salesforce?

Answer:

Governor Limits are runtime limits enforced by the Apex runtime engine to ensure that runaway code or processes don’t monopolize shared resources on the multitenant Salesforce platform. These limits prevent one tenant from consuming excessive resources that would impact other tenants.

Key Governor Limits:

Limit TypeSynchronousAsynchronous
Total SOQL queries100200
Total records retrieved by SOQL50,00050,000
Total DML statements150150
Total records processed by DML10,00010,000
Total heap size6 MB12 MB
Maximum CPU time10,000 ms60,000 ms
Total number of callouts100100
Maximum callout response size6 MB6 MB
Total SOSL queries2020

Checking Limits Programmatically:

public class LimitChecker {
    public static void checkLimits() {
        System.debug('SOQL Queries used: ' + Limits.getQueries() + ' / ' + Limits.getLimitQueries());
        System.debug('DML statements used: ' + Limits.getDmlStatements() + ' / ' + Limits.getLimitDmlStatements());
        System.debug('DML rows used: ' + Limits.getDmlRows() + ' / ' + Limits.getLimitDmlRows());
        System.debug('CPU time used: ' + Limits.getCpuTime() + ' / ' + Limits.getLimitCpuTime());
        System.debug('Heap size used: ' + Limits.getHeapSize() + ' / ' + Limits.getLimitHeapSize());
        System.debug('Callouts used: ' + Limits.getCallouts() + ' / ' + Limits.getLimitCallouts());
    }
}

Common Limit Errors and Solutions:

1. Too Many SOQL Queries (101 queries)

❌ Bad Code:

trigger AccountTrigger on Account (after insert) {
    for(Account acc : Trigger.new) {
        // SOQL inside loop - BAD!
        List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
        System.debug('Contacts: ' + contacts.size());
    }
}

✅ Good Code:

trigger AccountTrigger on Account (after insert) {
    Set<Id> accountIds = new Set<Id>();
    for(Account acc : Trigger.new) {
        accountIds.add(acc.Id);
    }
    
    // Single SOQL query outside loop
    Map<Id, List<Contact>> accountContactMap = new Map<Id, List<Contact>>();
    for(Contact con : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
        if(!accountContactMap.containsKey(con.AccountId)) {
            accountContactMap.put(con.AccountId, new List<Contact>());
        }
        accountContactMap.get(con.AccountId).add(con);
    }
    
    for(Account acc : Trigger.new) {
        List<Contact> contacts = accountContactMap.get(acc.Id);
        if(contacts != null) {
            System.debug('Contacts: ' + contacts.size());
        }
    }
}

2. Too Many DML Statements (151 statements)

❌ Bad Code:

for(Account acc : accounts) {
    acc.Rating = 'Hot';
    update acc; // DML inside loop - BAD!
}

✅ Good Code:

List<Account> accountsToUpdate = new List<Account>();
for(Account acc : accounts) {
    acc.Rating = 'Hot';
    accountsToUpdate.add(acc);
}
update accountsToUpdate; // Single DML statement

3. Heap Size Limit Exceeded

// Be careful with large collections and strings
public class HeapManager {
    public static void processLargeData() {
        List<Account> accounts = [SELECT Id, Name, Description FROM Account];
        
        // Check heap size periodically
        Integer heapUsed = Limits.getHeapSize();
        Integer heapLimit = Limits.getLimitHeapSize();
        
        if(heapUsed > (heapLimit * 0.8)) {
            System.debug('Warning: Heap usage at 80%');
            // Process in smaller batches or use different approach
        }
    }
}

Best Practices to Avoid Governor Limits:

  1. Bulkify your code: Always process collections, not individual records
  2. Move SOQL outside loops: Query once and use collections
  3. Move DML outside loops: Collect records and perform single DML
  4. Use Maps for lookups: Avoid nested loops
  5. Use Batch Apex: For processing large data volumes
  6. Use @future or Queueable: For operations that can be asynchronous
  7. Optimize SOQL: Use selective filters and query only needed fields
  8. Use Limits class: Monitor usage and adjust logic accordingly

Q5. What is the difference between Trigger.New and Trigger.Old?

Answer:

Trigger.New and Trigger.Old are context variables available in Apex triggers that provide access to record versions at different stages of the transaction.

Trigger.New

  • TypeList<sObject>
  • Contains: New versions of sObject records
  • Available In: insert, update, and undelete triggers
  • Modifiable: Only in before triggers
  • Use Case: Access new values, validate data, set field values
trigger AccountTrigger on Account (before insert, before update) {
    for(Account acc : Trigger.new) {
        // Access new values
        System.debug('New Name: ' + acc.Name);
        
        // Modify in before context (no DML needed)
        if(acc.Industry == 'Technology') {
            acc.Rating = 'Hot';
        }
    }
}

Trigger.Old

  • TypeList<sObject>
  • Contains: Old versions of sObject records
  • Available In: update and delete triggers only
  • Modifiable: Never (read-only)
  • Use Case: Compare old vs new values, audit changes
trigger AccountTrigger on Account (before update, before delete) {
    for(Account acc : Trigger.old) {
        // Access old values
        System.debug('Old Name: ' + acc.Name);
        
        // In delete context, only Trigger.old is available
        if(Trigger.isDelete) {
            System.debug('Deleting account: ' + acc.Name);
        }
    }
}

Trigger.NewMap and Trigger.OldMap

Convenient Map versions for quick lookups:

trigger AccountTrigger on Account (after update) {
    // Trigger.newMap: Map<Id, Account>
    // Trigger.oldMap: Map<Id, Account>
    
    for(Account newAcc : Trigger.new) {
        Account oldAcc = Trigger.oldMap.get(newAcc.Id);
        
        // Compare old vs new
        if(newAcc.Name != oldAcc.Name) {
            System.debug('Name changed from ' + oldAcc.Name + ' to ' + newAcc.Name);
        }
        
        if(newAcc.Phone != oldAcc.Phone) {
            System.debug('Phone changed from ' + oldAcc.Phone + ' to ' + newAcc.Phone);
        }
    }
}

Availability Matrix:

Context Variablebefore insertbefore updatebefore deleteafter insertafter updateafter deleteafter undelete
Trigger.new✅ (modifiable)✅ (modifiable)
Trigger.old
Trigger.newMap
Trigger.oldMap

Practical Examples:

Example 1: Field Value Comparison

trigger OpportunityTrigger on Opportunity (before update) {
    for(Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
        
        // Check if stage changed to Closed Won
        if(opp.StageName == 'Closed Won' && oldOpp.StageName != 'Closed Won') {
            opp.Closed_Date__c = System.today();
            System.debug('Opportunity closed: ' + opp.Name);
        }
    }
}

Example 2: Prevent Deletion Based on Conditions

trigger AccountTrigger on Account (before delete) {
    for(Account acc : Trigger.old) {
        if(acc.AnnualRevenue > 1000000) {
            acc.addError('Cannot delete high-value accounts');
        }
    }
}

Example 3: Audit Trail

trigger ContactTrigger on Contact (after update) {
    List<Audit_Log__c> logs = new List<Audit_Log__c>();
    
    for(Contact con : Trigger.new) {
        Contact oldCon = Trigger.oldMap.get(con.Id);
        
        if(con.Email != oldCon.Email) {
            logs.add(new Audit_Log__c(
                Record_Id__c = con.Id,
                Field_Name__c = 'Email',
                Old_Value__c = oldCon.Email,
                New_Value__c = con.Email,
                Changed_By__c = UserInfo.getUserId(),
                Changed_Date__c = System.now()
            ));
        }
    }
    
    if(!logs.isEmpty()) {
        insert logs;
    }
}

Q6. What are the different types of Apex triggers?

Answer:

Apex triggers are classified into two main categories based on when they execute in the transaction lifecycle:

1. Before Triggers

Execute before records are saved to the database.

Events:

  • before insert
  • before update
  • before delete

Characteristics:

  • Records are not yet committed to database
  • Can modify Trigger.new records directly (no DML needed)
  • Cannot modify Trigger.new in before delete (records are being deleted)
  • System fields (Id, CreatedDate, etc.) not yet set for new records
  • Best for validation and field updates on the same record

Use Cases:

  • Validate field values
  • Set default values
  • Update fields on the same record
  • Prevent operations (addError)
trigger AccountBeforeTrigger on Account (before insert, before update) {
    for(Account acc : Trigger.new) {
        // Validation
        if(acc.AnnualRevenue != null && acc.AnnualRevenue < 0) {
            acc.addError('Annual Revenue cannot be negative');
        }
        
        // Set default values
        if(acc.Rating == null) {
            acc.Rating = 'Cold';
        }
        
        // Format data
        if(acc.Phone != null) {
            acc.Phone = acc.Phone.replaceAll('[^0-9]', '');
        }
        
        // Update related field
        if(Trigger.isUpdate) {
            Account oldAcc = Trigger.oldMap.get(acc.Id);
            if(acc.Name != oldAcc.Name) {
                acc.Name_Changed_Date__c = System.today();
            }
        }
    }
}

2. After Triggers

Execute after records are saved to the database.

Events:

  • after insert
  • after update
  • after delete
  • after undelete

Characteristics:

  • Records are committed to database
  • Cannot modify Trigger.new records directly
  • System fields (Id, CreatedDate, etc.) are set
  • Use for operations on related records
  • Best for creating/updating other records

Use Cases:

  • Create related records
  • Update parent/child records
  • Send emails or notifications
  • Call external services
  • Create records in other objects
trigger AccountAfterTrigger on Account (after insert, after update, after delete) {
    
    if(Trigger.isInsert) {
        // Create default contact for new accounts
        List<Contact> newContacts = new List<Contact>();
        for(Account acc : Trigger.new) {
            newContacts.add(new Contact(
                AccountId = acc.Id, // Id is available in after trigger
                LastName = 'Default Contact',
                Email = 'default@' + acc.Name.toLowerCase().replaceAll(' ', '') + '.com'
            ));
        }
        if(!newContacts.isEmpty()) {
            insert newContacts;
        }
    }
    
    if(Trigger.isUpdate) {
        // Update related opportunities
        Set<Id> accountIds = new Set<Id>();
        for(Account acc : Trigger.new) {
            Account oldAcc = Trigger.oldMap.get(acc.Id);
            if(acc.Rating != oldAcc.Rating) {
                accountIds.add(acc.Id);
            }
        }
        
        if(!accountIds.isEmpty()) {
            List<Opportunity> oppsToUpdate = [
                SELECT Id, AccountId FROM Opportunity 
                WHERE AccountId IN :accountIds AND IsClosed = false
            ];
            
            Map<Id, Account> accountMap = new Map<Id, Account>(Trigger.new);
            for(Opportunity opp : oppsToUpdate) {
                opp.Description = 'Account rating changed to: ' + 
                                 accountMap.get(opp.AccountId).Rating;
            }
            
            if(!oppsToUpdate.isEmpty()) {
                update oppsToUpdate;
            }
        }
    }
    
    if(Trigger.isDelete) {
        // Create audit log before deletion
        List<Audit_Log__c> logs = new List<Audit_Log__c>();
        for(Account acc : Trigger.old) {
            logs.add(new Audit_Log__c(
                Record_Id__c = acc.Id,
                Record_Name__c = acc.Name,
                Action__c = 'Deleted',
                Deleted_By__c = UserInfo.getUserId()
            ));
        }
        if(!logs.isEmpty()) {
            insert logs;
        }
    }
}

Complete Trigger Example with All Events:

trigger OpportunityCompleteTrigger on Opportunity (
    before insert, before update, before delete,
    after insert, after update, after delete, after undelete
) {
    
    // BEFORE INSERT
    if(Trigger.isBefore && Trigger.isInsert) {
        for(Opportunity opp : Trigger.new) {
            // Set default close date if not provided
            if(opp.CloseDate == null) {
                opp.CloseDate = System.today().addDays(30);
            }
        }
    }
    
    // BEFORE UPDATE
    if(Trigger.isBefore && Trigger.isUpdate) {
        for(Opportunity opp : Trigger.new) {
            Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
            
            // Validate stage progression
            if(opp.StageName == 'Closed Won' && opp.Amount == null) {
                opp.addError('Amount required for Closed Won opportunities');
            }
        }
    }
    
    // BEFORE DELETE
    if(Trigger.isBefore && Trigger.isDelete) {
        for(Opportunity opp : Trigger.old) {
            // Prevent deletion of closed opportunities
            if(opp.IsClosed) {
                opp.addError('Cannot delete closed opportunities');
            }
        }
    }
    
    // AFTER INSERT
    if(Trigger.isAfter && Trigger.isInsert) {
        // Create default opportunity contact role
        List<OpportunityContactRole> roles = new List<OpportunityContactRole>();
        
        Set<Id> accountIds = new Set<Id>();
        for(Opportunity opp : Trigger.new) {
            if(opp.AccountId != null) {
                accountIds.add(opp.AccountId);
            }
        }
        
        Map<Id, Contact> primaryContacts = new Map<Id, Contact>();
        for(Contact con : [SELECT Id, AccountId FROM Contact 
                          WHERE AccountId IN :accountIds AND Primary_Contact__c = true]) {
            primaryContacts.put(con.AccountId, con);
        }
        
        for(Opportunity opp : Trigger.new) {
            Contact primaryContact = primaryContacts.get(opp.AccountId);
            if(primaryContact != null) {
                roles.add(new OpportunityContactRole(
                    OpportunityId = opp.Id,
                    ContactId = primaryContact.Id,
                    Role = 'Decision Maker',
                    IsPrimary = true
                ));
            }
        }
        
        if(!roles.isEmpty()) {
            insert roles;
        }
    }
    
    // AFTER UPDATE
    if(Trigger.isAfter && Trigger.isUpdate) {
        // Update account last modified date
        Set<Id> accountIds = new Set<Id>();
        for(Opportunity opp : Trigger.new) {
            Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
            if(opp.Amount != oldOpp.Amount || opp.StageName != oldOpp.StageName) {
                accountIds.add(opp.AccountId);
            }
        }
        
        if(!accountIds.isEmpty()) {
            List<Account> accounts = [SELECT Id FROM Account WHERE Id IN :accountIds];
            for(Account acc : accounts) {
                acc.Last_Opportunity_Update__c = System.now();
            }
            update accounts;
        }
    }
    
    // AFTER DELETE
    if(Trigger.isAfter && Trigger.isDelete) {
        // Send notification email
        List<Messaging.SingleEmailMessage> emails = new List<Messaging.SingleEmailMessage>();
        for(Opportunity opp : Trigger.old) {
            Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
            email.setToAddresses(new String[]{UserInfo.getUserEmail()});
            email.setSubject('Opportunity Deleted: ' + opp.Name);
            email.setPlainTextBody('An opportunity has been deleted: ' + opp.Name);
            emails.add(email);
        }
        if(!emails.isEmpty()) {
            Messaging.sendEmail(emails);
        }
    }
    
    // AFTER UNDELETE
    if(Trigger.isAfter && Trigger.isUndelete) {
        // Log restoration
        List<Audit_Log__c> logs = new List<Audit_Log__c>();
        for(Opportunity opp : Trigger.new) {
            logs.add(new Audit_Log__c(
                Record_Id__c = opp.Id,
                Action__c = 'Restored',
                Restored_By__c = UserInfo.getUserId()
            ));
        }
        if(!logs.isEmpty()) {
            insert logs;
        }
    }
}

Best Practices:

  1. One Trigger Per Object: Consolidate all logic in a single trigger
  2. Use Handler Classes: Keep trigger logic-less by delegating to handler classes
  3. Before for Same Record: Use before triggers to update the same record
  4. After for Related Records: Use after triggers to update related records
  5. Bulkify Always: Process collections, not individual records

Q7. What is a wrapper class in Apex?

Answer:

A wrapper class is a custom object that encapsulates (wraps) one or more standard or custom objects, primitive types, or other wrapper classes. It’s a design pattern used to combine different data types into a single unit.

Common Use Cases:

  1. Display data in Visualforce with checkboxes
  2. Combine data from multiple objects
  3. Pass complex data structures
  4. Group related information
  5. Return multiple data types from a method

Basic Wrapper Class Example:

public class AccountWrapper {
    public Account acc { get; set; }
    public Boolean isSelected { get; set; }
    public Integer numberOfContacts { get; set; }
    
    // Constructor
    public AccountWrapper(Account a) {
        this.acc = a;
        this.isSelected = false;
        this.numberOfContacts = 0;
    }
    
    // Constructor with all parameters
    public AccountWrapper(Account a, Boolean selected, Integer contactCount) {
        this.acc = a;
        this.isSelected = selected;
        this.numberOfContacts = contactCount;
    }
}

Advanced Wrapper Class Example (Visualforce + Controller):

Apex Controller:

public class AccountWrapperController {
    public List<AccountWrapper> accountWrappers { get; set; }
    
    public AccountWrapperController() {
        accountWrappers = new List<AccountWrapper>();
        
        // Query accounts with contact count
        for(Account acc : [SELECT Id, Name, Industry, AnnualRevenue,
                          (SELECT Id FROM Contacts) 
                          FROM Account LIMIT 50]) {
            AccountWrapper wrapper = new AccountWrapper();
            wrapper.acc = acc;
            wrapper.isSelected = false;
            wrapper.contactCount = acc.Contacts.size();
            wrapper.revenue = acc.AnnualRevenue != null ? 
                             '$' + String.valueOf(acc.AnnualRevenue) : 'N/A';
            accountWrappers.add(wrapper);
        }
    }
    
    public void processSelected() {
        List<Account> selectedAccounts = new List<Account>();
        
        for(AccountWrapper wrapper : accountWrappers) {
            if(wrapper.isSelected) {
                selectedAccounts.add(wrapper.acc);
            }
        }
        
        // Process selected accounts
        for(Account acc : selectedAccounts) {
            acc.Rating = 'Hot';
        }
        
        if(!selectedAccounts.isEmpty()) {
            update selectedAccounts;
            ApexPages.addMessage(new ApexPages.Message(
                ApexPages.Severity.INFO, 
                selectedAccounts.size() + ' accounts updated'
            ));
        }
    }
    
    // Inner wrapper class
    public class AccountWrapper {
        public Account acc { get; set; }
        public Boolean isSelected { get; set; }
        public Integer contactCount { get; set; }
        public String revenue { get; set; }
        
        public AccountWrapper() {
            isSelected = false;
            contactCount = 0;
        }
    }
}

Visualforce Page:

<apex:page controller="AccountWrapperController">
    <apex:form>
        <apex:pageBlock title="Account Selection">
            <apex:pageBlockButtons>
                <apex:commandButton value="Process Selected" action="{!processSelected}"/>
            </apex:pageBlockButtons>
            
            <apex:pageBlockTable value="{!accountWrappers}" var="wrapper">
                <apex:column>
                    <apex:inputCheckbox value="{!wrapper.isSelected}"/>
                </apex:column>
                <apex:column headerValue="Account Name">
                    <apex:outputField value="{!wrapper.acc.Name}"/>
                </apex:column>
                <apex:column headerValue="Industry">
                    <apex:outputField value="{!wrapper.acc.Industry}"/>
                </apex:column>
                <apex:column headerValue="Revenue">
                    <apex:outputText value="{!wrapper.revenue}"/>
                </apex:column>
                <apex:column headerValue="# of Contacts">
                    <apex:outputText value="{!wrapper.contactCount}"/>
                </apex:column>
            </apex:pageBlockTable>
        </apex:pageBlock>
    </apex:form>
</apex:page>

Wrapper Class for API Response:

public class APIResponseWrapper {
    public Boolean success { get; set; }
    public String message { get; set; }
    public List<DataRecord> records { get; set; }
    public ErrorDetails error { get; set; }
    
    public APIResponseWrapper() {
        this.success = false;
        this.records = new List<DataRecord>();
    }
    
    public class DataRecord {
        public String id { get; set; }
        public String name { get; set; }
        public String email { get; set; }
        public Decimal amount { get; set; }
    }
    
    public class ErrorDetails {
        public String errorCode { get; set; }
        public String errorMessage { get; set; }
        public Integer statusCode { get; set; }
    }
}

// Usage in REST API
@RestResource(urlMapping='/api/data/*')
global class DataAPI {
    @HttpGet
    global static APIResponseWrapper getData() {
        APIResponseWrapper response = new APIResponseWrapper();
        
        try {
            List<Account> accounts = [SELECT Id, Name, Phone, AnnualRevenue 
                                     FROM Account LIMIT 10];
            
            for(Account acc : accounts) {
                APIResponseWrapper.DataRecord record = new APIResponseWrapper.DataRecord();
                record.id = acc.Id;
                record.name = acc.Name;
                record.email = acc.Phone;
                record.amount = acc.AnnualRevenue;
                response.records.add(record);
            }
            
            response.success = true;
            response.message = 'Data retrieved successfully';
            
        } catch(Exception e) {
            response.success = false;
            response.error = new APIResponseWrapper.ErrorDetails();
            response.error.errorCode = 'QUERY_ERROR';
            response.error.errorMessage = e.getMessage();
            response.error.statusCode = 500;
        }
        
        return response;
    }
}

Complex Wrapper for Multi-Object Data:

public class OrderSummaryWrapper {
    public Account account { get; set; }
    public Contact primaryContact { get; set; }
    public List<Opportunity> opportunities { get; set; }
    public Decimal totalRevenue { get; set; }
    public Integer totalOrders { get; set; }
    public String status { get; set; }
    
    public OrderSummaryWrapper(Id accountId) {
        // Query account
        this.account = [SELECT Id, Name, Industry FROM Account 
                       WHERE Id = :accountId];
        
        // Query primary contact
        List<Contact> contacts = [SELECT Id, Name, Email FROM Contact 
                                 WHERE AccountId = :accountId AND Primary_Contact__c = true 
                                 LIMIT 1];
        this.primaryContact = contacts.isEmpty() ? null : contacts[0];
        
        // Query opportunities
        this.opportunities = [SELECT Id, Name, Amount, StageName 
                             FROM Opportunity 
                             WHERE AccountId = :accountId];
        
        // Calculate metrics
        this.totalRevenue = 0;
        this.totalOrders = this.opportunities.size();
        for(Opportunity opp : this.opportunities) {
            if(opp.Amount != null) {
                this.totalRevenue += opp.Amount;
            }
        }
        
        // Determine status
        if(this.totalRevenue > 1000000) {
            this.status = 'Premium';
        } else if(this.totalRevenue > 500000) {
            this.status = 'Gold';
        } else {
            this.status = 'Standard';
        }
    }
}

Benefits of Wrapper Classes:

  1. Flexibility: Combine any data types
  2. Reusability: Can be used across multiple methods
  3. Type Safety: Strongly typed custom structures
  4. Serialization: Can be easily serialized to JSON/XML
  5. Maintainability: Clear data structure definition

Q8. What is the order of execution in Salesforce?

Answer:

When a record is saved in Salesforce, the platform executes a specific sequence of operations. Understanding this order is crucial for debugging and building efficient solutions.

Complete Order of Execution:

  1. Loads the original record from database (or initializes for insert)
  2. Loads new field values from UI/API request
  3. System Validation Rules run
    • Field data types
    • Required fields at layout level
    • Max length
  4. Before Triggers execute
    • before insert
    • before update
    • before delete
  5. Custom Validation Rules execute
    • Field validation rules
    • Formula field calculations
  6. Duplicate Rules execute (if applicable)
  7. Record is saved to database (but not committed yet)
    • Record values updated in database
    • System fields (SystemModstamp, LastModifiedDate, etc.) updated
    • Record Id assigned (for new records)
  8. After Triggers execute
    • after insert
    • after update
    • after delete
    • after undelete
  9. Assignment Rules execute (if applicable)
    • Lead assignment
    • Case assignment
  10. Auto-Response Rules execute (if applicable)
  11. Workflow Rules execute (if applicable)
    • Field updates execute
    • Generates a new trigger cycle if fields updated
  12. Process Builder / Flows execute (if applicable)
  13. Entitlement Rules execute (if applicable)
  14. Escalation Rules execute (if applicable)
  15. Parent rollup summary fields calculate (if applicable)
  16. Criteria-based sharing evaluation (if applicable)
  17. Database commit executes
    • All DML operations committed
    • Record locks released
  18. Post-commit logic executes
    • Email notifications sent
    • Asynchronous Apex jobs queued (@future, Queueable)

Practical Example Demonstrating Order:

// Account Trigger
trigger AccountTrigger on Account (before insert, before update, 
                                   after insert, after update) {
    System.debug('=== TRIGGER START ===');
    System.debug('Is Before: ' + Trigger.isBefore);
    System.debug('Is After: ' + Trigger.isAfter);
    
    if(Trigger.isBefore && Trigger.isInsert) {
        System.debug('STEP 4: Before Insert Trigger');
        for(Account acc : Trigger.new) {
            // This change doesn't require DML
            acc.Description = 'Modified in before trigger';
        }
    }
    
    if(Trigger.isAfter && Trigger.isInsert) {
        System.debug('STEP 8: After Insert Trigger');
        System.debug('Record Id now available: ' + Trigger.new[0].Id);
        
        // Create related contact
        List<Contact> contacts = new List<Contact>();
        for(Account acc : Trigger.new) {
            contacts.add(new Contact(
                AccountId = acc.Id,
                LastName = 'Default Contact'
            ));
        }
        insert contacts;
    }
    
    System.debug('=== TRIGGER END ===');
}

Re-Trigger Scenarios:

Certain operations cause records to go through the save process multiple times:

Scenario 1: Workflow Field Update

// Initial save
1. Before triggers execute
2. After triggers execute
3. Workflow field update occurs
4. TRIGGERS FIRE AGAIN (before/after update) with new values
5. Commit

Scenario 2: Process Builder Update

// Initial save
1. Before triggers execute
2. After triggers execute
3. Process Builder update occurs
4. TRIGGERS FIRE AGAIN
5. Commit

Prevent Recursion Example:

public class TriggerHelper {
    private static Boolean isFirstTime = true;
    
    public static Boolean isFirstTime() {
        if(isFirstTime) {
            isFirstTime = false;
            return true;
        }
        return false;
    }
}

trigger AccountTrigger on Account (after update) {
    if(TriggerHelper.isFirstTime()) {
        // Execute logic only once
        System.debug('Executing once');
    }
}

Order with Multiple Objects:

// When you update Account, which triggers Contact update
trigger AccountTrigger on Account (after update) {
    List<Contact> contacts = [SELECT Id, AccountId FROM Contact 
                              WHERE AccountId IN :Trigger.new];
    
    for(Contact con : contacts) {
        con.Description = 'Account updated';
    }
    update contacts; // This starts a NEW order of execution for Contact
}

// Order:
// 1. Account: before update -> after update
// 2. Contact: before update -> after update (separate execution)
// 3. Account commit
// 4. Contact commit

Debug Log Example:

Account acc = new Account(Name = 'Test Account');
System.debug('STEP 1-2: Record initialized with values');

insert acc;
// Execution continues internally:
// STEP 3: System validation
// STEP 4: Before triggers
// STEP 5: Custom validation
// STEP 6: Duplicate rules
// STEP 7: Save to database
// STEP 8: After triggers
// ...continues through all steps
// STEP 17: Commit

System.debug('STEP 18+: Post-commit (record Id: ' + acc.Id + ')');

Key Insights:

  1. Before Triggers: Use for same-record updates (no DML needed)
  2. After Triggers: Use for related record operations
  3. Validation: Happens before triggers
  4. Record Id: Available only after save (step 7)
  5. Recursion: Be careful with field updates causing re-triggers
  6. Commit: Everything must succeed or everything rolls back
  7. Async Operations: Execute after commit completes

Q9. What is a static variable and static method in Apex?

Answer:

Static variables and methods in Apex belong to the class itself rather than to instances of the class. They are shared across all instances and retain their values throughout the execution context.

Static Variables

Definition: Variables that are shared across all instances of a class and persist for the duration of the transaction.

Characteristics:

  • Initialized only once when class is first loaded
  • Shared by all instances of the class
  • Retain values throughout the transaction
  • Reset when transaction completes
  • Cannot be accessed via object instances (only via class name)
public class StaticVariableExample {
    // Static variable
    public static Integer callCount = 0;
    
    // Instance variable
    public Integer instanceCount = 0;
    
    public void incrementCounters() {
        callCount++;        // Shared across all instances
        instanceCount++;    // Specific to this instance
    }
    
    public void displayCounts() {
        System.debug('Static Count: ' + callCount);
        System.debug('Instance Count: ' + instanceCount);
    }
}

// Usage
StaticVariableExample obj1 = new StaticVariableExample();
obj1.incrementCounters();
obj1.displayCounts(); // Static: 1, Instance: 1

StaticVariableExample obj2 = new StaticVariableExample();
obj2.incrementCounters();
obj2.displayCounts(); // Static: 2, Instance: 1 (new instance)

obj1.displayCounts(); // Static: 2, Instance: 1 (shared static)

Common Use Cases for Static Variables:

1. Trigger Recursion Prevention:

public class TriggerRecursionControl {
    public static Boolean isFirstTime = true;
    public static Set<Id> processedIds = new Set<Id>();
    
    public static Boolean isFirstRun() {
        if(isFirstTime) {
            isFirstTime = false;
            return true;
        }
        return false;
    }
    
    public static Boolean alreadyProcessed(Id recordId) {
        if(processedIds.contains(recordId)) {
            return true;
        }
        processedIds.add(recordId);
        return false;
    }
}

// Usage in trigger
trigger AccountTrigger on Account (after update) {
    if(TriggerRecursionControl.isFirstRun()) {
        // Process logic only once
        for(Account acc : Trigger.new) {
            if(!TriggerRecursionControl.alreadyProcessed(acc.Id)) {
                // Process account
            }
        }
    }
}

2. Caching Query Results:

public class AccountCache {
    private static Map<Id, Account> accountCache;
    
    public static Account getAccount(Id accountId) {
        // Initialize cache if needed
        if(accountCache == null) {
            accountCache = new Map<Id, Account>();
        }
        
        // Check cache first
        if(accountCache.containsKey(accountId)) {
            System.debug('Returning from cache');
            return accountCache.get(accountId);
        }
        
        // Query if not in cache
        Account acc = [SELECT Id, Name, Industry FROM Account 
                      WHERE Id = :accountId LIMIT 1];
        accountCache.put(accountId, acc);
        return acc;
    }
    
    public static void clearCache() {
        accountCache = null;
    }
}

3. Configuration Settings:

public class AppConfig {
    public static final String API_ENDPOINT = 'https://api.example.com';
    public static final Integer MAX_RETRIES = 3;
    public static final Integer TIMEOUT_SECONDS = 120;
    
    private static Boolean debugMode;
    
    public static Boolean isDebugMode() {
        if(debugMode == null) {
            // Load from custom setting
            App_Settings__c settings = App_Settings__c.getInstance();
            debugMode = settings.Debug_Mode__c;
        }
        return debugMode;
    }
}

Static Methods

Definition: Methods that belong to the class and can be called without creating an instance.

Characteristics:

  • Called using class name, not instance
  • Cannot access instance variables or methods
  • Can only access static variables and methods
  • Cannot use this keyword
  • Commonly used for utility methods
public class MathUtility {
    // Static method
    public static Integer add(Integer a, Integer b) {
        return a + b;
    }
    
    public static Decimal calculateTax(Decimal amount, Decimal taxRate) {
        return amount * (taxRate / 100);
    }
    
    public static Boolean isPrime(Integer num) {
        if(num <= 1) return false;
        if(num <= 3) return true;
        
        for(Integer i = 2; i <= Math.sqrt(num); i++) {
            if(Math.mod(num, i) == 0) {
                return false;
            }
        }
        return true;
    }
}

// Usage - no instance needed
Integer sum = MathUtility.add(5, 10);
Decimal tax = MathUtility.calculateTax(100, 7.5);
Boolean prime = MathUtility.isPrime(17);

Common Patterns with Static Methods:

1. Utility/Helper Classes:

public class StringUtils {
    public static String capitalize(String input) {
        if(String.isBlank(input)) return input;
        return input.substring(0, 1).toUpperCase() + 
               input.substring(1).toLowerCase();
    }
    
    public static String formatPhone(String phone) {
        if(String.isBlank(phone)) return phone;
        String cleaned = phone.replaceAll('[^0-9]', '');
        if(cleaned.length() == 10) {
            return '(' + cleaned.substring(0,3) + ') ' + 
                   cleaned.substring(3,6) + '-' + 
                   cleaned.substring(6);
        }
        return phone;
    }
    
    public static Boolean isValidEmail(String email) {
        String regex = '^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$';
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(email);
        return matcher.matches();
    }
}

2. Factory Pattern:

public class AccountFactory {
    public static Account createAccount(String name, String industry) {
        Account acc = new Account();
        acc.Name = name;
        acc.Industry = industry;
        acc.Rating = 'Cold';
        acc.CreatedFromFactory__c = true;
        return acc;
    }
    
    public static List<Account> createBulkAccounts(Integer count, String prefix) {
        List<Account> accounts = new List<Account>();
        for(Integer i = 0; i < count; i++) {
            accounts.add(createAccount(prefix + ' ' + i, 'Technology'));
        }
        return accounts;
    }
}

// Usage
Account acc = AccountFactory.createAccount('Test Corp', 'Technology');
List<Account> bulkAccounts = AccountFactory.createBulkAccounts(100, 'Test Account');

3. Trigger Handler:

public class AccountTriggerHandler {
    public static void handleBeforeInsert(List<Account> newAccounts) {
        for(Account acc : newAccounts) {
            if(acc.Rating == null) {
                acc.Rating = 'Cold';
            }
        }
    }
    
    public static void handleAfterInsert(List<Account> newAccounts) {
        List<Contact> contacts = new List<Contact>();
        for(Account acc : newAccounts) {
            contacts.add(new Contact(
                AccountId = acc.Id,
                LastName = 'Default Contact'
            ));
        }
        if(!contacts.isEmpty()) {
            insert contacts;
        }
    }
}

// Usage in trigger
trigger AccountTrigger on Account (before insert, after insert) {
    if(Trigger.isBefore && Trigger.isInsert) {
        AccountTriggerHandler.handleBeforeInsert(Trigger.new);
    }
    if(Trigger.isAfter && Trigger.isInsert) {
        AccountTriggerHandler.handleAfterInsert(Trigger.new);
    }
}

Static vs Instance Comparison:

public class ComparisonExample {
    // Static variable - shared
    public static Integer staticCounter = 0;
    
    // Instance variable - per object
    public Integer instanceCounter = 0;
    
    // Static method - no instance needed
    public static void incrementStatic() {
        staticCounter++;
        // instanceCounter++; // ERROR: Cannot access instance variable
    }
    
    // Instance method - requires object
    public void incrementInstance() {
        instanceCounter++;
        staticCounter++; // CAN access static variable
    }
}

// Usage
ComparisonExample.incrementStatic(); // Static method called on class
System.debug(ComparisonExample.staticCounter); // 1

ComparisonExample obj = new ComparisonExample();
obj.incrementInstance(); // Instance method called on object
System.debug(obj.instanceCounter); // 1
System.debug(ComparisonExample.staticCounter); // 2

Important Notes:

  1. Static variables reset after transaction completes
  2. Static methods cannot be overridden (but can be hidden in subclasses)
  3. Test classes should not rely on static variables between test methods
  4. Governor limits are per transaction, not per static variable
  5. Static blocks execute when class is first loaded

Q10. What is the difference between SOQL and SOSL?

Answer:

SOQL (Salesforce Object Query Language) and SOSL (Salesforce Object Search Language) are both query languages in Salesforce, but they serve different purposes and have distinct characteristics.

Comparison Table:

FeatureSOQLSOSL
PurposeQuery records from one objectSearch across multiple objects
Return TypeList of sObjectsList of List of sObjects
SyntaxSimilar to SQL SELECTSimilar to full-text search
Object ScopeSingle object at a timeMultiple objects simultaneously
Relationship QueriesSupports child/parent relationshipsLimited relationship support
Trigger UsageAllowedNot allowed
Search ScopeAll fields or specified fieldsName, Email, Phone, Text fields
PerformanceFast for specific queriesOptimized for text search
WildcardsLimited (LIKE with %)Extensive (* and ?)
Record Limit50,000 records2,000 records
Query Limit100 (sync), 200 (async)20 per transaction

SOQL Examples:

Basic SOQL Query:

// Query single object
List<Account> accounts = [SELECT Id, Name, Industry FROM Account 
                          WHERE Industry = 'Technology' 
                          LIMIT 10];

for(Account acc : accounts) {
    System.debug(acc.Name);
}

SOQL with Relationships:

// Parent to Child relationship
List<Account> accounts = [SELECT Id, Name, 
                          (SELECT Id, FirstName, LastName FROM Contacts)
                          FROM Account 
                          WHERE Industry = 'Technology'];

for(Account acc : accounts) {
    System.debug('Account: ' + acc.Name);
    for(Contact con : acc.Contacts) {
        System.debug('  Contact: ' + con.FirstName + ' ' + con.LastName);
    }
}

// Child to Parent relationship
List<Contact> contacts = [SELECT Id, FirstName, LastName, 
                          Account.Name, Account.Industry
                          FROM Contact 
                          WHERE Account.Industry = 'Technology'];

for(Contact con : contacts) {
    System.debug(con.FirstName + ' works at ' + con.Account.Name);
}

SOQL with Advanced Filtering:

// Multiple conditions
List<Opportunity> opps = [SELECT Id, Name, Amount, StageName 
                          FROM Opportunity 
                          WHERE Amount > 100000 
                          AND StageName = 'Prospecting'
                          AND CloseDate = THIS_YEAR
                          ORDER BY Amount DESC];

// Date literals
List<Account> recentAccounts = [SELECT Id, Name 
                                FROM Account 
                                WHERE CreatedDate = LAST_N_DAYS:30];

// IN clause
Set<String> industries = new Set<String>{'Technology', 'Healthcare', 'Finance'};
List<Account> accounts = [SELECT Id, Name 
                          FROM Account 
                          WHERE Industry IN :industries];

// LIKE operator
List<Account> accounts = [SELECT Id, Name 
                          FROM Account 
                          WHERE Name LIKE 'Acme%'];

Aggregate SOQL:

// COUNT, SUM, AVG, MIN, MAX
AggregateResult[] results = [SELECT Industry, COUNT(Id) total, AVG(AnnualRevenue) avgRevenue
                             FROM Account 
                             GROUP BY Industry
                             HAVING COUNT(Id) > 10];

for(AggregateResult ar : results) {
    String industry = (String)ar.get('Industry');
    Integer count = (Integer)ar.get('total');
    Decimal average = (Decimal)ar.get('avgRevenue');
    System.debug(industry + ': ' + count + ' accounts, Avg Revenue: ' + average);
}

SOSL Examples:

Basic SOSL Search:

// Search across multiple objects
List<List<SObject>> searchResults = [FIND 'John' 
                                     IN ALL FIELDS 
                                     RETURNING Account(Id, Name), 
                                               Contact(Id, Name, Email)];

List<Account> accounts = (List<Account>)searchResults[0];
List<Contact> contacts = (List<Contact>)searchResults[1];

System.debug('Found ' + accounts.size() + ' accounts');
System.debug('Found ' + contacts.size() + ' contacts');

SOSL with Specific Fields:

// Search in specific field groups
List<List<SObject>> searchResults = [FIND '[email protected]' 
                                     IN EMAIL FIELDS 
                                     RETURNING Contact(Id, Name, Email),
                                               Lead(Id, Name, Email)];

// Search in NAME FIELDS
List<List<SObject>> nameResults = [FIND 'Acme' 
                                   IN NAME FIELDS 
                                   RETURNING Account(Id, Name),
                                             Opportunity(Id, Name)];

// Search in PHONE FIELDS
List<List<SObject>> phoneResults = [FIND '415*' 
                                    IN PHONE FIELDS 
                                    RETURNING Contact(Id, Name, Phone)];

SOSL with Filters and Ordering:

// With WHERE clause and ORDER BY
List<List<SObject>> searchResults = [FIND 'test' 
                                     IN ALL FIELDS 
                                     RETURNING Account(Id, Name, Industry 
                                                      WHERE Industry = 'Technology'
                                                      ORDER BY Name 
                                                      LIMIT 10),
                                               Contact(Id, Name, Email 
                                                      WHERE Email != null
                                                      ORDER BY LastName)];

SOSL with Wildcards:

// * = zero or more characters
// ? = exactly one character

List<List<SObject>> results1 = [FIND 'john*' IN ALL FIELDS 
                                RETURNING Contact(Name, Email)];
// Matches: john, johnson, johnny

List<List<SObject>> results2 = [FIND 'jo?n' IN ALL FIELDS 
                                RETURNING Contact(Name, Email)];
// Matches: john, joan (but not johnson)

List<List<SObject>> results3 = [FIND 'test*@*.com' IN EMAIL FIELDS 
                                RETURNING Contact(Name, Email)];
// Matches: [email protected], [email protected]

When to Use SOQL vs SOSL:

Use SOQL When:

  1. Querying a single object
  2. Need to retrieve a specific set of records
  3. Working with relationships (parent-child)
  4. Need to query all fields
  5. Inside triggers
  6. Need precise matching
  7. Working with large datasets (up to 50,000 records)
// Example: Get all opportunities for specific accounts
Set<Id> accountIds = new Set<Id>{'001...', '001...'};
List<Opportunity> opps = [SELECT Id, Name, Amount 
                          FROM Opportunity 
                          WHERE AccountId IN :accountIds];

Use SOSL When:

  1. Searching across multiple objects
  2. Don’t know which object contains the data
  3. Performing text-based searches
  4. Searching name, email, or phone fields
  5. Need fuzzy matching or wildcards
  6. User input search (search bars)
  7. Working with smaller result sets (up to 2,000 records)
// Example: Global search for a person's information
String searchTerm = 'John Smith';
List<List<SObject>> results = [FIND :searchTerm 
                               IN ALL FIELDS 
                               RETURNING Account(Name),
                                         Contact(Name, Email),
                                         Lead(Name, Email),
                                         Opportunity(Name)];

// Process all results
if(!results[0].isEmpty()) System.debug('Found in Accounts');
if(!results[1].isEmpty()) System.debug('Found in Contacts');
if(!results[2].isEmpty()) System.debug('Found in Leads');
if(!results[3].isEmpty()) System.debug('Found in Opportunities');

Performance Considerations:

SOQL Performance Tips:

// Good: Selective query
List<Account> accounts = [SELECT Id, Name 
                          FROM Account 
                          WHERE Industry = 'Technology' 
                          AND CreatedDate = LAST_N_DAYS:30];

// Bad: Non-selective query
List<Account> allAccounts = [SELECT Id, Name FROM Account]; // Slow for large datasets

// Good: Use indexed fields (Id, Name, Owner, CreatedDate, etc.)
List<Contact> contacts = [SELECT Id, Name 
                          FROM Contact 
                          WHERE CreatedDate > :Date.today().addDays(-7)];

SOSL Performance Tips:

// Good: Specific field groups
List<List<SObject>> results = [FIND '[email protected]' 
                               IN EMAIL FIELDS 
                               RETURNING Contact(Name, Email)];

// Better: Limit returned objects
List<List<SObject>> results = [FIND 'test' 
                               IN ALL FIELDS 
                               RETURNING Contact(Name LIMIT 10)];

Practical Example Combining Both:

public class SearchUtility {
    // Use SOSL for initial broad search
    public static Map<String, List<SObject>> globalSearch(String searchTerm) {
        Map<String, List<SObject>> resultMap = new Map<String, List<SObject>>();
        
        List<List<SObject>> searchResults = [FIND :searchTerm 
                                             IN ALL FIELDS 
                                             RETURNING Account(Id, Name),
                                                       Contact(Id, Name, Email)];
        
        resultMap.put('Accounts', searchResults[0]);
        resultMap.put('Contacts', searchResults[1]);
        
        return resultMap;
    }
    
    // Use SOQL for detailed follow-up query
    public static List<Account> getAccountDetails(List<Id> accountIds) {
        return [SELECT Id, Name, Industry, AnnualRevenue, 
                (SELECT Id, FirstName, LastName, Email FROM Contacts),
                (SELECT Id, Name, StageName, Amount FROM Opportunities)
                FROM Account 
                WHERE Id IN :accountIds];
    }
}

// Usage
Map<String, List<SObject>> searchResults = SearchUtility.globalSearch('Acme');
List<Account> accounts = (List<Account>)searchResults.get('Accounts');

if(!accounts.isEmpty()) {
    List<Id> accountIds = new List<Id>();
    for(Account acc : accounts) {
        accountIds.add(acc.Id);
    }
    
    // Get full details with relationships
    List<Account> detailedAccounts = SearchUtility.getAccountDetails(accountIds);
}

Section 2: Intermediate Apex Questions

Q11. What is an Apex trigger handler pattern?

Answer:

The trigger handler pattern is a best practice design pattern that separates trigger logic from the trigger itself. It promotes code organization, reusability, maintainability, and testability by keeping triggers thin and moving business logic to dedicated handler classes.

Why Use Trigger Handler Pattern?

Problems with Logic in Triggers:

  1. No reusability: Logic cannot be called from other contexts
  2. Difficult testing: Hard to test specific scenarios
  3. Poor organization: Multiple operations mixed together
  4. Hard to maintain: Logic scattered across triggers
  5. Governor limit issues: Difficult to optimize
  6. Multiple developers: Conflicts when editing same trigger

Basic Trigger Handler Pattern:

Trigger (Thin):

trigger AccountTrigger on Account (before insert, before update, 
                                   after insert, after update, after delete) {
    AccountTriggerHandler handler = new AccountTriggerHandler();
    handler.run();
}

Handler Class:

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);
            } else if(Trigger.isDelete) {
                afterDelete(Trigger.old);
            }
        }
    }
    
    private void beforeInsert(List<Account> newAccounts) {
        for(Account acc : newAccounts) {
            if(acc.Rating == null) {
                acc.Rating = 'Cold';
            }
        }
    }
    
    private void beforeUpdate(List<Account> newAccounts, Map<Id, Account> oldAccountMap) {
        for(Account acc : newAccounts) {
            Account oldAcc = oldAccountMap.get(acc.Id);
            if(acc.Name != oldAcc.Name) {
                acc.Name_Last_Changed__c = System.now();
            }
        }
    }
    
    private void afterInsert(List<Account> newAccounts) {
        createDefaultContacts(newAccounts);
    }
    
    private void afterUpdate(List<Account> newAccounts, Map<Id, Account> oldAccountMap) {
        updateRelatedOpportunities(newAccounts, oldAccountMap);
    }
    
    private void afterDelete(List<Account> oldAccounts) {
        logDeletions(oldAccounts);
    }
    
    // Helper methods
    private void createDefaultContacts(List<Account> accounts) {
        List<Contact> contacts = new List<Contact>();
        for(Account acc : accounts) {
            contacts.add(new Contact(
                AccountId = acc.Id,
                LastName = 'Default Contact',
                Email = 'contact@' + acc.Name + '.com'
            ));
        }
        if(!contacts.isEmpty()) {
            insert contacts;
        }
    }
    
    private void updateRelatedOpportunities(List<Account> newAccounts, 
                                           Map<Id, Account> oldAccountMap) {
        Set<Id> changedAccountIds = new Set<Id>();
        
        for(Account acc : newAccounts) {
            Account oldAcc = oldAccountMap.get(acc.Id);
            if(acc.Industry != oldAcc.Industry) {
                changedAccountIds.add(acc.Id);
            }
        }
        
        if(!changedAccountIds.isEmpty()) {
            List<Opportunity> opps = [SELECT Id, AccountId, Description 
                                     FROM Opportunity 
                                     WHERE AccountId IN :changedAccountIds 
                                     AND IsClosed = false];
            
            Map<Id, Account> accountMap = new Map<Id, Account>(newAccounts);
            for(Opportunity opp : opps) {
                opp.Description = 'Account industry changed to: ' + 
                                 accountMap.get(opp.AccountId).Industry;
            }
            
            if(!opps.isEmpty()) {
                update opps;
            }
        }
    }
    
    private void logDeletions(List<Account> accounts) {
        List<Audit_Log__c> logs = new List<Audit_Log__c>();
        for(Account acc : accounts) {
            logs.add(new Audit_Log__c(
                Record_Id__c = acc.Id,
                Record_Name__c = acc.Name,
                Action__c = 'Deleted',
                Deleted_By__c = UserInfo.getUserId(),
                Deleted_Date__c = System.now()
            ));
        }
        if(!logs.isEmpty()) {
            insert logs;
        }
    }
}

Advanced Trigger Handler Framework:

Base Handler Class:

public virtual class TriggerHandler {
    // Static variable to prevent recursion
    private static Set<String> bypassedHandlers = new Set<String>();
    
    // Track execution count
    private static Map<String, Integer> executionCounts = new Map<String, Integer>();
    private Integer maxLoopCount = 5;
    
    public TriggerHandler() {
        String handlerName = String.valueOf(this).substring(0, String.valueOf(this).indexOf(':'));
        if(!executionCounts.containsKey(handlerName)) {
            executionCounts.put(handlerName, 0);
        }
    }
    
    // Main entry point
    public void run() {
        if(isBypassed()) {
            return;
        }
        
        if(!validateRun()) {
            return;
        }
        
        addToLoopCount();
        
        // Before triggers
        if(Trigger.isBefore) {
            if(Trigger.isInsert) {
                beforeInsert();
            } else if(Trigger.isUpdate) {
                beforeUpdate();
            } else if(Trigger.isDelete) {
                beforeDelete();
            }
        }
        
        // After triggers
        if(Trigger.isAfter) {
            if(Trigger.isInsert) {
                afterInsert();
            } else if(Trigger.isUpdate) {
                afterUpdate();
            } else if(Trigger.isDelete) {
                afterDelete();
            } else if(Trigger.isUndelete) {
                afterUndelete();
            }
        }
    }
    
    // Virtual methods to be overridden
    protected virtual void beforeInsert() {}
    protected virtual void beforeUpdate() {}
    protected virtual void beforeDelete() {}
    protected virtual void afterInsert() {}
    protected virtual void afterUpdate() {}
    protected virtual void afterDelete() {}
    protected virtual void afterUndelete() {}
    
    // Bypass mechanism
    public static void bypass(String handlerName) {
        bypassedHandlers.add(handlerName);
    }
    
    public static void clearBypass(String handlerName) {
        bypassedHandlers.remove(handlerName);
    }
    
    public static void clearAllBypasses() {
        bypassedHandlers.clear();
    }
    
    private Boolean isBypassed() {
        String handlerName = String.valueOf(this).substring(0, String.valueOf(this).indexOf(':'));
        return bypassedHandlers.contains(handlerName);
    }
    
    // Loop prevention
    private void addToLoopCount() {
        String handlerName = String.valueOf(this).substring(0, String.valueOf(this).indexOf(':'));
        Integer count = executionCounts.get(handlerName) + 1;
        executionCounts.put(handlerName, count);
    }
    
    private Boolean validateRun() {
        String handlerName = String.valueOf(this).substring(0, String.valueOf(this).indexOf(':'));
        if(executionCounts.get(handlerName) >= maxLoopCount) {
            throw new TriggerHandlerException('Maximum loop count reached: ' + handlerName);
        }
        return true;
    }
    
    public class TriggerHandlerException extends Exception {}
}

Specific Handler Extending Base:

public class AccountTriggerHandler extends TriggerHandler {
    
    private List<Account> newAccounts;
    private List<Account> oldAccounts;
    private Map<Id, Account> newAccountMap;
    private Map<Id, Account> oldAccountMap;
    
    public AccountTriggerHandler() {
        this.newAccounts = (List<Account>)Trigger.new;
        this.oldAccounts = (List<Account>)Trigger.old;
        this.newAccountMap = (Map<Id, Account>)Trigger.newMap;
        this.oldAccountMap = (Map<Id, Account>)Trigger.oldMap;
    }
    
    protected override void beforeInsert() {
        validateAccounts();
        setDefaultValues();
    }
    
    protected override void beforeUpdate() {
        validateAccounts();
        trackFieldChanges();
    }
    
    protected override void afterInsert() {
        createDefaultContacts();
        sendNotifications();
    }
    
    protected override void afterUpdate() {
        updateRelatedRecords();
    }
    
    protected override void afterDelete() {
        createAuditLogs();
    }
    
    // Business logic methods
    private void validateAccounts() {
        for(Account acc : newAccounts) {
            if(String.isBlank(acc.Name)) {
                acc.addError('Account Name is required');
            }
            if(acc.AnnualRevenue != null && acc.AnnualRevenue < 0) {
                acc.addError('Annual Revenue cannot be negative');
            }
        }
    }
    
    private void setDefaultValues() {
        for(Account acc : newAccounts) {
            if(acc.Rating == null) {
                acc.Rating = 'Cold';
            }
            if(acc.Type == null) {
                acc.Type = 'Prospect';
            }
        }
    }
    
    private void trackFieldChanges() {
        for(Account acc : newAccounts) {
            Account oldAcc = oldAccountMap.get(acc.Id);
            if(acc.Industry != oldAcc.Industry) {
                acc.Industry_Changed_Date__c = System.now();
            }
        }
    }
    
    private void createDefaultContacts() {
        List<Contact> contacts = new List<Contact>();
        for(Account acc : newAccounts) {
            contacts.add(new Contact(
                AccountId = acc.Id,
                LastName = 'Primary Contact'
            ));
        }
        insert contacts;
    }
    
    private void sendNotifications() {
        // Implementation
    }
    
    private void updateRelatedRecords() {
        // Implementation
    }
    
    private void createAuditLogs() {
        // Implementation
    }
}

Updated Trigger:

trigger AccountTrigger on Account (before insert, before update, before delete,
                                   after insert, after update, after delete, after undelete) {
    new AccountTriggerHandler().run();
}

Usage of Bypass Mechanism:

// In a data migration script
TriggerHandler.bypass('AccountTriggerHandler');

// Perform bulk operations without trigger execution
List<Account> accounts = new List<Account>();
for(Integer i = 0; i < 10000; i++) {
    accounts.add(new Account(Name = 'Migrated Account ' + i));
}
insert accounts;

// Re-enable trigger
TriggerHandler.clearBypass('AccountTriggerHandler');

Benefits of Trigger Handler Pattern:

  1. Separation of Concerns: Business logic separated from trigger context
  2. Reusability: Handler methods can be called from other classes
  3. Testability: Easier to write unit tests for specific methods
  4. Maintainability: Clear organization of code
  5. Recursion Control: Built-in mechanism to prevent infinite loops
  6. Bypass Capability: Can disable triggers when needed (data migrations)
  7. Multiple Developers: Easier for team collaboration
  8. Governor Limit Management: Better optimization opportunities

Q12. What is @future annotation and when would you use it?

Answer:

The @future annotation is used to identify methods that run asynchronously in a separate thread, at a later time when system resources become available. This allows the calling method to continue without waiting for the @future method to complete.

Characteristics of @future Methods:

  1. Asynchronous Execution: Runs in a separate thread after transaction completes
  2. Static Methods Only: Must be static and return void
  3. Parameter Restrictions: Can only pass primitive types and collections of primitives
  4. No sObject Parameters: Cannot pass sObjects directly (pass IDs instead)
  5. Higher Governor Limits: 200 SOQL queries, 60 seconds CPU time, 12 MB heap
  6. Cannot Chain: A @future method cannot call another @future method
  7. No Return Values: Cannot return any value
  8. Queue Limit: Maximum 50 @future calls per transaction

Basic Syntax:

public class FutureMethodExample {
    @future
    public static void myFutureMethod(Set<Id> recordIds) {
        // Your asynchronous code here
        List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :recordIds];
        
        for(Account acc : accounts) {
            acc.Description = 'Updated by future method';
        }
        
        update accounts;
    }
}

// Call from another method
Set<Id> accountIds = new Set<Id>{'001xxx', '001yyy'};
FutureMethodExample.myFutureMethod(accountIds);

Use Cases for @future Methods:

1. Web Service Callouts:

Cannot make callouts from triggers directly, must use @future with callout=true

public class ExternalSystemIntegration {
    @future(callout=true)
    public static void sendDataToExternalSystem(Set<Id> accountIds) {
        List<Account> accounts = [SELECT Id, Name, Phone, Industry 
                                 FROM Account 
                                 WHERE Id IN :accountIds];
        
        for(Account acc : accounts) {
            HttpRequest req = new HttpRequest();
            req.setEndpoint('https://api.external.com/accounts');
            req.setMethod('POST');
            req.setHeader('Content-Type', 'application/json');
            
            // Create JSON payload
            Map<String, Object> payload = new Map<String, Object>{
                'id' => acc.Id,
                'name' => acc.Name,
                'phone' => acc.Phone,
                'industry' => acc.Industry
            };
            req.setBody(JSON.serialize(payload));
            
            Http http = new Http();
            try {
                HttpResponse res = http.send(req);
                if(res.getStatusCode() == 200) {
                    System.debug('Successfully sent: ' + acc.Name);
                } else {
                    System.debug('Error: ' + res.getBody());
                }
            } catch(Exception e) {
                System.debug('Exception: ' + e.getMessage());
            }
        }
    }
}

// Trigger usage
trigger AccountTrigger on Account (after insert, after update) {
    if(Trigger.isAfter && (Trigger.isInsert || Trigger.isUpdate)) {
        Set<Id> accountIds = new Set<Id>();
        for(Account acc : Trigger.new) {
            accountIds.add(acc.Id);
        }
        ExternalSystemIntegration.sendDataToExternalSystem(accountIds);
    }
}

2. Mixed DML Operations:

Cannot perform DML on setup and non-setup objects in the same transaction. In the below example we are creating account and User where account is non-setup object and User is setup object.

public class MixedDMLHandler {
    // Setup objects: User, Group, GroupMember, QueueSObject, etc.
    // Non-setup objects: Account, Contact, Custom Objects, etc.
    
    public static void createAccountAndUser(String accountName, String userName) {
        // Insert Account (non-setup object)
        Account acc = new Account(Name = accountName);
        insert acc;
        
        // Call @future to insert User (setup object)
        createUserAsync(userName);
    }
    
    @future
    public static void createUserAsync(String userName) {
        Profile prof = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1];
        UserRole ceoRole = [SELECT Id FROM UserRole WHERE Name = 'CEO' LIMIT 1];
        User newUser = new User(
            FirstName = 'Test',
            LastName = userName,
            Email = userName + '@test.com',
            Username = userName + '@test.com.dev',
            Alias = userName.substring(0, 5),
            TimeZoneSidKey = 'America/Los_Angeles',
            LocaleSidKey = 'en_US',
            EmailEncodingKey = 'UTF-8',
            LanguageLocaleKey = 'en_US',
            ProfileId = prof.Id,
            UserRoleId = ceoRole.Id
        );
        
        insert newUser;
    }
}

3. Long-Running Operations:

Operations that might hit synchronous governor limits

public class DataProcessor {
    @future
    public static void processLargeDataSet(Set<Id> recordIds) {
        // Process large amount of data
        List<Account> accounts = [SELECT Id, Name, 
                                 (SELECT Id FROM Contacts),
                                 (SELECT Id FROM Opportunities)
                                 FROM Account 
                                 WHERE Id IN :recordIds];
        
        for(Account acc : accounts) {
            // Complex processing
            calculateAccountMetrics(acc);
            updateRelatedRecords(acc);
            generateReports(acc);
        }
    }
    
    private static void calculateAccountMetrics(Account acc) {
        // Time-consuming calculations
    }
    
    private static void updateRelatedRecords(Account acc) {
        // Update multiple related records
    }
    
    private static void generateReports(Account acc) {
        // Generate and store reports
    }
}

4. Avoid Lock Contention:

When multiple users update the same parent record

public class OpportunityRollup {
    @future
    public static void updateAccountRollups(Set<Id> accountIds) {
        List<Account> accountsToUpdate = new List<Account>();
        
        for(Account acc : [SELECT Id,
                          (SELECT Id, Amount FROM Opportunities WHERE StageName = 'Closed Won')
                          FROM Account 
                          WHERE Id IN :accountIds]) {
            
            Decimal totalRevenue = 0;
            Integer opportunityCount = 0;
            
            for(Opportunity opp : acc.Opportunities) {
                if(opp.Amount != null) {
                    totalRevenue += opp.Amount;
                }
                opportunityCount++;
            }
            
            acc.Total_Won_Revenue__c = totalRevenue;
            acc.Number_of_Won_Opportunities__c = opportunityCount;
            accountsToUpdate.add(acc);
        }
        
        if(!accountsToUpdate.isEmpty()) {
            update accountsToUpdate;
        }
    }
}

Best Practices and Patterns:

1. Pass IDs, Not sObjects:

❌ Wrong:

@future
public static void processAccount(Account acc) { // ERROR: Cannot pass sObject
    acc.Rating = 'Hot';
    update acc;
}

✅ Correct:

@future
public static void processAccount(Id accountId) {
    Account acc = [SELECT Id, Rating FROM Account WHERE Id = :accountId];
    acc.Rating = 'Hot';
    update acc;
}

2. Bulkify @future Methods:

❌ Wrong:

trigger AccountTrigger on Account (after insert) {
    for(Account acc : Trigger.new) {
        FutureClass.processAccount(acc.Id); // Calling in loop - BAD!
    }
}

✅ Correct:

trigger AccountTrigger on Account (after insert) {
    Set<Id> accountIds = new Set<Id>();
    for(Account acc : Trigger.new) {
        accountIds.add(acc.Id);
    }
    FutureClass.processAccounts(accountIds); // Single call with Set
}

public class FutureClass {
    @future
    public static void processAccounts(Set<Id> accountIds) {
        List<Account> accounts = [SELECT Id, Rating FROM Account WHERE Id IN :accountIds];
        for(Account acc : accounts) {
            acc.Rating = 'Hot';
        }
        update accounts;
    }
}

3. Error Handling:

@future
public static void processRecords(Set<Id> recordIds) {
    try {
        List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :recordIds];
        
        // Process records
        for(Account acc : accounts) {
            acc.Description = 'Processed';
        }
        
        update accounts;
        
    } catch(DmlException e) {
        // Log error
        System.debug('DML Error: ' + e.getMessage());
        
        // Create error log
        Error_Log__c log = new Error_Log__c(
            Error_Message__c = e.getMessage(),
            Stack_Trace__c = e.getStackTraceString(),
            Record_IDs__c = String.join(new List<Id>(recordIds), ',')
        );
        insert log;
        
    } catch(Exception e) {
        System.debug('General Error: ' + e.getMessage());
    }
}

Limitations and Considerations:

LimitationDescription
Cannot chain@future method cannot call another @future method
50 method calls per transactionMaximum 50 @future calls
No return valueCannot return data
TestingUse Test.startTest() and Test.stopTest()
Order not guaranteedMultiple @future calls may execute in any order
Cannot pass sObjectsMust pass primitive types only
No callouts in same transactionIf making callout, no other DML in same transaction before @future

When NOT to Use @future:

  1. Need return value → Use Queueable instead
  2. Need to chain jobs → Use Queueable instead
  3. Need to pass sObjects → Use Queueable instead
  4. Need monitoring → Use Batch Apex or Queueable
  5. Complex workflows → Use Queueable or Platform Events

Alternative: Queueable Apex

For more flexibility, consider Queueable Apex which offers:

  • Can pass sObjects
  • Can chain jobs
  • Returns job ID for monitoring
  • Can implement Finalizer for error handling
public class AccountProcessor implements Queueable {
    private List<Account> accounts;
    
    public AccountProcessor(List<Account> accs) {
        this.accounts = accs;
    }
    
    public void execute(QueueableContext context) {
        for(Account acc : accounts) {
            acc.Rating = 'Hot';
        }
        update accounts;
    }
}

// Usage
List<Account> accounts = [SELECT Id, Rating FROM Account LIMIT 10];
Id jobId = System.enqueueJob(new AccountProcessor(accounts));
System.debug('Job ID: ' + jobId);

Q13. What is Batch Apex and when should you use it?

Answer:

Batch Apex allows you to process large numbers of records asynchronously by breaking the processing into smaller batches. Each batch is treated as a separate transaction with its own set of governor limits, making it ideal for processing millions of records that would otherwise hit governor limits in synchronous processing.

Key Characteristics:

  • Process large datasets: Up to 50 million records
  • Separate transactions: Each batch has its own governor limits
  • Asynchronous: Runs in background
  • Three methods required: start(), execute(), finish()
  • Batch size: Default 200 records, configurable (1-2000)
  • Governor limits per batch: 100 SOQL queries, 10,000 DML operations
  • Five concurrent batch jobs: Maximum 5 batch jobs running simultaneously

Basic Structure:

public class BatchClassName implements Database.Batchable<sObject> {
    
    // 1. START METHOD - Query records to process
    public Database.QueryLocator start(Database.BatchableContext bc) {
        String query = 'SELECT Id, Name FROM Account';
        return Database.getQueryLocator(query);
    }
    
    // 2. EXECUTE METHOD - Process each batch
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        // Process records in this batch
        for(Account acc : scope) {
            acc.Description = 'Processed by batch';
        }
        update scope;
    }
    
    // 3. FINISH METHOD - Post-processing
    public void finish(Database.BatchableContext bc) {
        // Send email notification, call another batch, etc.
        System.debug('Batch job completed');
    }
}

// Execute the batch
Database.executeBatch(new BatchClassName(), 200); // 200 = batch size

Complete Example with All Features:

public class AccountBatchProcessor implements Database.Batchable<sObject>, 
                                              Database.Stateful {
    
    // Instance variables (persist across batches with Database.Stateful)
    public Integer recordsProcessed = 0;
    public Integer errorCount = 0;
    public List<String> errorMessages = new List<String>();
    
    public Database.QueryLocator start(Database.BatchableContext bc) {
        System.debug('Batch Job Started at: ' + System.now());
        
        // Query can be dynamic
        String query = 'SELECT Id, Name, Industry, AnnualRevenue, Rating ' +
                      'FROM Account ' +
                      'WHERE Industry = \'Technology\' ' +
                      'AND AnnualRevenue > 1000000';
        
        return Database.getQueryLocator(query);
    }
    
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        List<Account> accountsToUpdate = new List<Account>();
        
        try {
            for(Account acc : scope) {
                // Business logic
                if(acc.AnnualRevenue > 10000000) {
                    acc.Rating = 'Hot';
                } else if(acc.AnnualRevenue > 5000000) {
                    acc.Rating = 'Warm';
                } else {
                    acc.Rating = 'Cold';
                }
                
                acc.Description = 'Processed on ' + System.today();
                acc.Last_Batch_Process_Date__c = System.now();
                
                accountsToUpdate.add(acc);
                recordsProcessed++;
            }
            
            // DML operation
            if(!accountsToUpdate.isEmpty()) {
                update accountsToUpdate;
            }
            
        } catch(Exception e) {
            errorCount++;
            errorMessages.add('Batch error: ' + e.getMessage() + ' | Stack: ' + e.getStackTraceString());
            System.debug('ERROR in batch: ' + e.getMessage());
        }
        
        System.debug('Processed batch of ' + scope.size() + ' records');
        System.debug('Total records processed so far: ' + recordsProcessed);
    }
    
    public void finish(Database.BatchableContext bc) {
        // Get job info
        AsyncApexJob job = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed,
                           TotalJobItems, CreatedBy.Email, ExtendedStatus
                           FROM AsyncApexJob 
                           WHERE Id = :bc.getJobId()];
        
        System.debug('Batch Job Completed');
        System.debug('Status: ' + job.Status);
        System.debug('Total Batches: ' + job.TotalJobItems);
        System.debug('Processed Batches: ' + job.JobItemsProcessed);
        System.debug('Failed Batches: ' + job.NumberOfErrors);
        System.debug('Records Processed: ' + recordsProcessed);
        System.debug('Errors Encountered: ' + errorCount);
        
        // Send email notification
        Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
        email.setToAddresses(new String[]{job.CreatedBy.Email});
        email.setSubject('Batch Job Completed: AccountBatchProcessor');
        
        String body = 'Batch Job Statistics:\n\n';
        body += 'Status: ' + job.Status + '\n';
        body += 'Total Records Processed: ' + recordsProcessed + '\n';
        body += 'Total Errors: ' + errorCount + '\n';
        body += 'Completion Time: ' + System.now() + '\n\n';
        
        if(errorCount > 0) {
            body += 'Error Messages:\n';
            for(String error : errorMessages) {
                body += error + '\n';
            }
        }
        
        email.setPlainTextBody(body);
        
        if(!Test.isRunningTest()) {
            Messaging.sendEmail(new Messaging.SingleEmailMessage[]{email});
        }
        
        // Chain another batch if needed
        // Database.executeBatch(new AnotherBatchClass());
    }
}

// Execute the batch
Id batchJobId = Database.executeBatch(new AccountBatchProcessor(), 200);
System.debug('Batch Job ID: ' + batchJobId);

Database.Stateful Interface:

Without Database.Stateful, instance variables are reset between batches. Use it to maintain state across batches.

// WITHOUT Database.Stateful
public class BatchWithoutStateful implements Database.Batchable<sObject> {
    public Integer counter = 0;
    
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        counter += scope.size();
        System.debug('Counter: ' + counter); // Always shows batch size, not cumulative
    }
}

// WITH Database.Stateful
public class BatchWithStateful implements Database.Batchable<sObject>, Database.Stateful {
    public Integer counter = 0;
    
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        counter += scope.size();
        System.debug('Counter: ' + counter); // Shows cumulative count
    }
}

Dynamic Query with Iterable:

When you can’t use a simple SOQL query or need custom logic:

public class CustomIterableBatch implements Database.Batchable<sObject>, Iterable<sObject> {
    
    public Iterable<sObject> start(Database.BatchableContext bc) {
        return this; // Return the class itself as it implements Iterable
    }
    
    public Iterator<sObject> iterator() {
        // Custom logic to build list
        List<Account> accounts = new List<Account>();
        
        // Complex query or data manipulation
        for(Account acc : [SELECT Id, Name FROM Account WHERE Industry = 'Technology']) {
            if(someComplexCondition(acc)) {
                accounts.add(acc);
            }
        }
        
        return accounts.iterator();
    }
    
    private Boolean someComplexCondition(Account acc) {
        // Custom logic
        return true;
    }
    
    public void execute(Database.BatchableContext bc, List<sObject> scope) {
        // Process
    }
    
    public void finish(Database.BatchableContext bc) {
        // Cleanup
    }
}

Chaining Batch Jobs:

public class FirstBatch implements Database.Batchable<sObject> {
    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator('SELECT Id FROM Account');
    }
    
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        // Process accounts
    }
    
    public void finish(Database.BatchableContext bc) {
        // Chain second batch
        Database.executeBatch(new SecondBatch(), 100);
    }
}

public class SecondBatch implements Database.Batchable<sObject> {
    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator('SELECT Id FROM Contact');
    }
    
    public void execute(Database.BatchableContext bc, List<Contact> scope) {
        // Process contacts
    }
    
    public void finish(Database.BatchableContext bc) {
        System.debug('All batch jobs completed');
    }
}

Scheduling Batch Jobs:

public class ScheduledBatchJob implements Schedulable {
    public void execute(SchedulableContext sc) {
        Database.executeBatch(new AccountBatchProcessor(), 200);
    }
}

// Schedule to run daily at 2 AM
String cronExp = '0 0 2 * * ?';
String jobName = 'Daily Account Batch Processing';
System.schedule(jobName, cronExp, new ScheduledBatchJob());

Monitoring Batch Jobs:

// Query batch job status
AsyncApexJob[] jobs = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed,
                       TotalJobItems, CreatedDate, CompletedDate
                       FROM AsyncApexJob
                       WHERE ApexClass.Name = 'AccountBatchProcessor'
                       ORDER BY CreatedDate DESC
                       LIMIT 10];

for(AsyncApexJob job : jobs) {
    System.debug('Job ID: ' + job.Id);
    System.debug('Status: ' + job.Status);
    System.debug('Progress: ' + job.JobItemsProcessed + '/' + job.TotalJobItems);
    System.debug('Errors: ' + job.NumberOfErrors);
}

// Abort a running batch job
Database.executeBatch(new AccountBatchProcessor());
// Get job ID and abort if needed
// System.abortJob(batchJobId);

Best Practices:

1. Optimal Batch Size:

// For simple operations: 200 (default)
Database.executeBatch(new SimpleBatch(), 200);

// For complex operations or callouts: smaller batch size
Database.executeBatch(new ComplexBatch(), 50);

// For very simple operations: larger batch size
Database.executeBatch(new VerySimpleBatch(), 2000);

2. Error Handling:

public void execute(Database.BatchableContext bc, List<Account> scope) {
    List<Account> successList = new List<Account>();
    List<Account> errorList = new List<Account>();
    
    for(Account acc : scope) {
        try {
            // Process record
            acc.Description = 'Processed';
            successList.add(acc);
        } catch(Exception e) {
            errorList.add(acc);
            System.debug('Error processing account: ' + acc.Id + ' - ' + e.getMessage());
        }
    }
    
    // Use Database methods for partial success
    if(!successList.isEmpty()) {
        Database.SaveResult[] results = Database.update(successList, false);
        for(Integer i = 0; i < results.size(); i++) {
            if(!results[i].isSuccess()) {
                System.debug('Failed to update: ' + successList[i].Id);
            }
        }
    }
}

When to Use Batch Apex:

✅ Use Batch Apex When:

  • Processing more than 10,000 records
  • Need to process large data volumes regularly
  • Operations would hit governor limits synchronously
  • Need to schedule large data processing jobs
  • Want separate transactions for each batch
  • Processing millions of records

❌ Don’t Use Batch Apex When:

  • Processing less than 10,000 records (use Queueable or @future)
  • Need immediate results (Batch is asynchronous)
  • Need to chain complex workflows (use Queueable for better control)
  • Operations are simple and fast (overhead not worth it)

Governor Limits:

Limit TypePer Batch
SOQL queries100
DML statements150
Records retrieved by SOQL50,000
Records processed by DML10,000
Heap size12 MB
CPU time60,000 ms

Q14. What is Queueable Apex?

Answer:

Queueable Apex combines the best features of @future methods and Batch Apex, providing a more flexible way to run asynchronous processes. It allows you to submit jobs for asynchronous processing with better monitoring and the ability to pass complex data types.

Key Advantages Over @future:

Feature@futureQueueable
Pass sObjects❌ No✅ Yes
Pass Complex Types❌ No✅ Yes
Job Monitoring❌ No ID returned✅ Returns Job ID
Job Chaining❌ Cannot chain✅ Can chain (max 1 chain per job)
Governor LimitsHigher (async)Higher (async)

Basic Implementation:

public class AccountProcessor implements Queueable {
    private List<Account> accounts;
    
    public AccountProcessor(List<Account> accs) {
        this.accounts = accs;
    }
    
    public void execute(QueueableContext context) {
        for(Account acc : accounts) {
            acc.Rating = 'Hot';
            acc.Description = 'Processed by Queueable';
        }
        update accounts;
        
        System.debug('Job ID: ' + context.getJobId());
    }
}

// Execute
List<Account> accounts = [SELECT Id, Rating FROM Account LIMIT 100];
Id jobId = System.enqueueJob(new AccountProcessor(accounts));
System.debug('Enqueued Job ID: ' + jobId);

Complete Example with All Features:

public class OpportunityProcessor implements Queueable, Database.AllowsCallouts {
    private List<Opportunity> opportunities;
    private String calloutEndpoint;
    private Boolean sendNotification;
    
    // Constructor accepts complex parameters
    public OpportunityProcessor(List<Opportunity> opps, 
                                String endpoint, 
                                Boolean notify) {
        this.opportunities = opps;
        this.calloutEndpoint = endpoint;
        this.sendNotification = notify;
    }
    
    public void execute(QueueableContext context) {
        List<Opportunity> processedOpps = new List<Opportunity>();
        
        try {
            // Process opportunities
            for(Opportunity opp : opportunities) {
                opp.Description = 'Processed on ' + System.now();
                processedOpps.add(opp);
            }
            
            // DML operation
            if(!processedOpps.isEmpty()) {
                update processedOpps;
            }
            
            // Make callout if endpoint provided
            if(String.isNotBlank(calloutEndpoint)) {
                makeCallout(processedOpps);
            }
            
            // Send notification if requested
            if(sendNotification) {
                sendEmailNotification(processedOpps.size());
            }
            
            // Chain another job if needed
            if(hasMoreWork()) {
                System.enqueueJob(new OpportunityProcessor(
                    getNextBatch(),
                    calloutEndpoint,
                    false
                ));
            }
            
        } catch(Exception e) {
            logError(e, context.getJobId());
        }
    }
    
    private void makeCallout(List<Opportunity> opps) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(calloutEndpoint);
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(JSON.serialize(opps));
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        if(res.getStatusCode() != 200) {
            System.debug('Callout failed: ' + res.getBody());
        }
    }
    
    private void sendEmailNotification(Integer count) {
        Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
        email.setToAddresses(new String[]{UserInfo.getUserEmail()});
        email.setSubject('Queueable Job Completed');
        email.setPlainTextBody(count + ' opportunities processed successfully');
        Messaging.sendEmail(new Messaging.SingleEmailMessage[]{email});
    }
    
    private Boolean hasMoreWork() {
        // Check if more records to process
        return false;
    }
    
    private List<Opportunity> getNextBatch() {
        return new List<Opportunity>();
    }
    
    private void logError(Exception e, Id jobId) {
        Error_Log__c log = new Error_Log__c(
            Error_Message__c = e.getMessage(),
            Stack_Trace__c = e.getStackTraceString(),
            Job_ID__c = jobId
        );
        insert log;
    }
}

// Usage
List<Opportunity> opps = [SELECT Id, Description FROM Opportunity LIMIT 100];
Id jobId = System.enqueueJob(new OpportunityProcessor(
    opps,
    'https://api.example.com/opportunities',
    true
));

Chaining Queueable Jobs:

public class FirstQueueableJob implements Queueable {
    public void execute(QueueableContext context) {
        List<Account> accounts = [SELECT Id FROM Account LIMIT 100];
        
        for(Account acc : accounts) {
            acc.Description = 'Processed by first job';
        }
        update accounts;
        
        // Chain second job (max 1 chain per job in synchronous context)
        if(!Test.isRunningTest()) {
            System.enqueueJob(new SecondQueueableJob());
        }
    }
}

public class SecondQueueableJob implements Queueable {
    public void execute(QueueableContext context) {
        List<Contact> contacts = [SELECT Id FROM Contact LIMIT 100];
        
        for(Contact con : contacts) {
            con.Description = 'Processed by second job';
        }
        update contacts;
    }
}

Best Practices:

public class QueueableBestPractices implements Queueable {
    private List<Account> accounts;
    private Integer retryCount;
    private static final Integer MAX_RETRIES = 3;
    
    public QueueableBestPractices(List<Account> accs, Integer retries) {
        this.accounts = accs;
        this.retryCount = retries != null ? retries : 0;
    }
    
    public void execute(QueueableContext context) {
        try {
            // Process accounts
            update accounts;
            
        } catch(Exception e) {
            // Retry logic
            if(retryCount < MAX_RETRIES) {
                System.debug('Retrying job. Attempt: ' + (retryCount + 1));
                System.enqueueJob(new QueueableBestPractices(accounts, retryCount + 1));
            } else {
                System.debug('Max retries reached. Logging error.');
                // Log final error
            }
        }
    }
}

Q15. What is the difference between System.debug() and System.assertEquals()?

System.debug():

  • Used for logging and debugging purposes
  • Prints information to the debug logs
  • Helps developers track the flow of execution and variable values
  • Does not affect the execution of code or test results
  • Syntax: System.debug('Message: ' + variable);

System.assertEquals():

  • Used in test classes for assertion
  • Compares expected and actual values
  • If the assertion fails, the test method fails
  • Essential for validating test results
  • Syntax: System.assertEquals(expected, actual, 'Optional message');

Example:

// Using System.debug()
Integer count = 5;
System.debug('Count value: ' + count);

// Using System.assertEquals() in a test class
@isTest
static void testMethod() {
    Integer expected = 10;
    Integer actual = 5 + 5;
    System.assertEquals(expected, actual, 'Values should match');
}

Must Read

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