Author: callinj
Name: Joe Callin
Title: Sr. Salesforce Developer
Email: joe@jcallin.dev

Table of Contents

  1. Introduction
  2. Naming Conventions
  3. Code Organization
  4. Null Handling and Safe Navigation
  5. Error Handling
  6. Security
  7. Method Visibility and Structure
  8. Frontend Integration
  9. Documentation
  10. Code Style and Formatting
  11. Code Complexity
  12. Performance
  13. Testing Standards
  14. Logging and Debugging
  15. Code Maintenance
  16. 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.

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., accountName not name, accountList not list)
  • 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:

  1. Constants - Static final fields
  2. Properties - Instance and class variables
  3. Constructors - Class constructors
  4. 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 errors
  • FieldException - For field-related errors
  • PermissionException - For permission-related errors
  • ValidationException - 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:

  1. Reasoning is provided - Clear explanation of why sharing rules must be bypassed
  2. Prior approval is obtained - Team lead or architect approval
  3. It is documented - Inline comments and class documentation explain the exception
  4. Only the specific piece is isolated - Only the specific operation that requires without sharing should 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 @AuraEnabled for methods called from LWC/Aura
  • Use @AuraEnabled(cacheable=true) for read-only methods that can be cached
  • Always return SFC_Response or 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 @InvocableMethod for 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 @description tag, 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 Limits class 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 LIMIT when 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 Database methods with allOrNone parameter 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:

  1. Document why it's necessary
  2. Explain why test data creation isn't sufficient
  3. 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 @isTest annotation
  • 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.debug statements 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:

  1. Documentation - Document why the code is deprecated and what to use instead
  2. Transition Period - Provide a grace period for migration
  3. 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: 50
  • NcssCount - Method: 60, Class: 1000
  • AvoidDeeplyNestedIfStmts - Maximum depth: 4
  • ExcessiveParameterList - Maximum: 5 parameters

Security Rules:

  • ApexSharingViolations - Must declare sharing model
  • ApexCRUDViolation - Validate CRUD before SOQL/DML
  • ApexSOQLInjection - Use bind variables, not string concatenation

Performance Rules:

  • OperationWithLimitsInLoop - Avoid DML in loops

Code Style Rules:

  • IfStmtsMustUseBraces - Always use braces
  • ForLoopsMustUseBraces - Always use braces
  • ClassNamingConventions - PascalCase for classes
  • MethodNamingConventions - camelCase for methods

Test Rules:

  • ApexAssertionsShouldIncludeMessage - Include messages in assertions
  • ApexUnitTestClassShouldHaveAsserts - Tests must have assertions
  • ApexUnitTestShouldNotUseSeeAllDataTrue - 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:

  1. String Handling Methods - Guidelines for String.isBlank() vs isEmpty() vs isNotBlank() - when to use each
  2. Switch Statements - switch on vs if/else chains - when to use each pattern
  3. Type Checking - instanceof patterns and type casting best practices
  4. Collection Selection - When to use List vs Set vs Map - selection criteria and patterns
  5. Schema Patterns - Detailed guidelines for Schema.getGlobalDescribe(), getSObjectType(), getDescribe() usage
  6. Governor Limit Checking - Limits class usage patterns and best practices
  7. Async Patterns - @Future, Queueable, Batchable, Schedulable patterns (even if not currently used)
  8. Callout Patterns - HTTP callouts, named credentials usage (ruleset mentions ApexSuggestUsingNamedCred)
  9. Transient Keyword - For @AuraEnabled properties to reduce view state
  10. Property Getters/Setters - Custom getter/setter patterns and when to use them
  11. JSON Serialization - JSON.serialize()/deserialize() patterns and best practices
  12. 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