Skip to content
Back to skills

102 Best Practices 064cbc39

ASecurity

<!-- Parent: sf-apex/SKILL.md -->

  • 9 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 11, 2026
documentationgospringtestinggitapidatabasesecurityperformancedocumentation

Works with

  • api

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 102-best-practices_064cbc39 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 102 Best Practices 064cbc39?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 102 Best Practices 064cbc39
[![Security: A β€” Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-102-best-practices-064cbc39/badge)](https://www.skillsdirectory.com/skills/tools-only-102-best-practices-064cbc39)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
<!-- Parent: sf-apex/SKILL.md -->
# Apex Best Practices Reference

## 1. Bulkification

### The Problem
Apex triggers can process up to 200 records at once. Code that works for single records often fails at scale.

### Anti-Pattern
```apex
// BAD: SOQL in loop - will hit 100 query limit
for (Account acc : Trigger.new) {
    Contact c = [SELECT Id FROM Contact WHERE AccountId = :acc.Id LIMIT 1];
}
```

### Best Practice
```apex
// GOOD: Query once, use Map for lookup
Set<Id> accountIds = new Map<Id, Account>(Trigger.new).keySet();
Map<Id, Contact> contactsByAccountId = new Map<Id, Contact>();
for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
    contactsByAccountId.put(c.AccountId, c);
}

for (Account acc : Trigger.new) {
    Contact c = contactsByAccountId.get(acc.Id);
}
```

### DML Bulkification
```apex
// BAD: DML in loop
for (Account acc : accounts) {
    acc.Status__c = 'Active';
    update acc;
}

// GOOD: Collect and update once
List<Account> toUpdate = new List<Account>();
for (Account acc : accounts) {
    acc.Status__c = 'Active';
    toUpdate.add(acc);
}
update toUpdate;
```

### Test with Bulk Data
```apex
@isTest
static void testBulkOperation() {
    List<Account> accounts = new List<Account>();
    for (Integer i = 0; i < 251; i++) {  // 251 to span two trigger batches
        accounts.add(new Account(Name = 'Test ' + i));
    }

    Test.startTest();
    insert accounts;
    Test.stopTest();

    Assert.areEqual(251, [SELECT COUNT() FROM Account]);
}
```

---

## 2. Collections Best Practices

### Use Map Constructor for ID Extraction
```apex
// BAD: Loop to get IDs
Set<Id> accountIds = new Set<Id>();
for (Account acc : accounts) {
    accountIds.add(acc.Id);
}

// GOOD: Map constructor
Set<Id> accountIds = new Map<Id, Account>(accounts).keySet();
```

### Use Maps for Lookups
```apex
// Build lookup map
Map<Id, Account> accountsById = new Map<Id, Account>(accounts);

// O(1) lookup instead of O(n) loop
Account acc = accountsById.get(someId);
```

### Collection Naming Convention
```apex
List<Account> accounts;              // Plural noun
Set<Id> accountIds;                  // Type suffix
Map<Id, Account> accountsById;       // Key description
Map<String, List<Contact>> contactsByEmail;  // Nested collection
```

---

## 3. SOQL Best Practices

### Always Use Selective Queries
```apex
// GOOD: Filter on indexed fields
[SELECT Id FROM Account WHERE Id = :recordId]
[SELECT Id FROM Account WHERE Name = :name]
[SELECT Id FROM Account WHERE CreatedDate > :startDate]
[SELECT Id FROM Contact WHERE Email = :email]  // If External ID

// BAD: Non-selective patterns
[SELECT Id FROM Account WHERE Custom_Field__c != null]  // != null
[SELECT Id FROM Account WHERE Name LIKE '%test%']       // Leading wildcard
[SELECT Id FROM Account WHERE CALENDAR_YEAR(CreatedDate) = 2025]  // Function
```

### Use Bind Variables (Prevent SOQL Injection)
```apex
// BAD: String concatenation (SOQL injection risk)
String query = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\'';

// GOOD: Bind variable
String query = 'SELECT Id FROM Account WHERE Name = :userInput';
List<Account> accounts = Database.query(query);
```

### Use USER_MODE for Security
```apex
// Enforces CRUD/FLS automatically
List<Account> accounts = [SELECT Id, Name FROM Account WITH USER_MODE];

// For Database methods
Database.query(query, AccessLevel.USER_MODE);
Database.insert(records, AccessLevel.USER_MODE);
```

### SOQL For Loops for Large Data
```apex
// Memory efficient - processes 200 records at a time
for (Account acc : [SELECT Id, Name FROM Account]) {
    // Process each account
}

// Or batch processing
for (List<Account> batch : [SELECT Id, Name FROM Account]) {
    // Process batch of 200
}
```

---

## 4. Governor Limits Awareness

### Key Limits
| Limit | Synchronous | Asynchronous |
|-------|-------------|--------------|
| SOQL Queries | 100 | 200 |
| DML Statements | 150 | 150 |
| CPU Time | 10,000 ms | 60,000 ms |
| Heap Size | 6 MB | 12 MB |
| Callouts | 100 | 100 |

### Monitor Limits
```apex
System.debug('SOQL: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());
System.debug('DML: ' + Limits.getDmlStatements() + '/' + Limits.getLimitDmlStatements());
System.debug('CPU: ' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());
System.debug('Heap: ' + Limits.getHeapSize() + '/' + Limits.getLimitHeapSize());
```

### Heap Optimization
```apex
// BAD: Class-level variable holds large data
public class BadExample {
    private List<Account> allAccounts;  // Stays in memory
}

// GOOD: Let variables go out of scope
public class GoodExample {
    public void process() {
        List<Account> accounts = [SELECT Id FROM Account];
        // Process...
    }  // accounts released here
}
```

---

## 5. Null Safety

### Null Coalescing Operator (??)
```apex
// Old way
String value = input != null ? input : 'default';

// New way (API 60+)
String value = input ?? 'default';

// Chaining
String value = firstChoice ?? secondChoice ?? 'default';
```

### Safe Navigation Operator (?.)
```apex
// Old way
String accountName = null;
if (contact != null && contact.Account != null) {
    accountName = contact.Account.Name;
}

// New way
String accountName = contact?.Account?.Name;

// With null coalescing
String accountName = contact?.Account?.Name ?? 'Unknown';
```

### Null-Safe Collection Access
```apex
// Check before accessing
if (myMap != null && myMap.containsKey(key)) {
    return myMap.get(key);
}

// Or use getOrDefault pattern
public static Object getOrDefault(Map<Id, Object> m, Id key, Object defaultVal) {
    return m?.containsKey(key) == true ? m.get(key) : defaultVal;
}
```

---

## 6. Error Handling

### Catch Specific Exceptions
```apex
try {
    insert accounts;
} catch (DmlException e) {
    // Handle DML-specific errors
    for (Integer i = 0; i < e.getNumDml(); i++) {
        System.debug('Field: ' + e.getDmlFieldNames(i));
        System.debug('Message: ' + e.getDmlMessage(i));
    }
} catch (QueryException e) {
    // Handle query errors
} catch (Exception e) {
    // Generic fallback - log and rethrow
    System.debug(LoggingLevel.ERROR, e.getMessage());
    throw e;
}
```

### Custom Exceptions
```apex
public class InsufficientInventoryException extends Exception {}

public void processOrder(Order ord) {
    if (ord.Quantity__c > availableStock) {
        throw new InsufficientInventoryException(
            'Requested: ' + ord.Quantity__c + ', Available: ' + availableStock
        );
    }
}
```

### AuraHandledException for LWC
```apex
@AuraEnabled
public static void processRecord(Id recordId) {
    try {
        // Business logic
    } catch (Exception e) {
        throw new AuraHandledException(e.getMessage());
    }
}
```

---

## 7. Async Apex Selection

### @future
```apex
// Simple, fire-and-forget
@future(callout=true)
public static void makeCallout(Set<Id> recordIds) {
    // Cannot return value, cannot chain
}
```

### Queueable
```apex
// Complex logic, can chain, can pass complex types
public class ProcessRecordsQueueable implements Queueable {
    private List<Account> accounts;

    public ProcessRecordsQueueable(List<Account> accounts) {
        this.accounts = accounts;
    }

    public void execute(QueueableContext context) {
        // Process accounts

        // Chain next job if needed
        if (moreWork) {
            System.enqueueJob(new ProcessRecordsQueueable(nextBatch));
        }
    }
}
```

### Batch Apex
```apex
// Large data volumes (millions of records)
public class ProcessAccountsBatch implements Database.Batchable<SObject> {
    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator('SELECT Id FROM Account');
    }

    public void execute(Database.BatchableContext bc, List<Account> scope) {
        // Process batch (default 200 records)
    }

    public void finish(Database.BatchableContext bc) {
        // Cleanup, notifications, chain next batch
    }
}
```

---

## 8. Platform Cache

### When to Use
- Frequently accessed, rarely changed data
- Expensive calculations
- Cross-transaction data sharing

### Implementation
```apex
// 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
```apex
// Cache can be evicted at any time
Object cachedValue = Cache.Org.get(key);
if (cachedValue == null) {
    // Rebuild from source
}
```

---

## 9. Static Variables for Transaction Caching

### Prevent Duplicate Queries
```apex
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
```apex
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;
    }
}
```

---

## 10. Guard Clauses & Fail-Fast

> πŸ’‘ *Principles inspired by "Clean Apex Code" by Pablo Gonzalez.
> [Purchase the book](https://link.springer.com/book/10.1007/979-8-8688-1411-2) for complete coverage.*

### The Problem

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

### Anti-Pattern
```apex
// 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
```apex
// 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:

```apex
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('...')` |

---

## 11. Comment Best Practices

> πŸ’‘ *Principles inspired by "Clean Apex Code" by Pablo Gonzalez.
> [Purchase the book](https://link.springer.com/book/10.1007/979-8-8688-1411-2) for complete coverage.*

### Core Principle

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

### When Comments Add Value

```apex
// GOOD: Explains business decision
// Salesforce processes triggers in batches of 200. We use 201 to ensure
// our code handles batch boundaries correctly during testing.
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
// DML on empty list still consumes ~10x CPU. Always check isEmpty().
if (!accountsToUpdate.isEmpty()) {
    update accountsToUpdate;
}
```

### Comment Anti-Patterns

```apex
// 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:

```apex
// 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);
}
```

---

## 12. DML Performance Pattern

> πŸ’‘ *Principles inspired by "Clean Apex Code" by Pablo Gonzalez.
> [Purchase the book](https://link.springer.com/book/10.1007/979-8-8688-1411-2) for complete coverage.*

### The Problem

DML operations on empty collections still consume significant CPU time (~10x more than checking isEmpty first).

### Anti-Pattern
```apex
// BAD: DML on potentially empty list
List<Account> accountsToUpdate = new List<Account>();
// ... conditional logic that might not add anything ...
update accountsToUpdate;  // Wastes CPU even if empty
```

### Best Practice
```apex
// GOOD: Always check isEmpty() before DML
if (!accountsToUpdate.isEmpty()) {
    update accountsToUpdate;
}
```

### SafeDML Wrapper

Create a utility class to enforce this pattern:

```apex
public class SafeDML {

    public static Database.SaveResult[] safeInsert(List<SObject> records) {
        if (records == null || records.isEmpty()) {
            return new List<Database.SaveResult>();
        }
        return Database.insert(records, false);
    }

    public static Database.SaveResult[] safeUpdate(List<SObject> records) {
        if (records == null || records.isEmpty()) {
            return new List<Database.SaveResult>();
        }
        return Database.update(records, false);
    }

    public static Database.DeleteResult[] safeDelete(List<SObject> records) {
        if (records == null || records.isEmpty()) {
            return new List<Database.DeleteResult>();
        }
        return Database.delete(records, false);
    }

    public static Database.UpsertResult[] safeUpsert(
        List<SObject> records,
        Schema.SObjectField externalIdField
    ) {
        if (records == null || records.isEmpty()) {
            return new List<Database.UpsertResult>();
        }
        return Database.upsert(records, externalIdField, false);
    }
}
```

### Usage
```apex
// Clean, safe DML operations
SafeDML.safeInsert(newAccounts);
SafeDML.safeUpdate(modifiedContacts);
SafeDML.safeDelete(obsoleteRecords);
```

### Performance Impact

| Scenario | CPU Time |
|----------|----------|
| `update emptyList` | ~100-200 CPU ms |
| `if (!empty) update` | ~10-20 CPU ms |
| **Savings** | **~10x improvement** |

In triggers processing many records, this optimization compounds significantly.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.