DEV Community

Cover image for Salesforce Apex Code Failing Due to Lack of Bulkification
Selavina B
Selavina B

Posted on

Salesforce Apex Code Failing Due to Lack of Bulkification

Salesforce Apex Code Failing Due to Lack of Bulkification (And How to Fix It)

If you are developing on Salesforce, sooner or later you will face this error:

  • Too many SOQL queries: 101
  • Too many DML statements
  • CPU time limit exceeded

In most cases, the root cause is simple but dangerous:
your Apex code is not bulkified.

This issue usually does not appear during development or testing with single records. It shows up suddenly in production during data imports, integrations, or batch jobs and breaks business processes.

This post explains what bulkification really means, why Apex fails without it, and how to fix it properly with code.

The Real Problem
Salesforce does not execute Apex code one record at a time.

Instead:

  • Triggers can receive up to 200 records
  • Batch Apex can process thousands
  • Data Loader and integrations insert records in bulk

If your Apex logic assumes single-record execution, Salesforce governor limits will be exceeded.

That’s when things fail.

Step 1: Understand How Salesforce Executes Apex
Consider this trigger:

trigger AccountTrigger on Account (before insert) {
    for (Account acc : Trigger.new) {
        // Your logic here
    }
}
Enter fullscreen mode Exit fullscreen mode

This trigger might:

  • Run for 1 record
  • Or 200 records at once

Salesforce does not guarantee single-record execution.
Your code must always expect bulk input.

Step 2: Identify Non-Bulkified Code

Non-bulkified Apex

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

        acc.Description = 'Updated';
        update acc;
    }
}
Enter fullscreen mode Exit fullscreen mode

Why this fails

  • One SOQL query per record
  • One DML per record
  • 200 records = 200 queries + 200 DML
  • Governor limits exceeded

This code will break in production.

Step 3: Bulkify SOQL Queries (The Right Way)

Correct bulkified approach

trigger ContactTrigger on Contact (after insert) {

    Set<Id> accountIds = new Set<Id>();

    for (Contact c : Trigger.new) {
        if (c.AccountId != null) {
            accountIds.add(c.AccountId);
        }
    }

    Map<Id, Account> accountMap = new Map<Id, Account>(
        [SELECT Id, Description FROM Account WHERE Id IN :accountIds]
    );

    for (Account acc : accountMap.values()) {
        acc.Description = 'Updated';
    }

    update accountMap.values();
}
Enter fullscreen mode Exit fullscreen mode

What changed

  • One SOQL query
  • One DML statement
  • Works for 1 record or 200 records
  • Safe for production

Step 4: Never Put SOQL or DML Inside Loops

This is the golden rule of Apex.

What NOT to do

for (Opportunity opp : Trigger.new) {
    insert new Task(WhatId = opp.Id);
}
Enter fullscreen mode Exit fullscreen mode

Correct approach

List<Task> tasks = new List<Task>();

for (Opportunity opp : Trigger.new) {
    tasks.add(new Task(WhatId = opp.Id));
}

insert tasks;
Enter fullscreen mode Exit fullscreen mode

Step 5: Bulkify Helper Classes and Services

Bulkification must apply everywhere, not just triggers.

Hidden problem

public class AccountService {
    public static void updateAccount(Id accId) {
        Account acc = [SELECT Id FROM Account WHERE Id = :accId];
        acc.Name = 'Updated';
        update acc;
    }
}
Enter fullscreen mode Exit fullscreen mode

Calling this method in a loop will break limits.

Bulk-safe service method

public class AccountService {
    public static void updateAccounts(Set<Id> accountIds) {

        List<Account> accounts = [
            SELECT Id, Name FROM Account WHERE Id IN :accountIds
        ];

        for (Account acc : accounts) {
            acc.Name = 'Updated';
        }

        update accounts;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Use Trigger Handler Pattern (Highly Recommended)

Multiple triggers increase risk of non-bulkified logic.

Trigger

trigger AccountTrigger on Account (before update) {
    AccountHandler.beforeUpdate(Trigger.new);
}
Enter fullscreen mode Exit fullscreen mode

Handler

public class AccountHandler {
    public static void beforeUpdate(List<Account> accounts) {
        for (Account acc : accounts) {
            // Bulk-safe logic
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This pattern:

  • Forces bulk handling
  • Improves readability
  • Simplifies testing

Step 7: Write Bulk Test Cases (Mandatory)

If your test inserts only one record, your code is not truly tested.

Weak test

insert new Account(Name = 'Test');
Enter fullscreen mode Exit fullscreen mode

Proper bulk test

List<Account> accounts = new List<Account>();

for (Integer i = 0; i < 200; i++) {
    accounts.add(new Account(Name = 'Test ' + i));
}

insert accounts;
Enter fullscreen mode Exit fullscreen mode

If this test fails → your code is not bulkified.

Step 8: Watch These Governor Limits Closely

Limit Max
SOQL queries 100
DML statements 150
Records per DML 10,000
CPU time 10 sec (sync)

Bulkification is the only way to stay within these limits.

Key Takeaways

Salesforce Apex code usually fails in production not because it is wrong—but because it is written for single records.

To avoid failures:

  • Assume every trigger runs on 200 records
  • Never put SOQL or DML inside loops
  • Use collections (Set, Map, List)
  • Bulkify service classes
  • Test with bulk data, not single records

Once you adopt bulk-first thinking, Apex becomes stable, scalable, and production-safe.

Top comments (0)