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
}
}
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;
}
}
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();
}
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);
}
Correct approach
List<Task> tasks = new List<Task>();
for (Opportunity opp : Trigger.new) {
tasks.add(new Task(WhatId = opp.Id));
}
insert tasks;
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;
}
}
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;
}
}
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);
}
Handler
public class AccountHandler {
public static void beforeUpdate(List<Account> accounts) {
for (Account acc : accounts) {
// Bulk-safe logic
}
}
}
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');
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;
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)