Apex Coding Standards
Author: callinj
Name: Joe Callin
Title: Sr. Salesforce Developer
Email: joe@jcallin.dev
Table of Contents
- Introduction
- Naming Conventions
- Code Organization
- Null Handling and Safe Navigation
- Error Handling
- Security
- Method Visibility and Structure
- Frontend Integration
- Documentation
- Code Style and Formatting
- Code Complexity
- Performance
- Testing Standards
- Logging and Debugging
- Code Maintenance
- Code Analyzer Compliance
Introduction
Purpose
This document establishes comprehensive coding standards and best practices for Apex development within the Salesforce component library. These standards ensure consistency, maintainability, security, and quality across all Apex code.
Scope
These standards apply to all Apex classes, triggers, and related code within the component library. All code must comply with these standards and pass Salesforce Code Analyzer validation using the project's ruleset.
Related Documentation
- Code Analyzer Compliance - Detailed Code Analyzer rules and compliance requirements
- Testing Standards - Test coverage and testing patterns
- Git Branching Strategy - Git branching and merge strategy for the repository
- Component Library Modernization Plan - Overall project modernization strategy
Naming Conventions
Classes
-
Format:
SFC_ClassNameInPascalCase -
Naming: Project abbreviation prefix followed by underscore, then descriptive PascalCase name
-
Pattern:
SFC_<DescriptiveClassName> -
Examples:
public class SFC_QueryBuilderService { } public class SFC_AccountTriggerHandler { } public class SFC_PermissionUtils { } public class SFC_RosterMemberSvc { }
Methods
-
Format: camelCase
-
Naming: Verb-based names that describe the action performed
-
Examples:
public static List<Account> getAccounts() { } public void processRecords(List<SObject> records) { } private Boolean validateInput(String input) { }
Variables
-
Format: camelCase
-
Naming: Descriptive names that clearly indicate what the variable holds
-
Meaningful Names Required: Variable names must be meaningful and self-documenting
-
Single/Double Character Variables: Not allowed except for loop index variables (
i,j,k, etc.) -
Examples:
// Good: Meaningful variable names String accountName = 'Test Account'; Integer recordCount = 10; Boolean isLoading = true; List<Account> accountList = new List<Account>(); Account currentAccount; String errorMessage = error.getMessage(); // Good: Loop index variables (exception to the rule) for (Integer i = 0; i < items.size(); i++) { processItem(items[i]); } for (Integer j = 0; j < rows.size(); j++) { for (Integer k = 0; k < columns.size(); k++) { processCell(rows[j], columns[k]); } } // Bad: Single/double character variables (except loop indices) String a = 'Test Account'; // Bad - use accountName Integer x = 10; // Bad - use recordCount Account acc = [SELECT Id FROM Account LIMIT 1]; // Bad - use currentAccount or accountRecord String msg = error.getMessage(); // Bad - use errorMessage // Bad: Unclear abbreviations String accNm = 'Test Account'; // Bad - use accountName List<Account> accs = new List<Account>(); // Bad - use accountList or accounts Integer cnt = 0; // Bad - use recordCount or count
Best Practices:
- Use full words, not abbreviations (unless the abbreviation is widely understood)
- Be specific about what the variable contains (e.g.,
accountNamenotname,accountListnotlist) - Use descriptive names even for temporary variables
- Only use single-character variables (
i,j,k) for loop indices
Constants
-
Format: UPPER_SNAKE_CASE
-
Naming: Descriptive names in all caps with underscores
-
Examples:
static final String DEFAULT_STATUS = 'Active'; static final Set<String> DATE_LITERALS = new Set<String>{ 'YESTERDAY', 'TODAY', 'TOMORROW' }; static final Integer MAX_RETRY_ATTEMPTS = 3;
Inner Classes
-
Format: PascalCase (same as top-level classes)
-
Usage: Discretionary - wrappers and response classes are acceptable as inner classes, but don't overuse
-
Examples:
public class SFC_QueryBuilderService { public class QueryResult { // Inner class for query results } }
Exceptions
-
Format: PascalCase with "Exception" suffix
-
Naming: Descriptive name indicating the exception type
-
Examples:
public class SFC_ValidationException extends Exception { } public class SFC_SecurityException extends Exception { } public class SFC_QueryBuilderException extends Exception { }
Constants vs Enums
Note: This topic requires further planning and discussion. Guidelines will be added in a future update.
Code Organization
Class Structure Order
Classes should follow this order:
- Constants - Static final fields
- Properties - Instance and class variables
- Constructors - Class constructors
- Methods - Public methods first, then private/protected methods
Example:
public with sharing class SFC_AccountService {
// 1. Constants
private static final String DEFAULT_STATUS = 'Active';
// 2. Properties
private String accountName;
private List<Contact> contacts;
// 3. Constructors
public SFC_AccountService() {
contacts = new List<Contact>();
}
// 4. Public Methods
public static Account createAccount(String name) {
// Implementation
}
// 5. Private Methods
private Boolean validateAccount(Account acc) {
// Implementation
}
}
File Organization
- One class per file
- File name must match class name exactly
Package Organization
Code is organized in feature-based directories:
src/
├── utilities/
│ └── classes/
├── base/
│ └── classes/
└── components/
Null Handling and Safe Navigation
Safe Navigation Operator (?.)
The safe navigation operator (?.) returns null if the left operand is null, otherwise it evaluates the right operand. This prevents NullPointerException.
Usage Guidelines:
- Use safe navigation for chaining: When accessing properties or methods on potentially null objects
- Use explicit null checks for validation: When you need to validate input or handle null as an error condition
Examples:
// Good: Safe navigation for chaining
String objectName = record?.getSObjectType()?.getDescribe()?.getName();
List<SObject> results = queryResult?.getRecords();
// Good: Explicit check for validation
if (recordId == null) {
throw new SFC_ValidationException('Record ID is required');
}
// Good: Context-dependent usage
if (String.isNotBlank(querySort?.field)) {
// Process field
}
Collection Initialization
Always initialize empty collections in constructors to avoid null pointer exceptions.
Example:
public class SFC_Response {
public List<SObject> results { get; set; }
public List<String> messages { get; set; }
public SFC_Response() {
results = new List<SObject>();
messages = new List<String>();
}
}
SOQL Result Handling
Always check isEmpty() before accessing query results to prevent index out of bounds exceptions.
Example:
List<Account> accounts = [SELECT Id, Name FROM Account LIMIT 10];
if (!accounts.isEmpty()) {
Account firstAccount = accounts[0];
// Process account
} else {
// Handle empty result
}
Null Coalescing Patterns
Use ternary operators or conditional logic for null coalescing when appropriate.
Example:
String status = account.Status != null ? account.Status : 'Active';
Integer count = recordCount != null ? recordCount : 0;
Error Handling
Custom Exception Classes
Note: Guidelines for when to create custom exceptions vs. using standard exceptions require further planning. Current patterns in the codebase include:
TypeException- For type validation errorsFieldException- For field-related errorsPermissionException- For permission-related errorsValidationException- For general validation errors
Example:
public class SFC_ValidationException extends Exception { }
public static void validateRecord(Account acc) {
if (acc.Name == null) {
throw new SFC_ValidationException('Account name is required');
}
}
Try-Catch Patterns
- Always handle exceptions appropriately
- Never use empty catch blocks (violates Code Analyzer rules)
- Log errors appropriately (see Logging and Debugging)
- Return appropriate error responses for frontend methods
Example:
public static SFC_Response processRecord(Id recordId) {
SFC_Response response = new SFC_Response();
try {
Account acc = [SELECT Id, Name FROM Account WHERE Id = :recordId];
// Process account
response.results.add(acc);
} catch (QueryException e) {
System.debug('Error querying record: ' + e.getMessage());
response.success = false;
response.messages.add('Record not found');
} catch (Exception e) {
System.debug('Unexpected error: ' + e.getMessage());
response.success = false;
response.messages.add('An error occurred processing the record');
}
return response;
}
Error Message Best Practices
- Use clear, user-friendly error messages
- Include context when helpful (field name, record ID, etc.)
- Avoid exposing sensitive information
- Log detailed error information for debugging
Example:
throw new SFC_ValidationException('Account name is required');
throw new SFC_SecurityException('User does not have permission to access this record');
Exception Naming Conventions
- Use PascalCase with "Exception" suffix
- Be descriptive about the exception type
- Examples:
ValidationException,SecurityException,QueryBuilderException
Security
Sharing Model Declarations
All Apex classes that perform DML or SOQL operations must declare a sharing model.
Default Requirement:
All Apex classes must use with sharing by default. This enforces sharing rules and ensures proper security.
Exception for without sharing:
without sharing may only be used when:
- Reasoning is provided - Clear explanation of why sharing rules must be bypassed
- Prior approval is obtained - Team lead or architect approval
- It is documented - Inline comments and class documentation explain the exception
- Only the specific piece is isolated - Only the specific operation that requires
without sharingshould be in that class, not the surrounding logic
Example - Isolating without sharing:
If you need to query records without sharing rules, create a separate utility class for only that query:
// Bad: Entire service class without sharing
public without sharing class SFC_AccountService {
public static List<Account> getAccounts() {
// Query without sharing
}
public static void processAccounts(List<Account> accounts) {
// Processing logic - doesn't need without sharing
}
}
// Good: Isolated query class
public without sharing class SFC_AccountQueryService {
/**
* @description Queries accounts without sharing rules
* @reason Required to access accounts for system-level processing
* @approval Team Lead - 2024-01-01
*/
public static List<Account> getAccountsWithoutSharing() {
return [SELECT Id, Name FROM Account];
}
}
// Main service class uses with sharing
public with sharing class SFC_AccountService {
public static void processAccounts() {
// Get accounts without sharing (isolated operation)
List<Account> accounts = SFC_AccountQueryService.getAccountsWithoutSharing();
// Process accounts (with sharing enforced)
processAccountList(accounts);
}
private static void processAccountList(List<Account> accounts) {
// Processing logic with sharing enforced
}
}
CRUD/FLS Enforcement
Always enforce CRUD/FLS permissions before performing SOQL or DML operations.
Preferred Method:
Security.stripInaccessible() is the preferred method for CRUD/FLS enforcement. The QueryHelper/SFC_QueryBuilderService automatically handles this for query operations, making it the recommended approach.
All Enforcement Patterns Accepted:
While Security.stripInaccessible() is preferred, all enforcement patterns are accepted:
1. Preferred: Security.stripInaccessible()
// QueryHelper/QueryBuilderService automatically handles this
List<Account> accounts = SFC_QueryBuilderService.selectFrom('Account')
.withFields('Id', 'Name')
.execute(); // Automatically applies Security.stripInaccessible()
// Manual usage for DML
List<Account> accounts = [SELECT Id, Name FROM Account];
List<Account> accessibleAccounts = Security.stripInaccessible(
AccessType.READABLE,
accounts
).getRecords();
2. Alternative: Schema Checks
// Check object access
if (!Schema.sObjectType.Account.isAccessible()) {
throw new SFC_SecurityException('No access to Account object');
}
// Check field access
if (!Schema.sObjectType.Account.fields.Name.isAccessible()) {
throw new SFC_SecurityException('No access to Account.Name field');
}
3. Alternative: Manual Field Filtering
// Manually filter fields based on FLS
List<String> accessibleFields = new List<String>();
for (String field : requestedFields) {
if (Schema.sObjectType.Account.fields.getMap().get(field).getDescribe().isAccessible()) {
accessibleFields.add(field);
}
}
SOQL Injection Prevention
Never concatenate user input directly into SOQL queries. Always use bind variables.
Bad Example:
String query = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\'';
Good Example:
String query = 'SELECT Id FROM Account WHERE Name = :userInput';
List<Account> accounts = Database.query(query);
Input Validation
Always validate input parameters before processing:
public static SFC_Response processRecord(Id recordId) {
if (recordId == null) {
throw new SFC_ValidationException('Record ID is required');
}
// Process record
}
Method Visibility and Structure
Method Visibility Guidelines
- Public: Methods that are part of the class API, called from outside the class
- Private: Internal implementation methods, not called from outside
- Protected: Methods that may be overridden by subclasses
Default: Choose the most restrictive visibility that meets requirements. For utility classes with static methods, public is typically appropriate.
Static vs Instance Methods
Guidelines: Case-by-case based on class purpose
- Static: For stateless utility methods, factory methods, helper functions
- Instance: For stateful operations, when object state is needed
Example:
// Static utility method
public static List<Account> getAccounts() {
return [SELECT Id, Name FROM Account];
}
// Instance method (uses instance state)
public class SFC_AccountProcessor {
private String status;
public void processAccounts() {
// Uses this.status
}
}
@TestVisible Annotation
Usage: Only on private methods for 1:1 testing standard
The @TestVisible annotation allows test classes to access private methods. This should only be used when necessary to achieve 1:1 test method to code method relationship.
Example:
public class SFC_QueryBuilderService {
@TestVisible
private String buildQueryString(String type, String field) {
// Private method accessible to tests
}
}
Return Statement Patterns
Guidelines:
- Prefer single return for complex logic - Easier to debug and maintain
- Allow early returns for guard clauses - Reduces nesting and complexity
Example with Early Returns (Guard Clauses):
public static SFC_Response processRecord(Id recordId) {
if (recordId == null) {
return createErrorResponse('Record ID is required');
}
if (!hasAccess(recordId)) {
return createErrorResponse('Access denied');
}
// Main logic with less nesting
SFC_Response response = new SFC_Response();
// Process record
return response;
}
Example with Single Return (Complex Logic):
public static String calculateStatus(Account acc) {
String status;
if (acc.Amount > 10000) {
if (acc.Industry == 'Technology') {
status = 'Premium';
} else {
status = 'High Value';
}
} else {
status = 'Standard';
}
return status;
}
Frontend Integration
Response Wrapper Pattern
All methods returning to frontend (LWC/Aura) must return SFC_Response or a subclass.
The SFC_Response base class provides a standardized response structure:
public virtual with sharing class SFC_Response {
@AuraEnabled
public Boolean success { get; set; }
@AuraEnabled
public List<SObject> results { get; set; }
@AuraEnabled
public List<String> messages { get; set; }
@AuraEnabled
public Exception error { get; set; }
public SFC_Response() {
success = true;
messages = new List<String>();
results = new List<SObject>();
}
}
Using Response Wrapper
Example:
@AuraEnabled
public static SFC_Response shareRecord(Id parentId, Id userId) {
SFC_Response response = new SFC_Response();
try {
// Process sharing logic
response.messages.add('Record shared successfully');
} catch (Exception e) {
System.debug('Error creating share record: ' + e.getMessage());
response.success = false;
response.messages.add(e.getMessage());
}
return response;
}
Custom Response Classes
You can extend SFC_Response for specific use cases:
public class SFC_QueryResponse extends SFC_Response {
@AuraEnabled
public Integer totalResults { get; set; }
public SFC_QueryResponse() {
super();
totalResults = 0;
}
}
@AuraEnabled Method Patterns
- Use
@AuraEnabledfor methods called from LWC/Aura - Use
@AuraEnabled(cacheable=true)for read-only methods that can be cached - Always return
SFC_Responseor subclass - Handle errors gracefully and return error information in response
Example:
@AuraEnabled(cacheable=true)
public static SFC_Response getAccounts() {
SFC_Response response = new SFC_Response();
try {
List<Account> accounts = [SELECT Id, Name FROM Account LIMIT 100];
response.results.addAll(accounts);
} catch (Exception e) {
response.success = false;
response.messages.add('Error retrieving accounts');
}
return response;
}
@InvocableMethod Patterns for Flow
- Use
@InvocableMethodfor methods called from Flow - Return appropriate data types for Flow consumption
- Handle errors appropriately
Example:
public class SFC_FlowUtilities {
@InvocableMethod(label='Process Accounts' description='Processes a list of accounts')
public static List<String> processAccounts(List<Id> accountIds) {
List<String> results = new List<String>();
// Process accounts
return results;
}
}
Documentation
ApexDoc Comment Format
All public and protected classes and methods must have ApexDoc comments.
Class Documentation:
/**
* @description Service class for managing Account records.
*
* This class provides utility methods for handling Account record operations,
* including creation, updates, and query operations. It serves as a central point
* for Account-related operations in the application.
*
* Key functionalities:
* - Creating new Account records with validation
* - Updating existing Account records
* - Querying Accounts with various filters
* - Processing bulk Account operations
*
* The class uses various helper methods to validate and process Account data.
* It interacts with other service classes such as ContactService and OpportunityService
* to perform related operations.
*
* @author callinj, user2, user3
* @since 03-01-2024 Removed sharing enforcement - user3
* @since 02-01-2024 Updated method to include sharing enformement - user2
* @since 01-01-2024 Initial Implementation - callinj
*
* @see SFC_AccountTriggerHandler For trigger-related Account operations
* @see SFC_ContactService For Contact-related operations
*
* Example usage:
* <pre>
* SFC_AccountService.createAccount('Test Account');
* </pre>
*/
public class SFC_AccountService {
}
Method Documentation:
/**
* Creates a new Account record with the specified name and validates the input.
* @param accountName The name for the new account
* @return `Account` The created Account record
* @throws SFC_ValidationException if accountName is null or empty
* @see Account
* @see SFC_AccountService.updateAccount
*/
public static Account createAccount(String accountName) {
// Implementation
}
Example with Multiple Parameters:
/**
* A private helper method which runs the data query and packages the results for the DataListResponse.
* @param queryData An QueryObject wrapper object that will be used to build the final query.
* @param fieldNameType A map used to track field names and their corresponding data types.
* @param columnReferenceFields A map used to track field names and corresponding reference field names for clarity and labeling purposes.
* @param additionalFields List of additional fields that should be added to the record data.
* @return `Map<String, List<FieldWrapper>>` A map that tracks the field name and associated record data for that field after running the data query.
* @throws SFC_ValidationException if queryData is null or invalid
* @see SFC_QueryObject
* @see SFC_QueryBuilderService.createQueryObject
* @see SFC_DMLService.deleteRecords
*/
private Map<String, List<FieldWrapper>> executeDataQuery(
SFC_QueryObject queryData,
Map<String, String> fieldNameType,
Map<String, String> columnReferenceFields,
List<String> additionalFields
) {
// Implementation
}
Required Documentation Elements
For Classes:
@description- Brief description followed by expanded details explaining the class purpose, context, and role in the application- Key functionalities - Bulleted list of main features/capabilities provided by the class
- Additional context - Explanation of how the class interacts with other classes and its role in the system
@author- Comma-separated list of version control users who have contributed to the class@since- Date and initial implementation information (format: MM-DD-YYYY Initial Implementation - versionControlUser)@see- References to related classes/methods with brief descriptions of the relationship- Code examples (when helpful) - Usage examples in
<pre>tags
For Methods:
- Description - Plain text description of what the method does (no
@descriptiontag, just the description text) @param- Parameter descriptions (for each parameter, one per line)@return- Return value description with type in backticks (format:`Type` Description)@throws- Exceptions that may be thrown with conditions@see- References to related classes/methods (when applicable, one per line)
Code Examples in Docstrings
Include code examples in docstrings when they help clarify usage:
/**
* @description Queries accounts using the QueryBuilderService
*
* Example:
* <pre>
* SFC_QueryBuilderService.QueryBuilder builder = SFC_QueryBuilderService.selectFrom('Account');
* List<Account> accounts = builder.withFields('Id', 'Name').execute();
* </pre>
*/
@see References
Use @see to reference related classes, methods, or documentation:
/**
* @description Validates account data
* @see SFC_AccountService
* @see SFC_ValidationException
*/
Inline Comments Guidelines
- Prefer self-documenting code - Use clear variable and method names
- Add comments when logic is complex - Explain "why" not "what"
- Avoid obvious comments - Don't restate what the code clearly shows
- Update comments with code changes - Keep comments in sync with code
Good Example:
// Use safe navigation to avoid NPE when record type is null
String objectName = record?.getSObjectType()?.getDescribe()?.getName();
Bad Example:
// Set the account name
account.Name = 'Test';
Code Style and Formatting
Braces Requirement
Always use braces for all control structures (if, else, for, while, etc.), even for single-line statements.
Good Example:
if (account != null) {
processAccount(account);
}
for (Account acc : accounts) {
processAccount(acc);
}
Bad Example:
if (account != null)
processAccount(account);
for (Account acc : accounts)
processAccount(acc);
This is enforced by the Code Analyzer ruleset: IfStmtsMustUseBraces, ForLoopsMustUseBraces, WhileLoopsMustUseBraces, IfElseStmtsMustUseBraces.
Code Formatting Alignment
- Use Prettier for code formatting (configured in
.prettierrc) - Run Prettier before committing code
- Ensure consistent formatting across the codebase
Code Complexity
Cognitive Complexity Limits
- Method level: Maximum 15
- Class level: Maximum 50
These limits are enforced by the Code Analyzer ruleset. If complexity exceeds these limits, refactor the code.
NCSS Limits (Non-Commenting Source Statements)
- Method level: Maximum 60 lines
- Class level: Maximum 1000 lines
These limits are enforced by the Code Analyzer ruleset (NcssCount rule).
Nesting Depth Limits
- Maximum nesting depth: 4 levels
Deeply nested if/else statements are hard to read and maintain. Use early returns or refactor to reduce nesting.
Example - Too Deep:
if (condition1) {
if (condition2) {
if (condition3) {
if (condition4) {
if (condition5) { // Too deep!
// Code
}
}
}
}
}
Example - Refactored:
if (!condition1) {
return;
}
if (!condition2) {
return;
}
if (!condition3) {
return;
}
// Continue with less nesting
Parameter Count Limits
- Maximum parameters: 5
If a method requires more than 5 parameters, consider:
- Using a parameter object/wrapper class
- Refactoring into multiple methods
- Using a builder pattern
This is enforced by the Code Analyzer ruleset (ExcessiveParameterList rule).
Performance
Bulkification Patterns
Always write code that handles bulk operations (200+ records).
Bad Example:
for (Account acc : accounts) {
insert new Contact(AccountId = acc.Id, LastName = 'Test');
}
Good Example:
List<Contact> contacts = new List<Contact>();
for (Account acc : accounts) {
contacts.add(new Contact(AccountId = acc.Id, LastName = 'Test'));
}
insert contacts;
Governor Limit Awareness
- Be aware of governor limits (SOQL queries, DML statements, CPU time, etc.)
- Use
Limitsclass to check current usage when needed - Design code to stay within limits
Example:
if (Limits.getQueries() < Limits.getLimitQueries() - 5) {
// Safe to perform query
}
Query Optimization
- Only query fields you need
- Use appropriate WHERE clauses and filters
- Use
LIMITwhen appropriate - Avoid queries in loops
- Use
Security.stripInaccessible()for CRUD/FLS
DML Best Practices
- Bulkify DML operations - Collect records and perform DML once
- Use Database methods -
Database.insert(),Database.update(), etc. for partial success handling - Use
Security.stripInaccessible()- Before DML operations
Example:
List<Account> accounts = new List<Account>();
// Populate accounts list
List<Database.SaveResult> results = Database.insert(
Security.stripInaccessible(AccessType.CREATABLE, accounts).getRecords(),
false // allOrNone = false for partial success
);
Transaction Control
Use QueryHelper/QueryBuilderService when possible - This is the standard best practice for query operations.
For direct DML operations:
- Use
Databasemethods withallOrNoneparameter for transaction control - Use savepoints for complex transaction scenarios
- Handle partial DML success appropriately
Example:
Savepoint sp = Database.setSavepoint();
try {
insert accounts;
insert contacts;
} catch (Exception e) {
Database.rollback(sp);
throw e;
}
Testing Standards
Note: This section provides Apex-specific testing guidelines. For comprehensive testing standards covering all aspects of test coverage, test structure, and best practices, see the Testing Standards document.
@isTest(seeAllData=true) Prohibition
@isTest(seeAllData=true) is not allowed without reasoning and documentation.
If you must use seeAllData=true, you must:
- Document why it's necessary
- Explain why test data creation isn't sufficient
- Get approval from the team lead
Example:
/**
* @description Test class for Organization settings
* @seeAllData true - Required to test Organization-level settings that cannot be created in tests
*/
@isTest(seeAllData=true)
public class SFC_OrganizationSettingsTest {
// Tests
}
@TestVisible Usage Guidelines
Only use @TestVisible on private methods for 1:1 testing standard.
This allows test classes to access private methods when necessary to achieve 1:1 test method to code method relationship.
Example:
public class SFC_QueryBuilderService {
@TestVisible
private String buildQueryString(String type, String field) {
// Private method accessible to tests
}
}
Test Method Structure
- Use descriptive test method names
- Follow naming convention:
testMethodName_condition_expectedResult - Use
@isTestannotation - Include assertions with descriptive messages
Example:
@isTest
private static void createAccount_validName_createsAccount() {
// Arrange
String accountName = 'Test Account';
// Act
Account acc = SFC_AccountService.createAccount(accountName);
// Assert
System.assertNotEquals(null, acc.Id, 'Account should be created with an ID');
System.assertEquals(accountName, acc.Name, 'Account name should match');
}
Logging and Debugging
System.debug Usage
System.debug should only be used for error logging. All other debug statements must be removed before committing code.
Good Example:
try {
// Process record
} catch (Exception e) {
System.debug('Error processing record: ' + e.getMessage());
// Handle error
}
Bad Example:
System.debug('Processing account: ' + account.Name); // Remove before commit
System.debug('Account ID: ' + account.Id); // Remove before commit
Remove Debug Statements Before Commits
- Remove all
System.debugstatements except those used for error logging - Error logging debug statements should remain for production debugging
- Use proper error handling and response wrappers instead of debug statements
Logging Patterns and Best Practices
- Log errors with context (method name, parameters, etc.)
- Use consistent log message format
- Include exception stack traces when helpful
- Don't log sensitive information
Example:
try {
processRecord(recordId);
} catch (Exception e) {
System.debug('Error in processRecord for recordId ' + recordId + ': ' + e.getMessage());
System.debug('Stack trace: ' + e.getStackTraceString());
throw e;
}
Code Maintenance
TODO/FIXME Comments
TODO and FIXME comments are acceptable if there is a discussion, issue, or bug created to capture that work.
Format:
// TODO: [Issue #123] Refactor this method to use QueryBuilderService
// FIXME: [Bug #456] Handle edge case when recordId is null
Bad Example:
// TODO: Fix this later
// FIXME: This doesn't work
API Version Standardization
Use the latest API version from the project. SFDX handles this automatically when creating new files.
- Don't manually set API version unless necessary
- Keep API versions consistent across the project
- Update API versions as part of regular maintenance
Deprecation Process
Note: This topic requires further discussion and planning. The deprecation process should include:
- Documentation - Document why the code is deprecated and what to use instead
- Transition Period - Provide a grace period for migration
- Process to List Usages/Updates - Track all usages and required updates
Placeholder for Future Implementation:
/**
* @description [DEPRECATED] Use SFC_QueryBuilderService instead
* @deprecated This method will be removed in version 2.0. Use SFC_QueryBuilderService.selectFrom() instead.
* @see SFC_QueryBuilderService
*/
@Deprecated
public static List<SObject> queryRecords(String objectName) {
// Deprecated implementation
}
Code Analyzer Compliance
Ruleset Reference
All code must comply with the Code Analyzer ruleset defined in rulesets/salesforce.xml.
Key Rules to Be Aware Of
Complexity Rules:
CognitiveComplexity- Method: 15, Class: 50NcssCount- Method: 60, Class: 1000AvoidDeeplyNestedIfStmts- Maximum depth: 4ExcessiveParameterList- Maximum: 5 parameters
Security Rules:
ApexSharingViolations- Must declare sharing modelApexCRUDViolation- Validate CRUD before SOQL/DMLApexSOQLInjection- Use bind variables, not string concatenation
Performance Rules:
OperationWithLimitsInLoop- Avoid DML in loops
Code Style Rules:
IfStmtsMustUseBraces- Always use bracesForLoopsMustUseBraces- Always use bracesClassNamingConventions- PascalCase for classesMethodNamingConventions- camelCase for methods
Test Rules:
ApexAssertionsShouldIncludeMessage- Include messages in assertionsApexUnitTestClassShouldHaveAsserts- Tests must have assertionsApexUnitTestShouldNotUseSeeAllDataTrue- Avoid seeAllData=true
Common Violations and Solutions
Violation: Cognitive Complexity Too High
Solution: Refactor into smaller methods, use early returns, reduce nesting
Violation: DML in Loop
Solution: Collect records in a list, perform DML once outside the loop
Violation: Missing Sharing Declaration
Solution: Add with sharing, without sharing, or inherited sharing to class declaration
Violation: SOQL Injection
Solution: Use bind variables instead of string concatenation
@SuppressWarnings Usage Guidelines
Use @SuppressWarnings sparingly and document why.
If you must suppress a warning:
/**
* @description Suppresses PMD warning for this method
* Suppression reason: This method intentionally uses a switch statement
* for type checking which PMD flags, but is the most readable approach here.
*/
@SuppressWarnings('PMD.AvoidDeeplyNestedIfStmts')
public static String processType(String type) {
// Implementation
}
Future Considerations / Roadmap
The following topics are identified for future discussion and potential addition to these standards:
- String Handling Methods - Guidelines for
String.isBlank()vsisEmpty()vsisNotBlank()- when to use each - Switch Statements -
switch onvs if/else chains - when to use each pattern - Type Checking -
instanceofpatterns and type casting best practices - Collection Selection - When to use
ListvsSetvsMap- selection criteria and patterns - Schema Patterns - Detailed guidelines for
Schema.getGlobalDescribe(),getSObjectType(),getDescribe()usage - Governor Limit Checking -
Limitsclass usage patterns and best practices - Async Patterns -
@Future,Queueable,Batchable,Schedulablepatterns (even if not currently used) - Callout Patterns - HTTP callouts, named credentials usage (ruleset mentions
ApexSuggestUsingNamedCred) - Transient Keyword - For
@AuraEnabledproperties to reduce view state - Property Getters/Setters - Custom getter/setter patterns and when to use them
- JSON Serialization -
JSON.serialize()/deserialize()patterns and best practices - Date/Time Handling - Date/Datetime/Time patterns and timezone considerations
These items should be discussed and added to the standards documentation as needed based on project requirements and codebase evolution.
Change History
Version 1.0 - 2026-01-14
- Initial release of Apex coding standards documentation
Last Updated: 2026-01-14
Version: 1.0