home Homebuild Toolsbug_report Errorsmenu_book Guideslightbulb Tipssmart_toy Promptsextension Extensionsfolder_open Resourcesinfo About
search

Writing good Apex code means understanding the constraints of the Salesforce platform and designing your solutions around them. This guide covers the essential patterns every Salesforce developer should follow.

Bulkification

Bulkification is the single most important Apex best practice. Every piece of Apex code should be designed to handle multiple records in a single transaction, not just one.

lightbulb

Golden Rule: Never put SOQL queries, DML statements, or callouts inside a loop. Always collect data first, then operate on it in bulk.

Apex — Bulkified Trigger
trigger AccountTrigger on Account (before insert, before update) {
    // 1. Collect all data needed
    Set<String> industries = new Set<String>();
    for (Account acc : Trigger.new) {
        if (acc.Industry != null) {
            industries.add(acc.Industry);
        }
    }

    // 2. Single query outside the loop
    Map<String, Decimal> benchmarks = new Map<String, Decimal>();
    for (Industry_Benchmark__c b : [
        SELECT Industry__c, Target_Revenue__c 
        FROM Industry_Benchmark__c
        WHERE Industry__c IN :industries
    ]) {
        benchmarks.put(b.Industry__c, b.Target_Revenue__c);
    }

    // 3. Process records using the Map
    for (Account acc : Trigger.new) {
        if (benchmarks.containsKey(acc.Industry)) {
            acc.Revenue_Target__c = benchmarks.get(acc.Industry);
        }
    }
}

Trigger Handler Pattern

Keep your trigger files thin — one trigger per object with all logic delegated to a handler class. This makes your code testable, maintainable, and easy to extend.

Apex — Trigger
trigger AccountTrigger on Account (
    before insert, before update, 
    after insert, after update
) {
    AccountTriggerHandler handler = new AccountTriggerHandler();
    
    if (Trigger.isBefore && Trigger.isInsert) handler.beforeInsert(Trigger.new);
    if (Trigger.isBefore && Trigger.isUpdate) handler.beforeUpdate(Trigger.new, Trigger.oldMap);
    if (Trigger.isAfter && Trigger.isInsert) handler.afterInsert(Trigger.new);
    if (Trigger.isAfter && Trigger.isUpdate) handler.afterUpdate(Trigger.new, Trigger.oldMap);
}
Apex — Handler Class
public class AccountTriggerHandler {
    
    public void beforeInsert(List<Account> newAccounts) {
        setDefaults(newAccounts);
    }
    
    public void beforeUpdate(
        List<Account> newAccounts, 
        Map<Id, Account> oldMap
    ) {
        validateChanges(newAccounts, oldMap);
    }
    
    private void setDefaults(List<Account> accounts) {
        for (Account acc : accounts) {
            if (acc.Rating == null) {
                acc.Rating = 'Cold';
            }
        }
    }
    
    private void validateChanges(
        List<Account> newAccounts, 
        Map<Id, Account> oldMap
    ) {
        for (Account acc : newAccounts) {
            Account old = oldMap.get(acc.Id);
            if (acc.AnnualRevenue != old.AnnualRevenue 
                && acc.AnnualRevenue < 0) {
                acc.addError('Revenue cannot be negative');
            }
        }
    }
}

Error Handling

Use structured error handling to make your code resilient and debuggable.

Apex
public class AccountService {
    
    public static List<Database.SaveResult> safeUpdate(
        List<Account> accounts
    ) {
        List<Database.SaveResult> results = 
            Database.update(accounts, false);
        
        for (Integer i = 0; i < results.size(); i++) {
            if (!results[i].isSuccess()) {
                for (Database.Error err : results[i].getErrors()) {
                    System.debug(LoggingLevel.ERROR,
                        'Failed: ' + accounts[i].Name + 
                        ' - ' + err.getMessage()
                    );
                }
            }
        }
        return results;
    }
}

Testing Strategies

Good tests are not just about hitting 75% coverage — they validate behavior, edge cases, and bulk scenarios.

rule

Test checklist: Test positive cases, negative cases, bulk operations (200+ records), user permissions, and boundary conditions.

Apex — Test Class
@isTest
private class AccountTriggerHandlerTest {
    
    @TestSetup
    static void makeData() {
        List<Account> accounts = new List<Account>();
        for (Integer i = 0; i < 200; i++) {
            accounts.add(new Account(
                Name = 'Test Account ' + i
            ));
        }
        insert accounts;
    }
    
    @isTest
    static void testDefaultRating_bulk() {
        List<Account> accounts = [
            SELECT Id, Rating 
            FROM Account
        ];
        
        System.assertEquals(200, accounts.size(),
            'Should handle 200 records');
        
        for (Account acc : accounts) {
            System.assertEquals('Cold', acc.Rating,
                'Default rating should be Cold');
        }
    }
    
    @isTest
    static void testNegativeRevenue_showsError() {
        Account acc = [SELECT Id FROM Account LIMIT 1];
        acc.AnnualRevenue = -500;
        
        Test.startTest();
        Database.SaveResult result = Database.update(acc, false);
        Test.stopTest();
        
        System.assert(!result.isSuccess(),
            'Negative revenue should fail');
    }
}

Governor Limit Awareness

Always design with limits in mind. Use the Limits class to monitor consumption.

100
SOQL queries (sync)
150
DML statements
6 MB
Heap size (sync)
10s
CPU time (sync)