5 Apex Best Practices Every Salesforce Developer Should Follow
Learn the essential Apex best practices that will help you write cleaner, more efficient, and scalable code in Salesforce. From bulkification to proper error handling.

As a Salesforce Architect with over 10 years of experience, I've seen countless Apex implementations, some elegant, others not so much. In this post, I'll share the five most important best practices that every Salesforce developer should follow to build scalable and robust applications.
1. Always Bulkify Your Code
Governor limits in Salesforce are designed to ensure fair use of shared resources. The most common mistake I see is writing code that works for a single record but fails when processing multiple records. Code that isn't bulkified will eventually break in production.
// ❌ Bad: Query inside a loop
for (Account acc : Trigger.new) {
List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
}
// ✅ Good: Collect IDs first, then query once
Set<Id> accountIds = new Set<Id>();
for (Account acc : Trigger.new) {
accountIds.add(acc.Id);
}
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
if (!contactsByAccount.containsKey(c.AccountId)) {
contactsByAccount.put(c.AccountId, new List<Contact>());
}
contactsByAccount.get(c.AccountId).add(c);
}
2. Use Maps for Efficient Data Access
Maps are your best friend when you need to access records by their ID or any unique field. They provide O(1) lookup time which is significantly faster than iterating through a list (O(n)) to find a matching record.
Map<Id, Account> accountMap = new Map<Id, Account>(
[SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds]
);
for (Opportunity opp : opportunities) {
// Instant access without looping through accounts
Account relatedAccount = accountMap.get(opp.AccountId);
if (relatedAccount != null) {
// Process with account data
}
}
3. Implement Proper Error Handling
Never let exceptions go unhandled. Use try-catch blocks strategically and ensure you handle specific exceptions like DmlException rather than just generic Exception.
public class AccountService {
public static void updateAccounts(List<Account> accounts) {
try {
update accounts;
} catch (DmlException e) {
// Handle specific DML errors for partial success or reporting
for (Integer i = 0; i < e.getNumDml(); i++) {
accounts[e.getDmlIndex(i)].addError(
'Failed to update: ' + e.getDmlMessage(i)
);
}
}
}
}
4. Separate Concerns with Handler Classes
Keep your triggers clean by delegating logic to handler classes. Triggers should only be responsible for dispatching to the correct handler method based on the operation context. This makes your code more testable, reusable, and maintainable.
// Trigger is minimal
trigger AccountTrigger on Account (before insert, before update, after insert) {
AccountTriggerHandler.handle(Trigger.new, Trigger.oldMap, Trigger.operationType);
}
// Helper class contains the actual logic
public class AccountTriggerHandler {
public static void handle(List<Account> newList, Map<Id, Account> oldMap, TriggerOperation operation) {
switch on operation {
when BEFORE_INSERT {
validateAccounts(newList);
}
when BEFORE_UPDATE {
handleFieldChanges(newList, oldMap);
}
when AFTER_INSERT {
createRelatedRecords(newList);
}
}
}
}
5. Write Meaningful Tests
Tests should cover not just code coverage but actual business logic. Always test:
- Bulk scenarios (200+ records)
- Positive use cases (expected behavior)
- Negative use cases (error handling)
- User permissions (runAs)
@isTest
private class AccountServiceTest {
@TestSetup
static void setup() {
// Create enough data to test limits
List<Account> accounts = TestDataFactory.createAccounts(200);
insert accounts;
}
@isTest
static void testBulkUpdate() {
List<Account> accounts = [SELECT Id, Name FROM Account];
Test.startTest();
AccountService.updateAccounts(accounts);
Test.stopTest();
List<Account> updatedAccounts = [SELECT Id, LastModifiedDate FROM Account];
System.assertEquals(200, updatedAccounts.size(), 'All accounts should be updated');
}
}
Conclusion
Following these best practices will help you build more robust Salesforce solutions that can scale with your business. Remember: code that works for one record should work for thousands.
Need help with Salesforce?

I help businesses design and build scalable Salesforce solutions.