Table of Contents

Have you ever created an Apex trigger that passed all tests but still you encountered the issues once it was deployed? Well, problems like “Too many SOQL queries: 101” or “CPU time limit exceeded” usually arise when the Salesforce developers overlook best practices. These challenges can be quite frustrating and may disrupt essential business processes.

This is why following Salesforce Apex best practices is so important. They help you write clean, efficient, and scalable code that runs smoothly within Salesforce’s strict limits. In this blog, we’ll go through the 15 Apex best practices every developer should know, with simple explanations and code examples, so you can avoid these mistakes and build reliable applications.

15 Salesforce Apex Best Practices You Must Follow

Let’s dive into the Salesforce Apex Best Practices that every developer should know.

1. One Trigger Per Object

Always follow the golden rule: one trigger per object. This ensures all logic is consolidated in one place, avoids duplicate execution, and keeps your Salesforce trigger management clean. Use handler classes to delegate logic, keeping your trigger lean and maintainable.

Every Salesforce object should have a single trigger. Multiple triggers on the same object make debugging difficult and can cause unpredictable execution order. Instead, use a handler class to organize the logic.

Example:

trigger AccountTrigger on Account (before insert, before update) { AccountTriggerHandler.handle(Trigger.new, Trigger.oldMap, Trigger.isInsert, Trigger.isUpdate); } public class AccountTriggerHandler { public static void handle(List newList, Map oldMap, Boolean isInsert, Boolean isUpdate) { if(isInsert){ // Insert logic here } if(isUpdate){ // Update logic here } } }

Here, we can observe that the trigger is just a gateway, while the heavy lifting is managed in the handler.

2. Bulkify Your Code

In Salesforce, operations can run on multiple records at once. That’s why bulkification is essential. Apex executes in bulk, which means your code should handle hundreds of records at once. Writing code for single records will almost always lead to governor limit errors in real-world scenarios.

Bad Code (not bulkified):

for(Account acc : Trigger.new){ Contact con = new Contact(LastName = 'Test', AccountId = acc.Id); insert con; // DML inside loop }

Good Code (bulkified):

List<Contact> contactsToInsert = new List<Contact>(); for(Account acc : Trigger.new){ contactsToInsert.add(new Contact(LastName = 'Test', AccountId = acc.Id)); } insert contactsToInsert; // Single DML

Bulkification ensures your Salesforce triggers work efficiently for both small and large datasets.

3. Avoid SOQL and DML Inside Loops

Placing queries or DML statements inside loops is one of the most common mistakes in Salesforce Apex development. It consumes governor limits quickly.

Bad Code:

for(Account acc : Trigger.new){ List cons = [SELECT Id FROM Contact WHERE AccountId = :acc.Id]; // SOQL inside loop }

Good Code:

Set<Id> accountIds = new Set<Id>(); for(Account acc : Trigger.new){ accountIds.add(acc.Id); } List<Contact> cons = [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]; // Single SOQL

4. Use Collections and Maps Effectively

When handling related data, Maps and Sets are your best friends. They allow fast lookups and reduce nested loops. For example, when updating associated records, use a Map with record IDs as keys to avoid multiple SOQL queries. Maps and Sets make your code cleaner, faster, and reduce nested loops. They’re essential for bulkified flows.

Example:

Map<Id, Account> accountMap = new Map<Id, Account>( [SELECT Id, Name FROM Account WHERE Id IN :accountIds] ); for(Contact con : contacts) { if(accountMap.containsKey(con.AccountId)) { con.Description = 'Linked to: ' + accountMap.get(con.AccountId).Name; } }

This makes lookups instant, without looping over lists.

5. Write Reusable Apex Methods

Avoid repetitive code by creating reusable Apex methods in utility classes. This makes your codebase modular, easier to test, and simpler to maintain in the long run. Also try to never repeat logic because it makes Salesforce Apex development easier to maintain.

Example:

public class AccountUtils { public static List<Account> getActiveAccounts() { return [SELECT Id, Name FROM Account WHERE IsActive__c = true]; } }

6. Respect the Governor’s Limits

Every Salesforce Apex developer must be aware of and respect governor limits. From SOQL queries (100 per transaction) to CPU timeouts (10,000 ms),(this is synchronous execution of code ), these limits are enforced at runtime. Efficient coding ensures you never exceed these, keeping your apps stable.

Salesforce enforces governor limits to ensure efficient resource usage. Always keep an eye on them during development.

Example:

System.debug('Total Queries: ' + Limits.getQueries()); System.debug('Total DML Statements: ' + Limits.getDMLStatements());

Understanding limits is key to writing sustainable Salesforce Apex code.

7. Implement Proper Error Handling

Wrap your logic in try-catch blocks and provide meaningful error messages. Logging exceptions (using tools like Apex Log Analyzer) helps with debugging and ensures a better experience for end users.

Example:

try { insert new Account(Name = 'Test Account'); } catch (DmlException e) { System.debug('Error inserting account: ' + e.getMessage()); }

8. Write Test Classes the Right Way

Salesforce requires at least 90% plus code coverage for deployment, but the focus should be on quality test cases, not just coverage numbers. Write tests that cover positive, negative, and bulk scenarios, and use @testSetup methods for reusable test data.

Example:

@isTest private class AccountTest { @testSetup static void setupData() { insert new Account(Name = 'Setup Account'); } @isTest static void testAccountUpdate() { Account acc = [SELECT Id, Name FROM Account WHERE Name = 'Setup Account' LIMIT 1]; acc.Name = 'Test Setup Updated'; update acc; Account updatedAcc = [SELECT Name FROM Account WHERE Id = :acc.Id LIMIT 1]; System.assertEquals('Test Setup Updated', updatedAcc.Name); } @isTest static void testAccountDelete() { Account acc = [SELECT Id, Name FROM Account WHERE Name = 'Setup Account' LIMIT 1]; try { delete acc; System.assert(true, 'Records Successfully Deleted'); } catch (Exception e) { System.assert(false, 'Record not deleted. Error Message => ' + e.getMessage()); } } }

This not only validates logic but also improves code coverage.

9. Use Custom Settings and Custom Metadata

Hardcoding values is one of the biggest mistakes in Salesforce Apex development. Instead, use Custom Settings or Custom Metadata Types to store configurable values, like record types, limits, or API endpoints. This makes updates easier without requiring code modifications.

Example: 

String endpoint = IntegrationSettings__mdt.getInstance('Default').API_Endpoint__c;

This practice also allows changes without modifying the code.

10. Keep Triggers Logic-Free

Triggers should only handle events and delegate logic to handler classes. This separation of concerns improves readability, testing, and maintainability. Following a trigger framework ensures a consistent structure.

Example:

trigger ContactTrigger on Contact (after insert) { if (Trigger.isInsert && Trigger.isAfter) { ContactHandler.afterInsert(Trigger.new); } }

This improves readability and maintenance.

11. Use Asynchronous Apex When Needed

For long-running tasks, such as callouts, batch updates, or integrations, use Queueable, Future, or Batch Apex. This ensures smoother execution and prevents governor limit violations during synchronous transactions.

Example (Future Method):

@future public static void updateAccountsAsync(List accountIds) { List accounts = [SELECT Id, Name, Description FROM Account WHERE Id IN :accountIds]; for (Account acc : accounts) { acc.Description = 'Updated asynchronously'; } update accounts; }

This ensures smooth execution without blocking processes.

12. Optimize SOQL Queries

Use selective queries, indexed fields, and filters like WHERE clauses instead of retrieving all records. A well-written SOQL query improves performance and reduces unnecessary data retrieval.

Bad Code:

List accounts = [SELECT Id, Name, (SELECT Id FROM Contacts) FROM Account]; // Heavy query

Good Code:

List accounts = [SELECT Id, Name FROM Account WHERE CreatedDate = TODAY]; //

Optimized queries improve performance and stay within limits.

13. Follow Naming Conventions

Clear naming conventions (camelCase for variables, PascalCase for classes) make your code professional and readable. For example, AccountHandler is more meaningful than AccHand.

Bad Code:

public class AccHand {} // unclear

Good Code:

public class AccountHandler {} // clear

Good naming conventions help teams collaborate better.

14. Use “With Sharing” Keyword Wisely

Consistently enforce Salesforce’s record-level security model by using “with sharing” in your classes unless you specifically need system context. This respects users’ permission sets and sharing rules.

Example:

public with sharing class OpportunityService { public static void updateOpportunityStage(Id oppId, String stage) { Opportunity opp = [SELECT Id, StageName FROM Opportunity WHERE Id = :oppId]; opp.StageName = stage; // Corrected field name update opp; } }

15. Document Your Code

Future developers will thank you for leaving meaningful comments because well-documented code is easier for future developers to understand. Explain why you made confident choices, especially for complex logic. Proper documentation ensures smoother handovers and long-term maintainability.

Example:

// Return only those active accounts which have any opportunities record public static List getActiveAccountsWithOpps() { return [ SELECT Id, Name FROM Account WHERE IsActive__c = true AND Id IN (SELECT AccountId FROM Opportunity) ]; }

Documentation reduces confusion in long-term projects.

Wrapping Up

Understanding and implementing these best practices will change how you write in Salesforce Apex. These practices, such as bulkification, avoiding SOQL within for loops, as well as optimal usage of asynchronous processing and writing proper test classes, enable you to write efficient, scalable, and future-proof code.

These best practices are a must for any Salesforce developer. They become “survival skills” that allow you to thrive in the ecosystem. If applied continuously, they eliminate the tendency for common Apex governor limit issues, improve performance, and ultimately help manage your org better.

In summary, don’t just write Apex triggers and classes “to make it happen”, write them to “make it last”.

Latest Salesforce Insights