DEV Community

Selavina B
Selavina B

Posted on

Performance Degradation Due to Inefficient Apex / ORM Usage (salesforce)

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
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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'
];
Enter fullscreen mode Exit fullscreen mode
  • Avoid unnecessary fields: SELECT Id, Name instead of SELECT *
  • Filter records using WHERE and LIMIT to 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';
}
Enter fullscreen mode Exit fullscreen mode

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);
}

Enter fullscreen mode Exit fullscreen mode

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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) {}
}
Enter fullscreen mode Exit fullscreen mode
  • 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

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode
  • 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)