--- description: 'Guidelines and best practices for Apex development on the Salesforce Platform' applyTo: '**/*.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 `PascalCase` for 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`) - **Methods**: Use `camelCase` for method names. Use verbs to indicate actions. - Good: `getActiveAccounts()`, `updateContactEmail()`, `deleteExpiredRecords()` - Avoid abbreviations: `getAccs()` → `getAccounts()` - **Variables**: Use `camelCase` for variable names. Use descriptive names. - Good: `accountList`, `emailAddress`, `totalAmount` - Avoid single letters except for loop counters: `a` → `account` - **Constants**: Use `UPPER_SNAKE_CASE` for constants. - Good: `MAX_BATCH_SIZE`, `DEFAULT_EMAIL_TEMPLATE`, `ERROR_MESSAGE_PREFIX` - **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. ```apex // Good Example - Bulkified public static void updateAccountRating(List 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` constructor to quickly convert query results to a map. - Ideal for matching related records, lookups, and avoiding nested loops. ```apex // Good Example - Using Map for O(1) lookup Map accountMap = new Map([ 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 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> contactsByAccountId = new Map>(); for (Contact con : contacts) { if (!contactsByAccountId.containsKey(con.AccountId)) { contactsByAccountId.put(con.AccountId, new List()); } 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.Limits` class 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. ```apex // Good Example - SOQL for loop for large data sets public static void processLargeDataSet() { for (List accounts : [SELECT Id, Name FROM Account]) { // Process batch of 200 records processAccounts(accounts); } } // Good Example - Using WHERE clause to reduce query results List 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_ENFORCED` in SOQL queries to enforce field-level security. - Use `Security.stripInaccessible()` to remove fields the user cannot access. - Implement `WITH SHARING` keyword for classes that enforce sharing rules. - Use `WITHOUT SHARING` only when necessary and document the reason. - Use `INHERITED SHARING` for utility classes to inherit the calling context. ```apex // Good Example - Checking CRUD and using stripInaccessible public with sharing class AccountService { public static List getAccounts() { if (!Schema.sObjectType.Account.isAccessible()) { throw new SecurityException('User does not have access to Account object'); } List 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. ```apex // Good Example - Proper exception handling public class AccountService { public class AccountServiceException extends Exception {} public static void safeUpdate(List 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 `LIMIT` clause when appropriate. - Use `LIMIT 1` when 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. ```apex // Good Example - Selective query with indexed fields List 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 accounts = Database.query('SELECT Id, Name FROM Account WHERE Name LIKE \'%' + searchTerm + '%\''); // Bad Example - Direct user input without escaping (SECURITY RISK) List accounts = Database.query('SELECT Id, Name FROM Account WHERE Name LIKE \'%' + userInput + '%\''); // Good Example - Selective query with indexed fields (high selectivity) List accounts = [ SELECT Id, Name FROM Account WHERE OwnerId = :UserInfo.getUserId() AND CreatedDate = TODAY LIMIT 100 ]; // Bad Example - Non-selective query (scans entire table) List 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. ```apex // 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 newAccounts; private List oldAccounts; private Map newAccountMap; private Map oldAccountMap; public AccountTriggerHandler() { this.newAccounts = (List) Trigger.new; this.oldAccounts = (List) Trigger.old; this.newAccountMap = (Map) Trigger.newMap; this.oldAccountMap = (Map) 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()` and `String.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** - Use `String.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. ```apex // 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 accountNames = new List(); 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 accountsCopy = accountList.clone(); Set accountIds = new Set(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. ```apex // 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 `private` by default** - Only expose methods that need to be public. - Use `protected` for methods that subclasses need to access. - Use `public` only for APIs that other classes need to call. - **Use `final` keyword** to prevent method override when appropriate. - Mark classes as `final` if they should not be extended. ```apex // Good Example - Proper encapsulation public class AccountService { // Public API public static void updateAccounts(List accounts) { validateAccounts(accounts); performUpdate(accounts); } // Private helper - not exposed private static void validateAccounts(List 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 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. ```apex // Good Example - Service Layer Pattern public class AccountService { public static void updateAccountRatings(Set accountIds) { List 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 selectByIds(Set accountIds) { return [ SELECT Id, Name, AnnualRevenue, Rating FROM Account WHERE Id IN :accountIds WITH SECURITY_ENFORCED ]; } public static List 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. ```apex // Good Example - Using Custom Metadata Type List 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:NamedCredential` syntax for secure, deployable integrations. - **Always check HTTP status codes** and handle errors gracefully. - Set appropriate timeouts to prevent long-running callouts. - Use `Database.AllowsCallouts` interface for Queueable and Batchable classes. ```apex // 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 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) 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 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 result = (Map) 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. ```apex // Good Example - AuraEnabled for LWC public with sharing class AccountController { @AuraEnabled(cacheable=true) public static List 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 sendNotification(List requests) { List results = new List(); 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.Stateful` to 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 **Scheduled Apex** for recurring operations. - Create a separate **Schedulable class** to schedule batch jobs. - Never implement both `Database.Batchable` and `Schedulable` in 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. - **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. ```apex // Good Example - Platform Events for decoupled communication public class OrderEventPublisher { public static void publishOrderCreated(List orders) { List events = new List(); 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 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 tasksToCreate = new List(); 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, 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 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 { 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 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 accountIds; public EmailNotificationQueueable(List accountIds) { this.accountIds = accountIds; } public void execute(QueueableContext context) { List 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 { 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 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, 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 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 `@TestSetup` methods to create test data shared across test methods. - Use `Test.startTest()` and `Test.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 `Assert` class methods** for assertions instead of deprecated `System.assert*()` methods. - Always add descriptive failure messages to assertions for clarity. ```apex // Good Example - Comprehensive test class @IsTest private class AccountServiceTest { @TestSetup static void setupTestData() { List accounts = new List(); 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 accounts = [SELECT Id FROM Account]; Set accountIds = new Map(accounts).keySet(); // Act Test.startTest(); AccountService.updateAccountRatings(accountIds); Test.stopTest(); // Assert List 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 accounts = [SELECT Id FROM Account LIMIT 1]; Set accountIds = new Map(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 accounts = [SELECT Id FROM Account]; Set accountIds = new Map(accounts).keySet(); Test.startTest(); AccountService.updateAccountRatings(accountIds); Test.stopTest(); List 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. ```apex // 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 `@author` and `@date` tags for tracking. - Include `@description`, `@param`, `@return`, and `@throws` tags. - Include `@param`, `@return`, and `@throws` tags **only** when applicable. - Do not use `@return void` for methods that return nothing. - Document complex business logic and design decisions. - Keep comments up-to-date with code changes. ```apex /** * @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 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. ```bash # 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 # 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. ```apex // 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 getUserPreferences() { Cache.SessionPartition sessionPart = Cache.Session.getPartition(CACHE_PARTITION); String cacheKey = 'UserPrefs_' + UserInfo.getUserId(); Map prefs = (Map) sessionPart.get(cacheKey); if (prefs == null) { // Load preferences from database or custom settings prefs = new Map{ '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.