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.
Golden Rule: Never put SOQL queries, DML statements, or callouts inside a loop. Always collect data first, then operate on it in bulk.
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.
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);
}
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.
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.
Test checklist: Test positive cases, negative cases, bulk operations (200+ records), user permissions, and boundary conditions.
@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.