mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 02:15:12 +00:00
* Add Apex instructions * Readme update * Update Apex Doc format * Add escapeSingleQuote and batch good practice * Add elements * Edit description * Fix readme --------- Co-authored-by: bmaucote <benoit.maucotel@capgemini.com>
46 KiB
46 KiB
description, applyTo
| description | applyTo |
|---|---|
| Guidelines and best practices for Apex development on the Salesforce Platform | **/*.cls, **/*.trigger |
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
PascalCasefor class names. Name classes descriptively to reflect their purpose.- Controllers: suffix with
Controller(e.g.,AccountController) - Trigger Handlers: suffix with
TriggerHandler(e.g.,AccountTriggerHandler) - Service Classes: suffix with
Service(e.g.,AccountService) - Selector Classes: suffix with
Selector(e.g.,AccountSelector) - Test Classes: suffix with
Test(e.g.,AccountServiceTest) - Batch Classes: suffix with
Batch(e.g.,AccountCleanupBatch) - Queueable Classes: suffix with
Queueable(e.g.,EmailNotificationQueueable)
- Controllers: suffix with
-
Methods: Use
camelCasefor method names. Use verbs to indicate actions.- Good:
getActiveAccounts(),updateContactEmail(),deleteExpiredRecords() - Avoid abbreviations:
getAccs()→getAccounts()
- Good:
-
Variables: Use
camelCasefor variable names. Use descriptive names.- Good:
accountList,emailAddress,totalAmount - Avoid single letters except for loop counters:
a→account
- Good:
-
Constants: Use
UPPER_SNAKE_CASEfor constants.- Good:
MAX_BATCH_SIZE,DEFAULT_EMAIL_TEMPLATE,ERROR_MESSAGE_PREFIX
- Good:
-
Triggers: Name triggers as
ObjectName+ trigger event (e.g.,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<>,Map<>) to process multiple records efficiently.
// 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
Map<Id, SObject>constructor to quickly convert query results to a map. - 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
System.Limitsclass to check consumption before hitting 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
WITH SECURITY_ENFORCEDin SOQL queries to enforce field-level security. - Use
Security.stripInaccessible()to remove fields the user cannot access. - Implement
WITH SHARINGkeyword for classes that enforce sharing rules. - Use
WITHOUT SHARINGonly when necessary and document the reason. - Use
INHERITED SHARINGfor utility classes to inherit the calling context.
// 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,OwnerId, custom indexed fields). - Limit query results with
LIMITclause when appropriate. - Use
LIMIT 1when you only need one record. - Avoid
SELECT *- always specify required fields. - Use relationship queries to minimize the number of SOQL queries.
- Order queries by indexed fields when possible.
- Always use
String.escapeSingleQuotes()when using user input in SOQL queries to prevent SOQL injection attacks. - 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,Trigger.isInsert, etc.
// 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
isEmpty()- Check if collections are empty using built-in methods instead of size comparisons. - 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
String.isBlank()andString.isNotBlank()- Check for null or empty strings properly. - Use
String.valueOf()- Safely convert values to strings to avoid null pointer exceptions. - 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 - UseString.join()for better performance. - Use Collection methods - Leverage
List.clone(),Set.addAll(),Map.keySet()for cleaner code. - 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
privateby default - Only expose methods that need to be public. - Use
protectedfor methods that subclasses need to access. - Use
publiconly for APIs that other classes need to call. - Use
finalkeyword to prevent method override when appropriate. - Mark classes as
finalif they should not be extended.
// 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
callout:NamedCredentialsyntax for secure, deployable integrations. - Always check HTTP status codes and handle errors gracefully.
- Set appropriate timeouts to prevent long-running callouts.
- Use
Database.AllowsCalloutsinterface for Queueable and Batchable classes.
// 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
@AuraEnabled- Expose methods to Lightning Web Components and Aura Components.@AuraEnabled(cacheable=true)- Enable client-side caching for read-only methods.@InvocableMethod- Make methods callable from Flow and Process Builder.@InvocableVariable- Define input/output parameters for invocable methods.@TestVisible- Expose private members to test classes only.@SuppressWarnings('PMD.RuleName')- Suppress specific PMD warnings.@RemoteAction- Expose methods for Visualforce JavaScript remoting (legacy).@Future- Execute methods asynchronously.@Future(callout=true)- Allow HTTP callouts in future methods.
// 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
Database.Statefulto maintain state across batch executions (e.g., counters, aggregations). - Without
Database.Stateful, batch classes are stateless and instance variables reset between batches. - 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
Database.BatchableandSchedulablein the same class.
- Use Platform Events for event-driven architecture and decoupled integrations.
- Publish events using
EventBus.publish()for asynchronous, fire-and-forget communication. - 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
@TestSetupmethods to create test data shared across test methods. - Use
Test.startTest()andTest.stopTest()to reset governor limits. - Test positive scenarios, negative scenarios, and bulk scenarios (200+ records).
- Use
System.runAs()to test different user contexts and permissions. - Mock external callouts using
Test.setMock(). - Never use
@SeeAllData=true- always create test data in tests. - Use the
Assertclass methods for assertions instead of deprecatedSystem.assert*()methods. - 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
@authorand@datetags for tracking. - Include
@description,@param,@return, and@throwstags. - Include
@param,@return, and@throwstags only when applicable. - Do not use
@return voidfor methods that return nothing. - 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.
Cache.OrgPartition- Shared across all users and sessions in the org.Cache.SessionPartition- Specific to a user's session.- 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.