afv-library/skills/generating-apex/references/best-practices.md

6.6 KiB

Apex Best Practices — Supplementary Patterns

Patterns not covered in the main SKILL.md rules or other reference files. For bulkification, security, error handling, and async patterns, see the dedicated reference files. For code smells and refactoring strategies, see anti-patterns.md.

1. Platform Cache

When to Use

  • Frequently accessed, rarely changed data
  • Expensive calculations
  • Cross-transaction data sharing

Implementation

// Check cache first
Account acc = (Account)Cache.Org.get('local.AccountCache.' + accountId);
if (acc == null) {
    acc = [SELECT Id, Name FROM Account WHERE Id = :accountId];
    Cache.Org.put('local.AccountCache.' + accountId, acc, 3600);  // 1 hour TTL
}
return acc;

Always Handle Cache Misses

// Cache can be evicted at any time
Object cachedValue = Cache.Org.get(key);
if (cachedValue == null) {
    // Rebuild from source
}

2. Static Variables for Transaction Caching

Prevent Duplicate Queries

public class AccountService {
    private static Map<Id, Account> accountCache;

    public static Account getAccount(Id accountId) {
        if (accountCache == null) {
            accountCache = new Map<Id, Account>();
        }

        if (!accountCache.containsKey(accountId)) {
            accountCache.put(accountId, [SELECT Id, Name FROM Account WHERE Id = :accountId]);
        }

        return accountCache.get(accountId);
    }
}

Recursion Prevention

public class TriggerHelper {
    private static Set<Id> processedIds = new Set<Id>();

    public static Boolean hasProcessed(Id recordId) {
        if (processedIds.contains(recordId)) {
            return true;
        }
        processedIds.add(recordId);
        return false;
    }
}

3. Guard Clauses & Fail-Fast

💡 Principles inspired by "Clean Apex Code" by Pablo Gonzalez. Purchase the book for complete coverage.

The Problem

Deeply nested validation leads to hard-to-read code where business logic is buried.

Anti-Pattern

// BAD: Deep nesting obscures business logic
public void processAccountUpdate(Account oldAccount, Account newAccount) {
    if (newAccount != null) {
        if (oldAccount != null) {
            if (newAccount.Id != null) {
                if (hasFieldChanged(oldAccount, newAccount)) {
                    if (UserInfo.getUserType() == 'Standard') {
                        // Actual business logic buried 5 levels deep
                        performSync(newAccount);
                        sendNotification(newAccount);
                    }
                }
            }
        }
    }
}

Best Practice: Guard Clauses

// GOOD: Guard clauses at the top, exit early
public void processAccountUpdate(Account oldAccount, Account newAccount) {
    // Guard clauses - validate preconditions and exit fast
    if (newAccount == null) return;
    if (oldAccount == null) return;
    if (newAccount.Id == null) return;
    if (!hasFieldChanged(oldAccount, newAccount)) return;
    if (UserInfo.getUserType() != 'Standard') return;

    // Main logic is now at the top level, clearly visible
    performSync(newAccount);
    sendNotification(newAccount);
}

Parameter Validation with Exceptions

For public APIs, throw exceptions for invalid input:

public Database.LeadConvertResult convertLead(Id leadId, Id accountId) {
    // Guard clauses with exceptions for public API
    if (leadId == null) {
        throw new IllegalArgumentException('Lead ID cannot be null');
    }

    if (leadId.getSObjectType() != Lead.SObjectType) {
        throw new IllegalArgumentException('Expected Lead ID, received: ' + leadId.getSObjectType());
    }

    Lead leadRecord = queryLead(leadId);

    if (leadRecord == null) {
        throw new IllegalArgumentException('Lead not found: ' + leadId);
    }

    if (leadRecord.IsConverted) {
        throw new IllegalArgumentException('Lead is already converted: ' + leadId);
    }

    // Main conversion logic
    return performConversion(leadRecord, accountId);
}

When to Use Each Pattern

Scenario Pattern Example
Private/internal methods return early if (list == null) return;
Public API throw Exception throw new IllegalArgumentException(...)
Trigger handlers return for skip if (records.isEmpty()) return;
Validation service addError() record.addError('...')

4. Comment Best Practices

💡 Principles inspired by "Clean Apex Code" by Pablo Gonzalez. Purchase the book for complete coverage.

Core Principle

Comments should explain "why", not "what". The code itself should communicate the "what".

When Comments Add Value

// GOOD: Explains business decision
// Salesforce processes triggers in batches of 200. We use 201 to ensure
// our code handles batch boundaries correctly during validation.
private static final Integer BULK_TEST_SIZE = 201;

// GOOD: Documents platform limitation
// Safe navigation (?.) doesn't work in formulas - must use IF(ISBLANK())
// See Known Issue W-12345678

// GOOD: References external documentation
// Algorithm based on RFC 7519 (JSON Web Token specification)
// See: https://tools.ietf.org/html/rfc7519#section-4.1

// GOOD: Explains non-obvious optimization
// SOQL query in a loop replaced with map-based lookup — O(1) per record.
Account acc = accountsByExternalId.get(record.ExternalId__c);

Comment Anti-Patterns

// BAD: Restates what code clearly shows
Integer count = 0;  // Initialize count to zero

// BAD: Version history belongs in Git
// Modified by John on 2024-01-15 to add validation
// Modified by Jane on 2024-02-20 to fix bug

// BAD: Commented-out code (delete it!)
// if (account.Type == 'Partner') {
//     processPartner(account);
// }

// BAD: TODO without owner or ticket
// TODO: fix this later

// GOOD: TODO with context
// TODO(JIRA-1234): Refactor to use Platform Events after Spring '26 release

Self-Documenting Code

Instead of comments, make code self-explanatory:

// BAD: Needs comment to explain
if (acc.AnnualRevenue > 1000000 && acc.Type == 'Enterprise' && acc.Industry == 'Technology') {
    // Process strategic tech accounts
}

// GOOD: Code explains itself
Boolean isStrategicTechAccount =
    acc.AnnualRevenue > 1000000 &&
    acc.Type == 'Enterprise' &&
    acc.Industry == 'Technology';

if (isStrategicTechAccount) {
    processStrategicAccount(acc);
}