Why Bulkification Matters
Salesforce enforces per-transaction governor limits, including a maximum of 100 SOQL queries and 150 DML statements, precisely because a single trigger invocation can process up to 200 records at once when triggered by a bulk API load, Data Loader import, or bulk API operation. Bulkification means writing trigger and handler logic so that all SOQL queries and DML statements execute once per transaction, operating on entire collections of records, rather than once per record inside a loop, which is what actually causes those limits to be exceeded.
Cricket analogy: Bulkification is like a groundstaff crew rolling the entire pitch in one continuous pass before a Test match rather than walking out to flatten one blade of grass at a time between every single delivery.
SOQL and DML Inside Loops
Placing a SOQL query or DML statement, such as an update, inside a for loop that iterates over Trigger.new is the single most common cause of hitting governor limits, because a bulk load of 200 records would trigger 200 separate queries or DML statements in that one transaction, quickly exceeding the 100-query or 150-DML ceiling and throwing a System.LimitException. The fix is to move any needed query outside the loop, gather the relevant Ids first using a Set<Id>, run one bulk query, then loop over the in-memory results to build a single bulk DML list executed once after the loop.
Cricket analogy: Running a SOQL query inside a loop over 200 records is like sending a runner to check the scoreboard individually after every single ball of a 200-ball innings instead of just glancing at the board once when needed.
// Anti-pattern: SOQL and DML inside a loop
for (Opportunity opp : Trigger.new) {
Account acc = [SELECT Id, Rating FROM Account WHERE Id = :opp.AccountId]; // query per record
acc.Rating = 'Hot';
update acc; // DML per record
}
// Bulk-safe pattern
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : Trigger.new) {
accountIds.add(opp.AccountId);
}
Map<Id, Account> accountsById = new Map<Id, Account>(
[SELECT Id, Rating FROM Account WHERE Id IN :accountIds]
);
List<Account> accountsToUpdate = new List<Account>();
for (Opportunity opp : Trigger.new) {
Account acc = accountsById.get(opp.AccountId);
if (acc != null) {
acc.Rating = 'Hot';
accountsToUpdate.add(acc);
}
}
if (!accountsToUpdate.isEmpty()) {
update accountsToUpdate;
}A trigger that works fine when tested with a single record in the UI can fail catastrophically in production the first time someone imports 200 records via Data Loader, because the SOQL-in-loop pattern that silently worked at scale of one now issues 200 queries and throws a 'Too many SOQL queries' limit exception.
Collections and Maps for Bulk-Safe Logic
A common bulk-safe pattern is to first collect related Ids into a Set<Id> while looping over Trigger.new, then issue a single SOQL query using an IN clause against that set, and finally store the results in a Map<Id, sObject> or Map<Id, List<sObject>> for O(1) lookups when building the final DML list. This pattern, summarized as 'collect, query once, map, then act', scales identically whether the trigger fires on one record or two hundred, because the number of queries and DML statements stays constant regardless of the record count.
Cricket analogy: Collecting Ids into a set before one bulk query is like a scorer noting down every batsman due to bat before checking the entire lineup card once, rather than flipping through the lineup card separately for each individual batsman.
Bulk-Safe Trigger Design Checklist
Beyond avoiding SOQL and DML in loops, a bulk-safe trigger keeps the trigger body itself minimal, delegating to a handler class; uses a static Boolean or Set<Id> as a recursion guard to prevent the same records from being reprocessed if the handler's own DML re-fires the trigger; and always assumes Trigger.new could contain anywhere from 1 to 200 records rather than hardcoding logic for a single record. Testing with bulk data, typically 200+ records, in unit tests is the only reliable way to catch a bulkification bug before it reaches production.
Cricket analogy: A recursion guard preventing a trigger from reprocessing its own DML is like a third umpire's protocol that a review, once conclusively decided, cannot be re-reviewed within the same passage of play, preventing an endless review loop.
A simple static recursion guard: public class TriggerControl { public static Boolean accountTriggerHasRun = false; } and the handler checks if (TriggerControl.accountTriggerHasRun) return; before setting it true, preventing infinite loops when the handler's own after-update DML re-fires AccountTrigger.
- Bulkification means SOQL queries and DML statements run once per transaction on full collections, not once per record inside a loop.
- A single trigger invocation can process up to 200 records at once from bulk API loads or Data Loader imports.
- The 'collect Ids, query once, map, then act' pattern keeps query and DML counts constant regardless of record volume.
- Never place a SOQL query or DML statement inside a loop over Trigger.new - this is the top cause of governor-limit exceptions.
- Use a static recursion guard to stop a handler's own DML from re-triggering itself and causing infinite loops.
- Always write and test trigger logic assuming Trigger.new can contain 1 to 200 records, not just one.
- Unit tests should insert 200+ records via Test.startTest()/stopTest() to catch bulkification bugs before production.
Practice what you learned
1. What is the primary cause of a 'Too many SOQL queries' governor limit exception?
2. What is the correct bulk-safe pattern for querying related records inside a trigger handler?
3. How many records can a single trigger invocation contain when triggered by a Data Loader bulk import?
4. What is the purpose of a static Boolean recursion guard in a trigger handler?
5. Why should unit tests insert 200+ records rather than just 1?
Was this page helpful?
You May Also Like
Apex Triggers Basics
An introduction to what Apex triggers are, how their syntax and events work, and when to reach for a trigger instead of declarative automation.
Trigger Context Variables
A guide to Trigger.new, Trigger.old, their map equivalents, and the boolean context flags that let a single handler branch correctly across all seven trigger contexts.
Trigger Frameworks
Why orgs adopt a shared trigger handler framework, the core components such frameworks share, and how metadata-driven bypass mechanisms support safe data migrations.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics