This comprehensive guide covers essential Apex development interview questions ranging from basic concepts to advanced scenarios.
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:
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:
Example:
public class HelloWorld {
public static void sayHello() {
System.debug('Hello, Salesforce World!');
}
}
When to Use Apex:
Answer:
Apex supports three main types of collections, each with specific use cases and characteristics:
// 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];
// 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);
}
// 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:
Answer:
Both DML statements and Database methods perform database operations, but they differ in error handling and flexibility.
| Feature | DML Statements | Database Methods |
|---|---|---|
| Syntax | insert, update, upsert, delete, undelete | Database.insert(), Database.update(), etc. |
| Error Handling | Throws exception on any error | Returns result object with error details |
| Partial Success | Not supported (all-or-nothing) | Supported with allOrNone parameter |
| Transaction Rollback | Automatic on error | Optional based on allOrNone flag |
| Return Value | None | Returns SaveResult, UpsertResult, etc. |
| Use Case | When all records must succeed | When partial success is acceptable |
| Error Information | Exception message only | Detailed error per record |
// 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
}
// 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
// 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:
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.
| Limit Type | Synchronous | Asynchronous |
|---|---|---|
| Total SOQL queries | 100 | 200 |
| Total records retrieved by SOQL | 50,000 | 50,000 |
| Total DML statements | 150 | 150 |
| Total records processed by DML | 10,000 | 10,000 |
| Total heap size | 6 MB | 12 MB |
| Maximum CPU time | 10,000 ms | 60,000 ms |
| Total number of callouts | 100 | 100 |
| Maximum callout response size | 6 MB | 6 MB |
| Total SOSL queries | 20 | 20 |
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());
}
}
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
}
}
}
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.
List<sObject>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';
}
}
}
List<sObject>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);
}
}
}
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);
}
}
}
| Context Variable | before insert | before update | before delete | after insert | after update | after delete | after undelete |
|---|---|---|---|---|---|---|---|
| Trigger.new | ✅ (modifiable) | ✅ (modifiable) | ❌ | ✅ | ✅ | ❌ | ✅ |
| Trigger.old | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ |
| Trigger.newMap | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ |
| Trigger.oldMap | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ |
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;
}
}
Answer:
Apex triggers are classified into two main categories based on when they execute in the transaction lifecycle:
Execute before records are saved to the database.
Events:
before insertbefore updatebefore deleteCharacteristics:
Trigger.new records directly (no DML needed)Trigger.new in before delete (records are being deleted)Use Cases:
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();
}
}
}
}
Execute after records are saved to the database.
Events:
after insertafter updateafter deleteafter undeleteCharacteristics:
Trigger.new records directlyUse Cases:
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;
}
}
}
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;
}
}
}
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.
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;
}
}
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>
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;
}
}
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:
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.
// 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 ===');
}
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');
}
}
// 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
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 + ')');
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.
Definition: Variables that are shared across all instances of a class and persist for the duration of the transaction.
Characteristics:
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)
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;
}
}
Definition: Methods that belong to the class and can be called without creating an instance.
Characteristics:
this keywordpublic 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);
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);
}
}
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
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.
| Feature | SOQL | SOSL |
|---|---|---|
| Purpose | Query records from one object | Search across multiple objects |
| Return Type | List of sObjects | List of List of sObjects |
| Syntax | Similar to SQL SELECT | Similar to full-text search |
| Object Scope | Single object at a time | Multiple objects simultaneously |
| Relationship Queries | Supports child/parent relationships | Limited relationship support |
| Trigger Usage | Allowed | Not allowed |
| Search Scope | All fields or specified fields | Name, Email, Phone, Text fields |
| Performance | Fast for specific queries | Optimized for text search |
| Wildcards | Limited (LIKE with %) | Extensive (* and ?) |
| Record Limit | 50,000 records | 2,000 records |
| Query Limit | 100 (sync), 200 (async) | 20 per transaction |
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);
}
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]
Use SOQL When:
// 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:
// 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');
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)];
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);
}
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.
Problems with Logic in Triggers:
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;
}
}
}
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();
}
// 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');
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.
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);
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;
}
}
}
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());
}
}
| Limitation | Description |
|---|---|
| Cannot chain | @future method cannot call another @future method |
| 50 method calls per transaction | Maximum 50 @future calls |
| No return value | Cannot return data |
| Testing | Use Test.startTest() and Test.stopTest() |
| Order not guaranteed | Multiple @future calls may execute in any order |
| Cannot pass sObjects | Must pass primitive types only |
| No callouts in same transaction | If making callout, no other DML in same transaction before @future |
For more flexibility, consider Queueable Apex which offers:
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);
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.
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
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);
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
}
}
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
}
}
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');
}
}
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());
// 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);
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);
}
}
}
}
✅ Use Batch Apex When:
❌ Don’t Use Batch Apex When:
| Limit Type | Per Batch |
|---|---|
| SOQL queries | 100 |
| DML statements | 150 |
| Records retrieved by SOQL | 50,000 |
| Records processed by DML | 10,000 |
| Heap size | 12 MB |
| CPU time | 60,000 ms |
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.
| Feature | @future | Queueable |
|---|---|---|
| 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 Limits | Higher (async) | Higher (async) |
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);
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
));
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;
}
}
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
}
}
}
}
System.debug():
System.debug('Message: ' + variable);System.assertEquals():
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');
}