
Introduction
In the ever-evolving world of Salesforce development, writing code that merely works isn’t enough. Apex developers are expected to write efficient, scalable, and maintainable code that aligns with the platform’s unique constraints and capabilities. Poorly optimized logic—like inefficient SOQL queries, excessive CPU usage, or repeated DML operations—can cause slowdowns, errors, and failed deployments.
To help developers navigate these challenges, Salesforce encourages adhering to well-established coding standards and best practices. This guide compiles essential practices every Salesforce developer should know and apply when working with Apex.
1. Respect Governor Limits
Salesforce enforces governor limits to ensure fair usage across its multi-tenant environment. These limits apply to things like SOQL queries, DML statements, and CPU time.
Wrong (SOQL inside loop):
for(Account acc : accList) {
Contact con = [SELECT Id FROM Contact WHERE AccountId = :acc.Id LIMIT 1];
}Correct (SOQL outside loop):
Map<Id, Contact> accountContactMap = new Map<Id, Contact>();
for(Contact con : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accIds]) {
accountContactMap.put(con.AccountId, con);
}
Pro Tip: Use Limits.getQueries() and Limits.getLimitQueries() during debugging to track SOQL usage.
2. Bulkify Your Code
Apex code should always be written to handle multiple records. Salesforce operations like triggers are inherently bulk-based and should never assume a single record context.
Wrong (non-bulkified):
trigger AccountTrigger on Account (before insert) {
Account acc = Trigger.new[0];
acc.Description = ‘Single record logic’;
}Correct (bulkified):
trigger AccountTrigger on Account (before insert, before update) {
for(Account acc : Trigger.new) {
if(acc.AnnualRevenue > 1000000) {
acc.Description = ‘High Revenue’;
}
}
}
Pro Tip: Always use Trigger.new or Trigger.old collections to ensure your code handles bulk operations.
3. Use Trigger Handler Patterns
Keep triggers clean and delegate logic to handler classes for better maintainability and testability.
Correct:
trigger ContactTrigger on Contact (before insert, before update) {
ContactTriggerHandler.handle(Trigger.new);
}public class ContactTriggerHandler {
public static void handle(List<Contact> contacts) {
for(Contact con : contacts) {
if(String.isBlank(con.Email)) {
con.Email = ‘[email protected]’;
}
}
}
}Wrong (all logic in trigger):
trigger ContactTrigger on Contact (before insert) {
for(Contact c : Trigger.new) {
if(c.Email == null) {
c.Email = ‘[email protected]’;
}
}
}
Pro Tip: Consider frameworks like fFlib for enterprise-grade trigger architecture.
4. Write Robust Test Classes
Salesforce requires 75% test coverage, but focus on quality over quantity. Your tests should cover all branches and edge cases.
Good Test Class:
@isTest
public class AccountTriggerTest {
@isTest
static void testHighRevenueDescription() {
Account acc = new Account(Name=’Test’, AnnualRevenue=2000000);
insert acc;Account fetched = [SELECT Description FROM Account WHERE Id = :acc.Id];
System.assertEquals(‘High Revenue’, fetched.Description);
}
}Bad Test (no assertions):
@isTest
public class BadTest {
@isTest
static void dummyTest() {
Account acc = new Account(Name=’Test’);
insert acc;
}
}
Pro Tip: Use Test.startTest() and Test.stopTest() for testing async logic.
5. Avoid Hardcoding Values
Hardcoded values reduce flexibility and are hard to maintain.
Wrong:
if (user.Profile.Name == ‘System Administrator’) {
// do something
}Correct (Custom Metadata or Label):
String adminProfileName = Label.Admin_Profile_Name;
if (user.Profile.Name == adminProfileName) {
// do something
}
Pro Tip: Use Custom Metadata Types for environment-specific values and feature toggles.
6. Prevent Recursive Triggers
Use static variables to prevent triggers from firing repeatedly.
Correct:
public class AccountTriggerHandler {
private static Boolean hasRun = false;public static void handle(List<Account> accList) {
if(hasRun) return;
hasRun = true;// Logic here
}
}Wrong (no prevention):
trigger AccountTrigger on Account (after update) {
// This might call an update again and re-fire the trigger
update Trigger.new;
}
Pro Tip: Use Trigger.isInsert, Trigger.isUpdate to control logic execution.
7. Offload Heavy Logic with Async Apex
Use async processing for long-running or callout-heavy logic.
Queueable Apex:
public class ProcessAccountsQueueable implements Queueable {
public void execute(QueueableContext context) {
// Heavy processing logic
}
}Wrong (heavy logic in trigger):
trigger HeavyLogicTrigger on Account (after insert) {
for(Account acc : Trigger.new) {
HttpRequest req = new HttpRequest();
// Callout inside trigger – not allowed
}
}
Pro Tip: Prefer Queueable over @future for job chaining and better error handling.
8. Log and Monitor Effectively
Basic Debug Log:
System.debug(‘Account Inserted: ‘ + acc.Id);
Custom Object Log:
Log__c log = new Log__c();
log.Message__c = ‘Critical operation executed’;
insert log;Wrong (no logging):
// Code fails silently, making it hard to debug
someMethodThatFails();
Pro Tip: Create dashboards on log objects or use Platform Events to monitor real-time errors.
Conclusion
Mastering Apex is not just about writing functional code it’s about writing efficient, clean, and scalable code that aligns with Salesforce’s platform best practices.
By following these principles:
– Respect governor limits
– Write bulk-safe logic
– Use handler classes for triggers
– Write meaningful test classes
– Avoid hardcoding values
– Prevent recursive logic
– Offload heavy processing
– Monitor using logs