Problem Statement: Slow page loads, long batch jobs, or timeouts occur when Apex code is not bulk-safe, triggers perform row-by-row operations, or inefficient queries are used, leading to governor limit exceptions and poor org performance.
Step 1 Identify Inefficient Apex Patterns
Common Bad Practice
trigger AccountTrigger on Account (after insert, after update) {
for (Account acc : Trigger.new) {
// N+1 query inside loop
List<Contact> contacts = [SELECT Id, Email FROM Contact WHERE AccountId = :acc.Id];
for (Contact c : contacts) {
c.Email = acc.Name + '.contact@example.com';
update c; // DML inside loop
}
}
}
Problems:
- SOQL in a loop → N+1 problem
- DML in a loop → governor limit errors
- Not bulk-safe → fails on large record batches
Step 2 Make Apex Code Bulk-Safe
Rule: Always process collections (lists, maps, sets) and avoid per-record queries or DML inside loops.
Bulk-Safe Version
trigger AccountTrigger on Account (after insert, after update) {
// Step 1: Collect all Account IDs
Set<Id> accountIds = new Set<Id>();
for (Account acc : Trigger.new) {
accountIds.add(acc.Id);
}
// Step 2: Query all related contacts at once
List<Contact> contactsToUpdate = [
SELECT Id, Email, AccountId
FROM Contact
WHERE AccountId IN :accountIds
];
// Step 3: Prepare contacts for update
for (Contact c : contactsToUpdate) {
Account parent = Trigger.newMap.get(c.AccountId);
c.Email = parent.Name + '.contact@example.com';
}
// Step 4: Bulk DML
if (!contactsToUpdate.isEmpty()) {
update contactsToUpdate;
}
}
Benefits:
- Single SOQL query → avoids N+1 problem
- Single DML statement → governor-safe
- Works with hundreds or thousands of records
Step 3 Optimize SOQL Queries
- Use relationship queries when possible
List<Account> accounts = [
SELECT Id, Name, (SELECT Id, Email FROM Contacts)
FROM Account
WHERE Industry = 'Technology'
];
- Avoid unnecessary fields:
SELECT Id, Nameinstead ofSELECT * - Filter records using
WHEREandLIMITto reduce data size
Step 4 Use Maps for Fast Lookups
Instead of iterating nested loops, use maps to improve performance:
Map<Id, Account> accountMap = new Map<Id, Account>(Trigger.new);
for (Contact c : contactsToUpdate) {
Account parent = accountMap.get(c.AccountId);
c.Email = parent.Name + '.contact@example.com';
}
Lookup in O(1) instead of O(n²)
Step 5 Move Logic to Handler Classes
Trigger:
trigger AccountTrigger on Account (after insert, after update) {
AccountTriggerHandler.handleAfter(Trigger.new, Trigger.oldMap);
}
Handler Class:
public class AccountTriggerHandler {
public static void handleAfter(List<Account> newList, Map<Id, Account> oldMap) {
Set<Id> accIds = new Set<Id>();
for (Account acc : newList) accIds.add(acc.Id);
List<Contact> contacts = [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accIds];
Map<Id, Account> accountMap = new Map<Id, Account>(newList);
for (Contact c : contacts) {
c.Email = accountMap.get(c.AccountId).Name + '.contact@example.com';
}
if (!contacts.isEmpty()) update contacts;
}
}
Benefits:
- Cleaner code
- Easier debugging
- Easy to reuse in batch jobs
Step 6 — Use Batch Apex for Large Data Volumes
For large record sets, Batch Apex is essential:
global class UpdateContactsBatch implements Database.Batchable<SObject> {
global Database.QueryLocator start(Database.BatchableContext BC) {
return Database.getQueryLocator([SELECT Id, AccountId FROM Contact]);
}
global void execute(Database.BatchableContext BC, List<Contact> scope) {
Set<Id> accountIds = new Set<Id>();
for (Contact c : scope) accountIds.add(c.AccountId);
Map<Id, Account> accMap = new Map<Id, Account>([
SELECT Id, Name FROM Account WHERE Id IN :accountIds
]);
for (Contact c : scope) {
c.Email = accMap.get(c.AccountId).Name + '.contact@example.com';
}
update scope;
}
global void finish(Database.BatchableContext BC) {}
}
- Processes 50k+ records safely
- Avoids governor limits
- Bulk-safe architecture
Step 7 Use @future or Queueable for Non-Critical Operations
For operations not required immediately (like sending emails or updating related objects):
@future
public static void updateContactEmails(Set accountIds) {
List contacts = [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds];
for (Contact c : contacts) {
c.Email = [SELECT Name FROM Account WHERE Id = :c.AccountId].Name + '.contact@example.com';
}
update contacts;
}
- Moves heavy work out of synchronous transaction
- Reduces UI latency
Step 8 Monitor and Test
- Enable Debug Logs → check SOQL and DML statements
- Test with bulk records (200+)
- Use Query Plan Tool for expensive queries
Conclusion
Performance degradation in Salesforce is almost always caused by inefficient Apex code or improper ORM usage. By making triggers bulk-safe, using batch operations, minimizing SOQL/DML inside loops, and separating logic into handler classes or batch jobs, you can eliminate slow page loads, prevent governor limit errors, and scale your org for large datasets efficiently.
Top comments (0)