OrgReady

Salesforce Skills & Readiness

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

← Back to Salesforce Development

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

Apex Transactions & Governor Limits

Apex does not run in isolation. One user save can start validation rules, Flow, triggers, handler classes, SOQL, and DML in the same transaction. If you understand the shared transaction budget, you can write code that works for one record and for a bulk update.

The concept

What is a transaction? A transaction is one unit of work that Salesforce tries to complete together. A simple analogy is a bank transfer: debit one account, credit another account, and commit only when the whole operation is valid. If something fails, the work can be rolled back. Salesforce example: User updates Account ↓ Validation rules / Flow ↓ Apex Trigger ↓ Trigger Handler ↓ SOQL / DML ↓ Transaction completes All that work can share the same transaction. Moving SOQL into a helper class makes the code cleaner, but it does not reset governor limits. The helper still spends from the same transaction budget. What are governor limits? Salesforce is a multi-tenant platform. Governor limits protect shared resources so one org or one piece of runaway code cannot monopolize the platform. This is why Apex design must be collection-based instead of one-record-only. Why one-record testing is misleading: A trigger that works from the UI for one Account can still fail when Data Loader, an integration, Flow, or another Apex process updates many records. A trigger can receive a collection of records, so design as if many records can arrive together. ✅ Recommended bulk pattern: Trigger.new ↓ Collect IDs ↓ Query once ↓ Process collection ↓ DML once ❌ Anti-pattern: Loop over Accounts ↓ Query Contacts inside the loop ↓ Update Contacts inside the loop Real-world project example: When Account.Industry changes, update related Contact.Description. A safe design collects changed Account IDs, queries all related Contacts once, sets each Contact Description from a map of Account Id to Industry, and performs one update for the Contact list.

Worked example

// ❌ Anti-pattern: database work repeats for every Account.
trigger AccountTrigger on Account (after update) {
    for (Account accountRecord : Trigger.new) {
        List<Contact> contacts = [
            SELECT Id, Description
            FROM Contact
            WHERE AccountId = :accountRecord.Id
        ];

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

        update contacts;
    }
}

// ✅ Better shape: collect, query once, process, 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 = [
    SELECT Id, AccountId, Description
    FROM Contact
    WHERE AccountId IN :changedAccountIds
];

for (Contact contactRecord : contactsToUpdate) {
    contactRecord.Description = industryByAccountId.get(contactRecord.AccountId);
}

update contactsToUpdate;

Step by step: 1. Compare Trigger.new with Trigger.oldMap so you only process Accounts whose Industry changed. 2. Collect Account IDs in a Set<Id>. A Set avoids duplicates and is ideal for an IN query. 3. Query related Contacts once, outside the Account loop. 4. Use a Map<Id, String> so each Contact can receive the correct new Industry. 5. Update the Contact list once, outside loops. 💡 The important idea is not memorizing every limit number. The important idea is recognizing repeated database work and replacing it with collection-based work.

Common mistake

Common mistake: "I moved the query into AccountTriggerHandler, so the trigger is bulk-safe." Why this is wrong: A helper class improves organization, but it does not create a new transaction. If the helper runs one query per record, the transaction still pays for one query per record. Another common mistake: Testing only one Account update. That proves the happy path, but it does not prove bulk safety.

Interview angle

🎯 Interview angle Question: "Why should Apex triggers be bulkified?" Weak answer: "Because Salesforce has governor limits." Stronger reasoning: "A trigger can receive multiple records in one transaction. If SOQL or DML runs once per record, resource usage grows with the batch size and can fail in production. I collect IDs, query records in bulk, process collections, and perform DML once."

Pause and apply

🧠 Key takeaway: A transaction is one shared unit of work. Helper classes, triggers, Flow, SOQL, and DML can all spend from the same budget. Bulk-safe code reduces repeated database work. Try it yourself: 1. In the Account/Contact example, where would you collect Account IDs? 2. Why is Trigger.oldMap useful for detecting changed fields? 3. What breaks if you query Contacts inside the Account loop? 4. If your code works for one Account, what extra test would prove it handles many Accounts?

Official references

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

Apply it in practice

Savepoint and Rollback for Multi-Step Transaction

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