OrgReady

Salesforce Skills & Readiness

Learning progress is unavailable. Reading and direct practice remain available; completion controls are disabled.

← Back to Salesforce Development

Lesson 2 of 6 · 5–10 minutes including the walkthrough and reflection

Bulkification

A trigger can receive one record from the UI or many records from Data Loader, an integration, Flow, or another Apex process. Bulkification means your Apex handles the whole collection with predictable SOQL and DML usage, instead of accidentally doing database work once per record.

The concept

Bulkification is not just “don’t put SOQL in a loop.” That rule is a symptom of a bigger design habit: Trigger.new ↓ Think in collections ↓ Collect unique IDs with Set<Id> ↓ Query related records once ↓ Use Maps for fast lookup ↓ Prepare changed records in memory ↓ Perform DML on a collection Why this matters: - A trigger runs with a collection, even when the collection has one record. - Single-record testing can hide code that fails during a 200-record update. - Moving a query into a helper method does not make it bulk-safe if the helper is still called once per record. - Duplicate related IDs are common in real data, so a Set<Id> helps avoid repeated work. Simple mental model: Do the thinking in loops. Do the database work outside loops. Real project scenario: When Account.Industry changes, update related Contacts so Contact.Description matches the Account’s new Industry. The safe design is: Trigger.new ↓ Compare each Account with Trigger.oldMap ↓ Collect changed Account IDs ↓ Query Contacts once using WHERE AccountId IN :changedAccountIds ↓ Use Map<Id, String> for Account Id → Industry ↓ Update all changed Contacts once

Worked example

// ❌ Problematic: SOQL and DML repeat for every Account.
trigger AccountTrigger on Account (after update) {
    for (Account accountRecord : Trigger.new) {
        Account oldAccount = Trigger.oldMap.get(accountRecord.Id);
        if (oldAccount.Industry == accountRecord.Industry) {
            continue;
        }

        List<Contact> contacts = [
            SELECT Id, Description
            FROM Contact
            WHERE AccountId = :accountRecord.Id
        ];

        for (Contact contactRecord : contacts) {
            contactRecord.Description = accountRecord.Industry;
        }

        update contacts;
    }
}

// ✅ Better: collect, query once, process in memory, DML once.
Set<Id> changedAccountIds = new Set<Id>();
Map<Id, String> industryByAccountId = new Map<Id, String>();

for (Account accountRecord : Trigger.new) {
    Account oldAccount = Trigger.oldMap.get(accountRecord.Id);
    if (oldAccount != null && accountRecord.Industry != oldAccount.Industry) {
        changedAccountIds.add(accountRecord.Id);
        industryByAccountId.put(accountRecord.Id, accountRecord.Industry);
    }
}

List<Contact> contactsToUpdate = new List<Contact>();

for (Contact contactRecord : [
    SELECT Id, AccountId, Description
    FROM Contact
    WHERE AccountId IN :changedAccountIds
]) {
    String newIndustry = industryByAccountId.get(contactRecord.AccountId);
    if (contactRecord.Description != newIndustry) {
        contactRecord.Description = newIndustry;
        contactsToUpdate.add(contactRecord);
    }
}

if (!contactsToUpdate.isEmpty()) {
    update contactsToUpdate;
}

Notice what changed: 1. The loop over Trigger.new only collects information. 2. Set<Id> keeps each Account Id once, even if upstream logic repeats references. 3. The Contact query happens once and asks only for Contacts related to changed Accounts. 4. Map<Id, String> lets each Contact find the correct Industry without another query. 5. The update happens once for the final list. 💡 Helper methods are still useful. A handler can make this code easier to test and maintain. The key question is what the helper does. A helper that queries once for a Set<Id> is bulk-friendly. A helper called inside a loop that queries one Account at a time is still a looped query.

Common mistake

❌ Common mistake: “I tested with one Account and it passed, so the trigger is safe.” Why that is incomplete: One-record testing proves only the smallest input. A production update might send many Accounts in the same transaction. If your code performs one SOQL query and one DML statement per Account, the risk grows with the input size. Another mistake: “I moved the query to AccountService, so there is no SOQL in the trigger.” That only improves organization. It does not reduce database work if AccountService is still called once per record.

Interview angle

🎯 Interview angle Question: “How do you bulkify an Apex trigger?” Weak answer: “I do not put SOQL inside loops.” Stronger answer: “I design the trigger for collections from the start. I collect IDs in Sets, query related records once, store them in Maps for lookup, process records in memory, and perform DML on collections. I also test with multiple records because single-record tests can miss bulk failures.”

Pause and apply

🧠 Key takeaway: Collect ↓ Query once ↓ Process in memory ↓ DML once Try it yourself: 1. You receive 200 Accounts and need their Contacts. Where should the Contact SOQL query happen? 2. If two records refer to the same related Account, which collection helps avoid duplicate IDs? 3. What is the difference between moving a query to a helper and making that helper bulk-safe? 4. In the Account/Contact scenario, why is Trigger.oldMap useful before querying Contacts?

Official references

Reviewed 2026-09-15. Examples are illustrative; no code is executed here.

Apply it in practice

Bulk-safe Account Trigger

Marking a lesson as read records learning activity, not mastery. Practice evidence continues to come from the challenge system.