Awesome-omni-skill apex
Guidelines and best practices for Apex development on the Salesforce Platform Triggers on: **/*.cls, **/*.trigger
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/apex" ~/.claude/skills/diegosouzapw-awesome-omni-skill-apex-034f93 && rm -rf "$T"
manifest:
skills/development/apex/SKILL.mdsource content
Apex Development
General Instructions
- Always use the latest Apex features and best practices for the Salesforce Platform.
- Write clear and concise comments for each class and method, explaining the business logic and any complex operations.
- Handle edge cases and implement proper exception handling with meaningful error messages.
- Focus on bulkification - write code that handles collections of records, not single records.
- Be mindful of governor limits and design solutions that scale efficiently.
- Implement proper separation of concerns using service layers, domain classes, and selector classes.
- Document external dependencies, integration points, and their purposes in comments.
Naming Conventions
-
Classes: Use
for class names. Name classes descriptively to reflect their purpose.PascalCase- Controllers: suffix with
(e.g.,Controller
)AccountController - Trigger Handlers: suffix with
(e.g.,TriggerHandler
)AccountTriggerHandler - Service Classes: suffix with
(e.g.,Service
)AccountService - Selector Classes: suffix with
(e.g.,Selector
)AccountSelector - Test Classes: suffix with
(e.g.,Test
)AccountServiceTest - Batch Classes: suffix with
(e.g.,Batch
)AccountCleanupBatch - Queueable Classes: suffix with
(e.g.,Queueable
)EmailNotificationQueueable
- Controllers: suffix with
-
Methods: Use
for method names. Use verbs to indicate actions.camelCase- Good:
,getActiveAccounts()
,updateContactEmail()deleteExpiredRecords() - Avoid abbreviations:
→getAccs()getAccounts()
- Good:
-
Variables: Use
for variable names. Use descriptive names.camelCase- Good:
,accountList
,emailAddresstotalAmount - Avoid single letters except for loop counters:
→aaccount
- Good:
-
Constants: Use
for constants.UPPER_SNAKE_CASE- Good:
,MAX_BATCH_SIZE
,DEFAULT_EMAIL_TEMPLATEERROR_MESSAGE_PREFIX
- Good:
-
Triggers: Name triggers as
+ trigger event (e.g.,ObjectName
,AccountTrigger
)ContactTrigger
Best Practices
Bulkification
- Always write bulkified code - Design all code to handle collections of records, not individual records.
- Avoid SOQL queries and DML statements inside loops.
- Use collections (
,List<>
,Set<>
) to process multiple records efficiently.Map<>
// Good Example - Bulkified public static void updateAccountRating(List<Account> accounts) { for (Account acc : accounts) { if (acc.AnnualRevenue > 1000000) { acc.Rating = 'Hot'; } } update accounts; } // Bad Example - Not bulkified public static void updateAccountRating(Account account) { if (account.AnnualRevenue > 1000000) { account.Rating = 'Hot'; update account; // DML in a method designed for single records } }
Maps for O(1) Lookup
- Use Maps for efficient lookups - Convert lists to maps for O(1) constant-time lookups instead of O(n) list iterations.
- Use
constructor to quickly convert query results to a map.Map<Id, SObject> - Ideal for matching related records, lookups, and avoiding nested loops.
// Good Example - Using Map for O(1) lookup Map<Id, Account> accountMap = new Map<Id, Account>([ SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds ]); for (Contact con : contacts) { Account acc = accountMap.get(con.AccountId); if (acc != null) { con.Industry__c = acc.Industry; } } // Bad Example - Nested loop with O(n²) complexity List<Account> accounts = [SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds]; for (Contact con : contacts) { for (Account acc : accounts) { if (con.AccountId == acc.Id) { con.Industry__c = acc.Industry; break; } } } // Good Example - Map for grouping records Map<Id, List<Contact>> contactsByAccountId = new Map<Id, List<Contact>>(); for (Contact con : contacts) { if (!contactsByAccountId.containsKey(con.AccountId)) { contactsByAccountId.put(con.AccountId, new List<Contact>()); } contactsByAccountId.get(con.AccountId).add(con); }
Governor Limits
- Be aware of Salesforce governor limits: SOQL queries (100), DML statements (150), heap size (6MB), CPU time (10s).
- Monitor governor limits proactively using
class to check consumption before hitting limits.System.Limits - Use efficient SOQL queries with selective filters and appropriate indexes.
- Implement SOQL for loops for processing large data sets.
- Use Batch Apex for operations on large data volumes (>50,000 records).
- Leverage Platform Cache to reduce redundant SOQL queries.
// Good Example - SOQL for loop for large data sets public static void processLargeDataSet() { for (List<Account> accounts : [SELECT Id, Name FROM Account]) { // Process batch of 200 records processAccounts(accounts); } } // Good Example - Using WHERE clause to reduce query results List<Account> accounts = [SELECT Id, Name FROM Account WHERE IsActive__c = true LIMIT 200];
Security and Data Access
- Always check CRUD/FLS permissions before performing SOQL queries or DML operations.
- Use
in SOQL queries to enforce field-level security.WITH SECURITY_ENFORCED - Use
to remove fields the user cannot access.Security.stripInaccessible() - Implement
keyword for classes that enforce sharing rules.WITH SHARING - Use
only when necessary and document the reason.WITHOUT SHARING - Use
for utility classes to inherit the calling context.INHERITED SHARING
// Good Example - Checking CRUD and using stripInaccessible public with sharing class AccountService { public static List<Account> getAccounts() { if (!Schema.sObjectType.Account.isAccessible()) { throw new SecurityException('User does not have access to Account object'); } List<Account> accounts = [SELECT Id, Name, Industry FROM Account WITH SECURITY_ENFORCED]; SObjectAccessDecision decision = Security.stripInaccessible( AccessType.READABLE, accounts ); return decision.getRecords(); } } // Good Example - WITH SHARING for sharing rules public with sharing class AccountController { // This class enforces record-level sharing }
Exception Handling
- Always use try-catch blocks for DML operations and callouts.
- Create custom exception classes for specific error scenarios.
- Log exceptions appropriately for debugging and monitoring.
- Provide meaningful error messages to users.
// Good Example - Proper exception handling public class AccountService { public class AccountServiceException extends Exception {} public static void safeUpdate(List<Account> accounts) { try { if (!Schema.sObjectType.Account.isUpdateable()) { throw new AccountServiceException('User does not have permission to update accounts'); } update accounts; } catch (DmlException e) { System.debug(LoggingLevel.ERROR, 'DML Error: ' + e.getMessage()); throw new AccountServiceException('Failed to update accounts: ' + e.getMessage()); } } }
SOQL Best Practices
- Use selective queries with indexed fields (
,Id
,Name
, custom indexed fields).OwnerId - Limit query results with
clause when appropriate.LIMIT - Use
when you only need one record.LIMIT 1 - Avoid
- always specify required fields.SELECT * - Use relationship queries to minimize the number of SOQL queries.
- Order queries by indexed fields when possible.
- Always use
when using user input in SOQL queries to prevent SOQL injection attacks.String.escapeSingleQuotes() - Check query selectivity - Aim for >10% selectivity (filters reduce results to <10% of total records).
- Use Query Plan to verify query efficiency and index usage.
- Test queries with realistic data volumes to ensure performance.
// Good Example - Selective query with indexed fields List<Account> accounts = [ SELECT Id, Name, (SELECT Id, LastName FROM Contacts) FROM Account WHERE OwnerId = :UserInfo.getUserId() AND CreatedDate = THIS_MONTH LIMIT 100 ]; // Good Example - LIMIT 1 for single record Account account = [SELECT Id, Name FROM Account WHERE Name = 'Acme' LIMIT 1]; // Good Example - escapeSingleQuotes() to prevent SOQL injection String searchTerm = String.escapeSingleQuotes(userInput); List<Account> accounts = Database.query('SELECT Id, Name FROM Account WHERE Name LIKE \'%' + searchTerm + '%\''); // Bad Example - Direct user input without escaping (SECURITY RISK) List<Account> accounts = Database.query('SELECT Id, Name FROM Account WHERE Name LIKE \'%' + userInput + '%\''); // Good Example - Selective query with indexed fields (high selectivity) List<Account> accounts = [ SELECT Id, Name FROM Account WHERE OwnerId = :UserInfo.getUserId() AND CreatedDate = TODAY LIMIT 100 ]; // Bad Example - Non-selective query (scans entire table) List<Account> accounts = [ SELECT Id, Name FROM Account WHERE Description LIKE '%test%' // Non-indexed field ]; // Check query performance in Developer Console: // 1. Enable 'Use Query Plan' in Developer Console // 2. Run SOQL query and review 'Query Plan' tab // 3. Look for 'Index' usage vs 'TableScan' // 4. Ensure selectivity > 10% for optimal performance
Trigger Best Practices
- Use one trigger per object to maintain clarity and avoid conflicts.
- Implement trigger logic in handler classes, not directly in triggers.
- Use a trigger framework for consistent trigger management.
- Leverage trigger context variables:
,Trigger.new
,Trigger.old
,Trigger.newMap
.Trigger.oldMap - Check trigger context:
,Trigger.isBefore
,Trigger.isAfter
, etc.Trigger.isInsert
// Good Example - Trigger with handler pattern trigger AccountTrigger on Account (before insert, before update, after insert, after update) { new AccountTriggerHandler().run(); } // Handler Class public class AccountTriggerHandler extends TriggerHandler { private List<Account> newAccounts; private List<Account> oldAccounts; private Map<Id, Account> newAccountMap; private Map<Id, Account> oldAccountMap; public AccountTriggerHandler() { this.newAccounts = (List<Account>) Trigger.new; this.oldAccounts = (List<Account>) Trigger.old; this.newAccountMap = (Map<Id, Account>) Trigger.newMap; this.oldAccountMap = (Map<Id, Account>) Trigger.oldMap; } public override void beforeInsert() { AccountService.setDefaultValues(newAccounts); } public override void afterUpdate() { AccountService.handleRatingChange(newAccountMap, oldAccountMap); } }
Code Quality Best Practices
- Use
- Check if collections are empty using built-in methods instead of size comparisons.isEmpty() - Use Custom Labels - Store user-facing text in Custom Labels for internationalization and maintainability.
- Use Constants - Define constants for hardcoded values, error messages, and configuration values.
- Use
andString.isBlank()
- Check for null or empty strings properly.String.isNotBlank() - Use
- Safely convert values to strings to avoid null pointer exceptions.String.valueOf() - Use safe navigation operator
- Access properties and methods safely without null pointer exceptions.?. - Use null-coalescing operator
- Provide default values for null expressions.?? - Avoid using
for string concatenation in loops - Use+
for better performance.String.join() - Use Collection methods - Leverage
,List.clone()
,Set.addAll()
for cleaner code.Map.keySet() - Use ternary operators - For simple conditional assignments to improve readability.
- Use switch expressions - Modern alternative to if-else chains for better readability and performance.
- Use SObject clone methods - Properly clone SObjects when needed to avoid unintended references.
// Good Example - Switch expression (modern Apex) String rating = switch on account.AnnualRevenue { when 0 { 'Cold'; } when 1, 2, 3 { 'Warm'; } when else { 'Hot'; } }; // Good Example - Switch on SObjectType String objectLabel = switch on record { when Account a { 'Account: ' + a.Name; } when Contact c { 'Contact: ' + c.LastName; } when else { 'Unknown'; } }; // Bad Example - if-else chain String rating; if (account.AnnualRevenue == 0) { rating = 'Cold'; } else if (account.AnnualRevenue >= 1 && account.AnnualRevenue <= 3) { rating = 'Warm'; } else { rating = 'Hot'; } // Good Example - SObject clone methods Account original = new Account(Name = 'Acme', Industry = 'Technology'); // Shallow clone with ID and relationships Account clone1 = original.clone(true, true); // Shallow clone without ID or relationships Account clone2 = original.clone(false, false); // Deep clone with all relationships Account clone3 = original.deepClone(true, true, true); // Good Example - isEmpty() instead of size comparison if (accountList.isEmpty()) { System.debug('No accounts found'); } // Bad Example - size comparison if (accountList.size() == 0) { System.debug('No accounts found'); } // Good Example - Custom Labels for user-facing text final String ERROR_MESSAGE = System.Label.Account_Update_Error; final String SUCCESS_MESSAGE = System.Label.Account_Update_Success; // Bad Example - Hardcoded strings final String ERROR_MESSAGE = 'An error occurred while updating the account'; // Good Example - Constants for configuration values public class AccountService { private static final Integer MAX_RETRY_ATTEMPTS = 3; private static final String DEFAULT_INDUSTRY = 'Technology'; private static final String ERROR_PREFIX = 'AccountService Error: '; public static void processAccounts() { // Use constants if (retryCount > MAX_RETRY_ATTEMPTS) { throw new AccountServiceException(ERROR_PREFIX + 'Max retries exceeded'); } } } // Good Example - isBlank() for null and empty checks if (String.isBlank(account.Name)) { account.Name = DEFAULT_NAME; } // Bad Example - multiple null checks if (account.Name == null || account.Name == '') { account.Name = DEFAULT_NAME; } // Good Example - String.valueOf() for safe conversion String accountId = String.valueOf(account.Id); String revenue = String.valueOf(account.AnnualRevenue); // Good Example - Safe navigation operator (?.) String ownerName = account?.Owner?.Name; Integer contactCount = account?.Contacts?.size(); // Bad Example - Nested null checks String ownerName; if (account != null && account.Owner != null) { ownerName = account.Owner.Name; } // Good Example - Null-coalescing operator (??) String accountName = account?.Name ?? 'Unknown Account'; Integer revenue = account?.AnnualRevenue ?? 0; String industry = account?.Industry ?? DEFAULT_INDUSTRY; // Bad Example - Ternary with null check String accountName = account != null && account.Name != null ? account.Name : 'Unknown Account'; // Good Example - Combining ?. and ?? String email = contact?.Email ?? contact?.Account?.Owner?.Email ?? 'no-reply@example.com'; // Good Example - String concatenation in loops List<String> accountNames = new List<String>(); for (Account acc : accounts) { accountNames.add(acc.Name); } String result = String.join(accountNames, ', '); // Bad Example - String concatenation in loops String result = ''; for (Account acc : accounts) { result += acc.Name + ', '; // Poor performance } // Good Example - Ternary operator String status = isActive ? 'Active' : 'Inactive'; // Good Example - Collection methods List<Account> accountsCopy = accountList.clone(); Set<Id> accountIds = new Set<Id>(accountMap.keySet());
Recursion Prevention
- Use static variables to track recursive calls and prevent infinite loops.
- Implement a circuit breaker pattern to stop execution after a threshold.
- Document recursion limits and potential risks.
// Good Example - Recursion prevention with static variable public class AccountTriggerHandler extends TriggerHandler { private static Boolean hasRun = false; public override void afterUpdate() { if (!hasRun) { hasRun = true; AccountService.updateRelatedContacts(Trigger.newMap.keySet()); } } } // Good Example - Circuit breaker with counter public class OpportunityService { private static Integer recursionCount = 0; private static final Integer MAX_RECURSION_DEPTH = 5; public static void processOpportunity(Id oppId) { recursionCount++; if (recursionCount > MAX_RECURSION_DEPTH) { System.debug(LoggingLevel.ERROR, 'Max recursion depth exceeded'); return; } try { // Process opportunity logic } finally { recursionCount--; } } }
Method Visibility and Encapsulation
- Use
by default - Only expose methods that need to be public.private - Use
for methods that subclasses need to access.protected - Use
only for APIs that other classes need to call.public - Use
keyword to prevent method override when appropriate.final - Mark classes as
if they should not be extended.final
// Good Example - Proper encapsulation public class AccountService { // Public API public static void updateAccounts(List<Account> accounts) { validateAccounts(accounts); performUpdate(accounts); } // Private helper - not exposed private static void validateAccounts(List<Account> accounts) { for (Account acc : accounts) { if (String.isBlank(acc.Name)) { throw new IllegalArgumentException('Account name is required'); } } } // Private implementation - not exposed private static void performUpdate(List<Account> accounts) { update accounts; } } // Good Example - Final keyword to prevent extension public final class UtilityHelper { // Cannot be extended public static String formatCurrency(Decimal amount) { return '$' + amount.setScale(2); } } // Good Example - Final method to prevent override public virtual class BaseService { // Can be overridden public virtual void process() { // Implementation } // Cannot be overridden public final void validateInput() { // Critical validation that must not be changed } }
Design Patterns
- Service Layer Pattern: Encapsulate business logic in service classes.
- Circuit Breaker Pattern: Prevent repeated failures by stopping execution after threshold.
- Selector Pattern: Create dedicated classes for SOQL queries.
- Domain Layer Pattern: Implement domain classes for record-specific logic.
- Trigger Handler Pattern: Use a consistent framework for trigger management.
- Builder Pattern: Use for complex object construction.
- Strategy Pattern: For implementing different behaviors based on conditions.
// Good Example - Service Layer Pattern public class AccountService { public static void updateAccountRatings(Set<Id> accountIds) { List<Account> accounts = AccountSelector.selectByIds(accountIds); for (Account acc : accounts) { acc.Rating = calculateRating(acc); } update accounts; } private static String calculateRating(Account acc) { if (acc.AnnualRevenue > 1000000) { return 'Hot'; } else if (acc.AnnualRevenue > 500000) { return 'Warm'; } return 'Cold'; } } // Good Example - Circuit Breaker Pattern public class ExternalServiceCircuitBreaker { private static Integer failureCount = 0; private static final Integer FAILURE_THRESHOLD = 3; private static DateTime circuitOpenedTime; private static final Integer RETRY_TIMEOUT_MINUTES = 5; public static Boolean isCircuitOpen() { if (circuitOpenedTime != null) { // Check if retry timeout has passed if (DateTime.now() > circuitOpenedTime.addMinutes(RETRY_TIMEOUT_MINUTES)) { // Reset circuit failureCount = 0; circuitOpenedTime = null; return false; } return true; } return failureCount >= FAILURE_THRESHOLD; } public static void recordFailure() { failureCount++; if (failureCount >= FAILURE_THRESHOLD) { circuitOpenedTime = DateTime.now(); System.debug(LoggingLevel.ERROR, 'Circuit breaker opened due to failures'); } } public static void recordSuccess() { failureCount = 0; circuitOpenedTime = null; } public static HttpResponse makeCallout(String endpoint) { if (isCircuitOpen()) { throw new CircuitBreakerException('Circuit is open. Service unavailable.'); } try { HttpRequest req = new HttpRequest(); req.setEndpoint(endpoint); req.setMethod('GET'); HttpResponse res = new Http().send(req); if (res.getStatusCode() == 200) { recordSuccess(); } else { recordFailure(); } return res; } catch (Exception e) { recordFailure(); throw e; } } public class CircuitBreakerException extends Exception {} } // Good Example - Selector Pattern public class AccountSelector { public static List<Account> selectByIds(Set<Id> accountIds) { return [ SELECT Id, Name, AnnualRevenue, Rating FROM Account WHERE Id IN :accountIds WITH SECURITY_ENFORCED ]; } public static List<Account> selectActiveAccountsWithContacts() { return [ SELECT Id, Name, (SELECT Id, LastName FROM Contacts) FROM Account WHERE IsActive__c = true WITH SECURITY_ENFORCED ]; } }
Configuration Management
Custom Metadata Types vs Custom Settings
- Prefer Custom Metadata Types (CMT) for configuration data that can be deployed.
- Use Custom Settings for user-specific or org-specific data that varies by environment.
- CMT is packageable, deployable, and can be used in validation rules and formulas.
- Custom Settings support hierarchy (Org, Profile, User) but are not deployable.
// Good Example - Using Custom Metadata Type List<API_Configuration__mdt> configs = [ SELECT Endpoint__c, Timeout__c, Max_Retries__c FROM API_Configuration__mdt WHERE DeveloperName = 'Production_API' LIMIT 1 ]; if (!configs.isEmpty()) { String endpoint = configs[0].Endpoint__c; Integer timeout = Integer.valueOf(configs[0].Timeout__c); } // Good Example - Using Custom Settings (user-specific) User_Preferences__c prefs = User_Preferences__c.getInstance(UserInfo.getUserId()); Boolean darkMode = prefs.Dark_Mode_Enabled__c; // Good Example - Using Custom Settings (org-level) Org_Settings__c orgSettings = Org_Settings__c.getOrgDefaults(); Integer maxRecords = Integer.valueOf(orgSettings.Max_Records_Per_Query__c);
Named Credentials and HTTP Callouts
- Always use Named Credentials for external API endpoints and authentication.
- Avoid hardcoding URLs, tokens, or credentials in code.
- Use
syntax for secure, deployable integrations.callout:NamedCredential - Always check HTTP status codes and handle errors gracefully.
- Set appropriate timeouts to prevent long-running callouts.
- Use
interface for Queueable and Batchable classes.Database.AllowsCallouts
// Good Example - Using Named Credentials public class ExternalAPIService { private static final String NAMED_CREDENTIAL = 'callout:External_API'; private static final Integer TIMEOUT_MS = 120000; // 120 seconds public static Map<String, Object> getExternalData(String recordId) { HttpRequest req = new HttpRequest(); req.setEndpoint(NAMED_CREDENTIAL + '/api/records/' + recordId); req.setMethod('GET'); req.setTimeout(TIMEOUT_MS); req.setHeader('Content-Type', 'application/json'); try { Http http = new Http(); HttpResponse res = http.send(req); if (res.getStatusCode() == 200) { return (Map<String, Object>) JSON.deserializeUntyped(res.getBody()); } else if (res.getStatusCode() == 404) { throw new NotFoundException('Record not found: ' + recordId); } else if (res.getStatusCode() >= 500) { throw new ServiceUnavailableException('External service error: ' + res.getStatus()); } else { throw new CalloutException('Unexpected response: ' + res.getStatusCode()); } } catch (System.CalloutException e) { System.debug(LoggingLevel.ERROR, 'Callout failed: ' + e.getMessage()); throw new ExternalAPIException('Failed to retrieve data', e); } } public class ExternalAPIException extends Exception {} public class NotFoundException extends Exception {} public class ServiceUnavailableException extends Exception {} } // Good Example - POST request with JSON body public static String createExternalRecord(Map<String, Object> data) { HttpRequest req = new HttpRequest(); req.setEndpoint(NAMED_CREDENTIAL + '/api/records'); req.setMethod('POST'); req.setTimeout(TIMEOUT_MS); req.setHeader('Content-Type', 'application/json'); req.setBody(JSON.serialize(data)); HttpResponse res = new Http().send(req); if (res.getStatusCode() == 201) { Map<String, Object> result = (Map<String, Object>) JSON.deserializeUntyped(res.getBody()); return (String) result.get('id'); } else { throw new CalloutException('Failed to create record: ' + res.getStatus()); } }
Common Annotations
- Expose methods to Lightning Web Components and Aura Components.@AuraEnabled
- Enable client-side caching for read-only methods.@AuraEnabled(cacheable=true)
- Make methods callable from Flow and Process Builder.@InvocableMethod
- Define input/output parameters for invocable methods.@InvocableVariable
- Expose private members to test classes only.@TestVisible
- Suppress specific PMD warnings.@SuppressWarnings('PMD.RuleName')
- Expose methods for Visualforce JavaScript remoting (legacy).@RemoteAction
- Execute methods asynchronously.@Future
- Allow HTTP callouts in future methods.@Future(callout=true)
// Good Example - AuraEnabled for LWC public with sharing class AccountController { @AuraEnabled(cacheable=true) public static List<Account> getAccounts() { return [SELECT Id, Name FROM Account WITH SECURITY_ENFORCED LIMIT 10]; } @AuraEnabled public static void updateAccount(Id accountId, String newName) { Account acc = new Account(Id = accountId, Name = newName); update acc; } } // Good Example - InvocableMethod for Flow public class FlowActions { @InvocableMethod(label='Send Email Notification' description='Sends email to account owner') public static List<Result> sendNotification(List<Request> requests) { List<Result> results = new List<Result>(); for (Request req : requests) { Result result = new Result(); try { // Send email logic result.success = true; result.message = 'Email sent successfully'; } catch (Exception e) { result.success = false; result.message = e.getMessage(); } results.add(result); } return results; } public class Request { @InvocableVariable(required=true label='Account ID') public Id accountId; @InvocableVariable(label='Email Template') public String templateName; } public class Result { @InvocableVariable public Boolean success; @InvocableVariable public String message; } } // Good Example - TestVisible for testing private methods public class AccountService { @TestVisible private static Boolean validateAccountName(String name) { return String.isNotBlank(name) && name.length() > 3; } }
Asynchronous Apex
- Use @future methods for simple asynchronous operations and callouts.
- Use Queueable Apex for complex asynchronous operations that require chaining.
- Use Batch Apex for processing large data volumes (>50,000 records).
- Use
to maintain state across batch executions (e.g., counters, aggregations).Database.Stateful - Without
, batch classes are stateless and instance variables reset between batches.Database.Stateful - Be mindful of governor limits when using stateful batches.
- Use
- Use Scheduled Apex for recurring operations.
- Create a separate Schedulable class to schedule batch jobs.
- Never implement both
andDatabase.Batchable
in the same class.Schedulable
- Use Platform Events for event-driven architecture and decoupled integrations.
- Publish events using
for asynchronous, fire-and-forget communication.EventBus.publish() - Subscribe to events using triggers on platform event objects.
- Ideal for integrations, microservices, and cross-org communication.
- Publish events using
- Optimize batch size based on processing complexity and governor limits.
- Default batch size is 200, but can be adjusted from 1 to 2000.
- Smaller batches (50-100) for complex processing or callouts.
- Larger batches (200) for simple DML operations.
- Test with realistic data volumes to find optimal size.
// Good Example - Platform Events for decoupled communication public class OrderEventPublisher { public static void publishOrderCreated(List<Order> orders) { List<Order_Created__e> events = new List<Order_Created__e>(); for (Order ord : orders) { Order_Created__e event = new Order_Created__e( Order_Id__c = ord.Id, Order_Amount__c = ord.TotalAmount, Customer_Id__c = ord.AccountId ); events.add(event); } // Publish events List<Database.SaveResult> results = EventBus.publish(events); // Check for errors for (Database.SaveResult result : results) { if (!result.isSuccess()) { for (Database.Error error : result.getErrors()) { System.debug('Error publishing event: ' + error.getMessage()); } } } } } // Good Example - Platform Event Trigger (Subscriber) trigger OrderCreatedTrigger on Order_Created__e (after insert) { List<Task> tasksToCreate = new List<Task>(); for (Order_Created__e event : Trigger.new) { Task t = new Task( Subject = 'Follow up on order', WhatId = event.Order_Id__c, Priority = 'High' ); tasksToCreate.add(t); } if (!tasksToCreate.isEmpty()) { insert tasksToCreate; } } // Good Example - Batch size optimization based on complexity public class ComplexProcessingBatch implements Database.Batchable<SObject>, Database.AllowsCallouts { public Database.QueryLocator start(Database.BatchableContext bc) { return Database.getQueryLocator([ SELECT Id, Name FROM Account WHERE IsActive__c = true ]); } public void execute(Database.BatchableContext bc, List<Account> scope) { // Complex processing with callouts - use smaller batch size for (Account acc : scope) { // Make HTTP callout HttpResponse res = ExternalAPIService.getAccountData(acc.Id); // Process response } } public void finish(Database.BatchableContext bc) { System.debug('Batch completed'); } } // Execute with smaller batch size for callout-heavy processing Database.executeBatch(new ComplexProcessingBatch(), 50); // Good Example - Simple DML batch with default size public class SimpleDMLBatch implements Database.Batchable<SObject> { public Database.QueryLocator start(Database.BatchableContext bc) { return Database.getQueryLocator([ SELECT Id, Status__c FROM Order WHERE Status__c = 'Draft' ]); } public void execute(Database.BatchableContext bc, List<Order> scope) { for (Order ord : scope) { ord.Status__c = 'Pending'; } update scope; } public void finish(Database.BatchableContext bc) { System.debug('Batch completed'); } } // Execute with larger batch size for simple DML Database.executeBatch(new SimpleDMLBatch(), 200); // Good Example - Queueable Apex public class EmailNotificationQueueable implements Queueable, Database.AllowsCallouts { private List<Id> accountIds; public EmailNotificationQueueable(List<Id> accountIds) { this.accountIds = accountIds; } public void execute(QueueableContext context) { List<Account> accounts = [SELECT Id, Name, Email__c FROM Account WHERE Id IN :accountIds]; for (Account acc : accounts) { sendEmail(acc); } // Chain another job if needed if (hasMoreWork()) { System.enqueueJob(new AnotherQueueable()); } } private void sendEmail(Account acc) { // Email sending logic } private Boolean hasMoreWork() { return false; } } // Good Example - Stateless Batch Apex (default) public class AccountCleanupBatch implements Database.Batchable<SObject> { public Database.QueryLocator start(Database.BatchableContext bc) { return Database.getQueryLocator([ SELECT Id, Name FROM Account WHERE LastActivityDate < LAST_N_DAYS:365 ]); } public void execute(Database.BatchableContext bc, List<Account> scope) { delete scope; } public void finish(Database.BatchableContext bc) { System.debug('Batch completed'); } } // Good Example - Stateful Batch Apex (maintains state across batches) public class AccountStatsBatch implements Database.Batchable<SObject>, Database.Stateful { private Integer recordsProcessed = 0; private Integer totalRevenue = 0; public Database.QueryLocator start(Database.BatchableContext bc) { return Database.getQueryLocator([ SELECT Id, Name, AnnualRevenue FROM Account WHERE IsActive__c = true ]); } public void execute(Database.BatchableContext bc, List<Account> scope) { for (Account acc : scope) { recordsProcessed++; totalRevenue += (Integer) acc.AnnualRevenue; } } public void finish(Database.BatchableContext bc) { // State is maintained: recordsProcessed and totalRevenue retain their values System.debug('Total records processed: ' + recordsProcessed); System.debug('Total revenue: ' + totalRevenue); // Send summary email or create summary record } } // Good Example - Schedulable class to schedule a batch public class AccountCleanupScheduler implements Schedulable { public void execute(SchedulableContext sc) { // Execute the batch with batch size of 200 Database.executeBatch(new AccountCleanupBatch(), 200); } } // Schedule the batch to run daily at 2 AM // Execute this in Anonymous Apex or in setup code: // String cronExp = '0 0 2 * * ?'; // System.schedule('Daily Account Cleanup', cronExp, new AccountCleanupScheduler());
Testing
- Always achieve 100% code coverage for production code (minimum 75% required).
- Write meaningful tests that verify business logic, not just code coverage.
- Use
methods to create test data shared across test methods.@TestSetup - Use
andTest.startTest()
to reset governor limits.Test.stopTest() - Test positive scenarios, negative scenarios, and bulk scenarios (200+ records).
- Use
to test different user contexts and permissions.System.runAs() - Mock external callouts using
.Test.setMock() - Never use
- always create test data in tests.@SeeAllData=true - Use the
class methods for assertions instead of deprecatedAssert
methods.System.assert*() - Always add descriptive failure messages to assertions for clarity.
// Good Example - Comprehensive test class @IsTest private class AccountServiceTest { @TestSetup static void setupTestData() { List<Account> accounts = new List<Account>(); for (Integer i = 0; i < 200; i++) { accounts.add(new Account( Name = 'Test Account ' + i, AnnualRevenue = i * 10000 )); } insert accounts; } @IsTest static void testUpdateAccountRatings_Positive() { // Arrange List<Account> accounts = [SELECT Id FROM Account]; Set<Id> accountIds = new Map<Id, Account>(accounts).keySet(); // Act Test.startTest(); AccountService.updateAccountRatings(accountIds); Test.stopTest(); // Assert List<Account> updatedAccounts = [ SELECT Id, Rating FROM Account WHERE AnnualRevenue > 1000000 ]; for (Account acc : updatedAccounts) { Assert.areEqual('Hot', acc.Rating, 'Rating should be Hot for high revenue accounts'); } } @IsTest static void testUpdateAccountRatings_NoAccess() { // Create user with limited access User testUser = createTestUser(); List<Account> accounts = [SELECT Id FROM Account LIMIT 1]; Set<Id> accountIds = new Map<Id, Account>(accounts).keySet(); Test.startTest(); System.runAs(testUser) { try { AccountService.updateAccountRatings(accountIds); Assert.fail('Expected SecurityException'); } catch (SecurityException e) { Assert.isTrue(true, 'SecurityException thrown as expected'); } } Test.stopTest(); } @IsTest static void testBulkOperation() { List<Account> accounts = [SELECT Id FROM Account]; Set<Id> accountIds = new Map<Id, Account>(accounts).keySet(); Test.startTest(); AccountService.updateAccountRatings(accountIds); Test.stopTest(); List<Account> updatedAccounts = [SELECT Id, Rating FROM Account]; Assert.areEqual(200, updatedAccounts.size(), 'All accounts should be processed'); } private static User createTestUser() { Profile p = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1]; return new User( Alias = 'testuser', Email = 'testuser@test.com', EmailEncodingKey = 'UTF-8', LastName = 'Testing', LanguageLocaleKey = 'en_US', LocaleSidKey = 'en_US', ProfileId = p.Id, TimeZoneSidKey = 'America/Los_Angeles', UserName = 'testuser' + DateTime.now().getTime() + '@test.com' ); } }
Common Code Smells and Anti-Patterns
- DML/SOQL in loops - Always bulkify your code to avoid governor limit exceptions.
- Hardcoded IDs - Use custom settings, custom metadata, or dynamic queries instead.
- Deeply nested conditionals - Extract logic into separate methods for clarity.
- Large methods - Keep methods focused on a single responsibility (max 30-50 lines).
- Magic numbers - Use named constants for clarity and maintainability.
- Duplicate code - Extract common logic into reusable methods or classes.
- Missing null checks - Always validate input parameters and query results.
// Bad Example - DML in loop for (Account acc : accounts) { acc.Rating = 'Hot'; update acc; // AVOID: DML in loop } // Good Example - Bulkified DML for (Account acc : accounts) { acc.Rating = 'Hot'; } update accounts; // Bad Example - Hardcoded ID Account acc = [SELECT Id FROM Account WHERE Id = '001000000000001']; // Good Example - Dynamic query Account acc = [SELECT Id FROM Account WHERE Name = :accountName LIMIT 1]; // Bad Example - Magic number if (accounts.size() > 200) { // Process } // Good Example - Named constant private static final Integer MAX_BATCH_SIZE = 200; if (accounts.size() > MAX_BATCH_SIZE) { // Process }
Documentation and Comments
- Use JavaDoc-style comments for classes and methods.
- Include
and@author
tags for tracking.@date - Include
,@description
,@param
, and@return
tags.@throws - Include
,@param
, and@return
tags only when applicable.@throws - Do not use
for methods that return nothing.@return void - Document complex business logic and design decisions.
- Keep comments up-to-date with code changes.
/** * @author Your Name * @date 2025-01-01 * @description Service class for managing Account records */ public with sharing class AccountService { /** * @author Your Name * @date 2025-01-01 * @description Updates the rating for accounts based on annual revenue * @param accountIds Set of Account IDs to update * @throws AccountServiceException if user lacks update permissions */ public static void updateAccountRatings(Set<Id> accountIds) { // Implementation } }
Deployment and DevOps
- Use Salesforce CLI for source-driven development.
- Leverage scratch orgs for development and testing.
- Implement CI/CD pipelines using tools like Salesforce CLI, GitHub Actions, or Jenkins.
- Use unlocked packages for modular deployments.
- Run Apex tests as part of deployment validation.
- Use Salesforce Code Analyzer to scan code for quality and security issues.
# Salesforce CLI commands (sf) sf project deploy start # Deploy source to org sf project deploy start --dry-run # Validate deployment without deploying sf apex run test --test-level RunLocalTests # Run local Apex tests sf apex get test --test-run-id <id> # Get test results sf project retrieve start # Retrieve source from org # Salesforce Code Analyzer commands sf code-analyzer rules # List all available rules sf code-analyzer rules --rule-selector eslint:Recommended # List recommended ESLint rules sf code-analyzer rules --workspace ./force-app # List rules for specific workspace sf code-analyzer run # Run analysis with recommended rules sf code-analyzer run --rule-selector pmd:Recommended # Run PMD recommended rules sf code-analyzer run --rule-selector "Security" # Run rules with Security tag sf code-analyzer run --workspace ./force-app --target "**/*.cls" # Analyze Apex classes sf code-analyzer run --severity-threshold 3 # Run analysis with severity threshold sf code-analyzer run --output-file results.html # Output results to HTML file sf code-analyzer run --output-file results.csv # Output results to CSV file sf code-analyzer run --view detail # Show detailed violation information
Performance Optimization
- Use selective SOQL queries with indexed fields.
- Implement lazy loading for expensive operations.
- Use asynchronous processing for long-running operations.
- Monitor with Debug Logs and Event Monitoring.
- Use ApexGuru and Scale Center for performance insights.
Platform Cache
- Use Platform Cache to store frequently accessed data and reduce SOQL queries.
- Shared across all users and sessions in the org.Cache.OrgPartition
- Specific to a user's session.Cache.SessionPartition- Implement proper cache invalidation strategies.
- Handle cache misses gracefully with fallback to database queries.
// Good Example - Using Org Cache public class AccountCacheService { private static final String CACHE_PARTITION = 'local.AccountCache'; private static final Integer TTL_SECONDS = 3600; // 1 hour public static Account getAccount(Id accountId) { Cache.OrgPartition orgPart = Cache.Org.getPartition(CACHE_PARTITION); String cacheKey = 'Account_' + accountId; // Try to get from cache Account acc = (Account) orgPart.get(cacheKey); if (acc == null) { // Cache miss - query database acc = [ SELECT Id, Name, Industry, AnnualRevenue FROM Account WHERE Id = :accountId LIMIT 1 ]; // Store in cache with TTL orgPart.put(cacheKey, acc, TTL_SECONDS); } return acc; } public static void invalidateCache(Id accountId) { Cache.OrgPartition orgPart = Cache.Org.getPartition(CACHE_PARTITION); String cacheKey = 'Account_' + accountId; orgPart.remove(cacheKey); } } // Good Example - Using Session Cache public class UserPreferenceCache { private static final String CACHE_PARTITION = 'local.UserPrefs'; public static Map<String, Object> getUserPreferences() { Cache.SessionPartition sessionPart = Cache.Session.getPartition(CACHE_PARTITION); String cacheKey = 'UserPrefs_' + UserInfo.getUserId(); Map<String, Object> prefs = (Map<String, Object>) sessionPart.get(cacheKey); if (prefs == null) { // Load preferences from database or custom settings prefs = new Map<String, Object>{ 'theme' => 'dark', 'language' => 'en_US' }; sessionPart.put(cacheKey, prefs); } return prefs; } }
Build and Verification
- After adding or modifying code, verify the project continues to build successfully.
- Run all relevant Apex test classes to ensure no regressions.
- Use Salesforce CLI:
sf apex run test --test-level RunLocalTests - Ensure code coverage meets the minimum 75% requirement (aim for 100%).
- Use Salesforce Code Analyzer to check for code quality issues:
sf code-analyzer run --severity-threshold 2 - Review violations and address them before deployment.