Bulkification & Governor Limits Mastery
๐ Concept
Bulkification is the most important concept in Salesforce development. It means writing code that efficiently handles 1 record or 200 records (or 10,000 in batch) with the same resource consumption pattern.
Why 200? When records are saved via Data Loader, API, or trigger re-firing, up to 200 records are processed in a single transaction (the chunk size). Your trigger fires ONCE for all 200 records, not 200 times.
The Bulkification Pattern:
1. Collect โ Gather all needed data (IDs, field values) from Trigger.new
2. Query โ Execute SOQL ONCE with collected data (WHERE IN :ids)
3. Map โ Build Map<Id, SObject> for O(1) lookups
4. Process โ Iterate and apply logic using the Map
5. DML โ Execute DML ONCE with all modified records
Governor Limits โ Complete Reference:
Synchronous Asynchronous
SOQL Queries 100 200
SOQL Rows Retrieved 50,000 50,000
DML Statements 150 150
DML Rows 10,000 10,000
CPU Time 10,000ms 60,000ms
Heap Size 6 MB 12 MB
Callouts 100 100
Future Methods 50 50 (from batch)
Queueable Jobs 50 1 (from another queueable)
Email Invocations 10 10
SOSL Searches 20 20
Why limits are per-transaction, not per-trigger: If a trigger on Account causes a trigger on Contact, and that causes a trigger on Task, ALL of those triggers share the SAME transaction's governor limits. This cascade effect is the #1 cause of limit failures in enterprise orgs.
Optimization techniques:
- Lazy loading โ Don't query data you might not need
- Selective queries โ Use indexed fields in WHERE clauses
- Query once, Map forever โ Build Maps from SOQL, then reference in loops
- Bulk collect โ Accumulate records to update, then DML once
- Short-circuit โ Exit early if no records meet your criteria
๐ป Code Example
1// Bulkification โ The Complete Pattern23public class BulkificationMastery {45 // โ NON-BULKIFIED (fails in production)6 public static void antiPattern(List<Opportunity> opps) {7 for (Opportunity opp : opps) {8 // SOQL in loop โ 200 records = 200 queries (limit is 100!)9 Account acc = [SELECT Name, Industry FROM Account WHERE Id = :opp.AccountId];1011 // DML in loop โ 200 records = 200 DML statements (limit is 150!)12 opp.Description = 'Account: ' + acc.Name;13 update opp;1415 // Callout in loop โ not even possible (must be after DML)16 }17 // With 200 records: 200 SOQL + 200 DML = GOVERNOR LIMIT EXCEPTION18 }1920 // โ BULKIFIED (production-ready)21 public static void bestPractice(List<Opportunity> opps) {22 // Step 1: COLLECT โ Gather all Account IDs23 Set<Id> accountIds = new Set<Id>();24 for (Opportunity opp : opps) {25 if (opp.AccountId != null) {26 accountIds.add(opp.AccountId);27 }28 }2930 // Step 2: QUERY ONCE โ Single SOQL for all accounts31 // Step 3: MAP โ Build Map for O(1) lookups32 Map<Id, Account> accountMap = new Map<Id, Account>(33 [SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds]34 );35 // Only 1 SOQL query used, regardless of how many opportunities3637 // Step 4: PROCESS โ Apply business logic using the Map38 List<Opportunity> oppsToUpdate = new List<Opportunity>();39 for (Opportunity opp : opps) {40 Account acc = accountMap.get(opp.AccountId);41 if (acc != null) {42 opp.Description = 'Account: ' + acc.Name;43 oppsToUpdate.add(opp);44 }45 }4647 // Step 5: DML ONCE โ Single update for all modified records48 if (!oppsToUpdate.isEmpty()) {49 update oppsToUpdate;50 }51 // Total: 1 SOQL + 1 DML, regardless of record count52 }5354 // Advanced: Handling multiple related objects55 public static void complexBulkification(List<Opportunity> opps) {56 // Collect ALL needed IDs upfront57 Set<Id> accountIds = new Set<Id>();58 Set<Id> ownerIds = new Set<Id>();5960 for (Opportunity opp : opps) {61 accountIds.add(opp.AccountId);62 ownerIds.add(opp.OwnerId);63 }6465 // Query related data in PARALLEL (2 queries, not 2N)66 Map<Id, Account> accounts = new Map<Id, Account>(67 [SELECT Id, Name, Industry, OwnerId FROM Account WHERE Id IN :accountIds]68 );69 Map<Id, User> owners = new Map<Id, User>(70 [SELECT Id, Name, Email FROM User WHERE Id IN :ownerIds]71 );7273 // Process with O(1) lookups74 List<Task> tasksToCreate = new List<Task>();75 for (Opportunity opp : opps) {76 if (opp.Amount > 100000) {77 Account acc = accounts.get(opp.AccountId);78 User owner = owners.get(opp.OwnerId);7980 tasksToCreate.add(new Task(81 WhatId = opp.Id,82 OwnerId = opp.OwnerId,83 Subject = 'High-value opp: ' + acc?.Name,84 Description = 'Assigned to: ' + owner?.Name,85 ActivityDate = Date.today().addDays(7)86 ));87 }88 }8990 if (!tasksToCreate.isEmpty()) {91 insert tasksToCreate;92 }93 // Total: 2 SOQL + 1 DML for ANY number of records94 }9596 // Monitoring governor limit consumption97 public static void monitorLimits() {98 System.debug('=== Governor Limit Usage ===');99 System.debug('SOQL: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());100 System.debug('DML: ' + Limits.getDmlStatements() + '/' + Limits.getLimitDmlStatements());101 System.debug('DML Rows: ' + Limits.getDmlRows() + '/' + Limits.getLimitDmlRows());102 System.debug('CPU: ' + Limits.getCpuTime() + 'ms/' + Limits.getLimitCpuTime() + 'ms');103 System.debug('Heap: ' + Limits.getHeapSize() + '/' + Limits.getLimitHeapSize());104 System.debug('SOQL Rows: ' + Limits.getQueryRows() + '/' + Limits.getLimitQueryRows());105 }106107 // Preventing trigger recursion108 public class RecursionGuard {109 private static Set<Id> processedIds = new Set<Id>();110111 public static Boolean hasProcessed(Id recordId) {112 return processedIds.contains(recordId);113 }114115 public static void markProcessed(Id recordId) {116 processedIds.add(recordId);117 }118119 public static void markProcessed(Set<Id> recordIds) {120 processedIds.addAll(recordIds);121 }122123 public static void reset() {124 processedIds.clear();125 }126 }127}
๐๏ธ Practice Exercise
Bulkification & Limits Exercises:
- Take a non-bulkified trigger and refactor it using the CollectโQueryโMapโProcessโDML pattern
- Write a trigger that handles 200 records while using only 2 SOQL queries and 1 DML statement
- Create a stress test that inserts 200 records via Data Loader and verify your trigger handles it
- Write a debugging utility that logs governor limit consumption at key points in your code
- Implement a recursion guard that prevents triggers from re-processing the same records
- Refactor a trigger cascade (AccountโContactโCase) to stay within limits
- Write code that intentionally exceeds each governor limit and handle the exceptions gracefully
- Design a batch process that handles 1 million records without hitting any limits
- Create a before-trigger that validates records against complex criteria using only 1 SOQL query
- Benchmark your code: measure SOQL, DML, and CPU usage for processing 1, 50, and 200 records
โ ๏ธ Common Mistakes
Writing code that works for 1 record and assuming it works for 200 โ always test with bulk data (200 records minimum)
Not recognizing cascading triggers โ a trigger on Account that updates Contacts fires the Contact trigger, sharing the same limits
Using Limits class for flow control โ checking limits to decide whether to query is an anti-pattern; it means your code isn't properly bulkified
Not accounting for existing automation โ your trigger shares limits with Flows, Process Builders, and other triggers on the same object
Ignoring CPU time limits โ even with perfect SOQL/DML optimization, complex logic in large loops can exceed the 10-second CPU limit
๐ผ Interview Questions
๐ค Mock Interview
Mock interview is powered by AI for Bulkification & Governor Limits Mastery. Login to unlock this feature.