What Are the Best Practices for General Trigger Bulkification?

What are the best practices for bulkifying Apex triggers in Salesforce? Also, how should we handle scenarios where more than 200 records are updated, since this will cause the trigger to execute multiple times?

Answer

Bulkification in Salesforce refers to designing Apex triggers and classes so they can efficiently handle large batches of records. Because Salesforce enforces strict governor limits on SOQL queries, DML operations, heap usage, and CPU, it is essential to write trigger logic that safely processes many records at once.

One of the fundamental techniques is to avoid performing DML statements or SOQL queries inside loops. Instead of inserting or updating each individual record separately, we should gather records in collections and perform operations once at the end of processing.

Here is a basic example of the wrong approach:

trigger AccountTrigger on Account(after insert){
    for(Account a : Trigger.new){
        MyObject__c obj = new MyObject__c(Account__c = a.Id);
        insert obj;  // WRONG - DML inside loop
    }
}

This performs an insert for each iteration of the loop and may easily exceed the DML limit when many Accounts are inserted.

The better approach is to collect objects and insert them together:

trigger AccountTrigger on Account(after insert){
    List<MyObject__c> objs = new List<MyObject__c>();
    for(Account a : Trigger.new){
        objs.add(new MyObject__c(Account__c = a.Id));
    }
    insert objs; // CORRECT - one DML outside loop
}

The same pattern applies to SOQL queries. Instead of querying inside every loop iteration, you collect keys and query once using IN:

Wrong version:

trigger ContactTrigger on Contact(before insert){
    for(Contact c : Trigger.new){
        Account acc = [SELECT ShippingCity FROM Account WHERE Id = :c.AccountId];
        c.ShippingCity = acc.ShippingCity;
    }
}

This performs one query for each Contact, which will quickly exceed SOQL limits.

The bulkified version queries everything at once by collecting all required Ids:

trigger ContactTrigger on Contact(before insert){
    Map<Id,Account> accountMap = new Map<Id,Account>();

    for(Contact c : Trigger.new){
        if(c.AccountId != null){
            accountMap.put(c.AccountId, null);
        }
    }

    accountMap.remove(null);
    accountMap.putAll([
        SELECT Id, ShippingCity 
        FROM Account 
        WHERE Id IN :accountMap.keySet()
    ]);

    for(Contact c : Trigger.new){
        if(accountMap.containsKey(c.AccountId)){
            c.ShippingCity = accountMap.get(c.AccountId).ShippingCity;
        }
    }
}

Here, the trigger performs only a single SOQL query no matter how many contacts are inserted.

Organizing Trigger Logic (One Trigger per Object)

Another recommended practice is to have only one trigger per object and move its logic into a handler class. This prevents multiple triggers from running in unpredictable order and makes the code cleaner and easier to maintain.

Example trigger:

trigger ContactTrigger on Contact(before insert, after insert, before update, after update){
    if(Trigger.isBefore && Trigger.isInsert){
        ContactTriggerHandler.beforeInsert(Trigger.new);
    }
    if(Trigger.isAfter && Trigger.isInsert){
        ContactTriggerHandler.afterInsert(Trigger.new);
    }
}

Then the handler class contains reusable methods:

public class ContactTriggerHandler {

    public static void beforeInsert(List<Contact> contacts){
        methodA(contacts);
        methodB(contacts);
    }

    public static void afterInsert(List<Contact> contacts){
        methodC(contacts);
    }

    private static void methodA(List<Contact> contacts){
        // logic
    }
}

This approach provides modularity: individual methods can be reused by other code and unit testing becomes easier.

Handling More Than 200 Records

Apex triggers automatically split processing into batches of up to 200 records. This means that even if 10,000 records are updated, the trigger is fired multiple times. If your bulk logic follows collection-based patterns, and avoids DML and SOQL inside loops, then your trigger is automatically capable of processing multiple batches.

So the best practice is not to handle large batches manually but to ensure the trigger code is already bulk-safe by design. Salesforce handles batch splitting automatically.

Summary Thought

Bulkification means designing triggers so they operate on all records at once instead of individually. The main techniques are avoiding queries and DML inside loops, using maps and lists, writing reusable logic in handler classes, and using a single trigger per object. Together these practices make your triggers scalable, maintainable, and safe from governor limit failures.

If needed, the same approach can be extended further into full trigger frameworks and interfaces, but these examples already demonstrate a solid foundation that most Salesforce developers should follow.

0
Would love your thoughts, please comment.x
()
x