AgentStack
SKILL verified MIT Self-run

Sf Apex Best Practices

skill-jiten-singh-shahi-salesforce-claude-code-sf-apex-best-practices · by jiten-singh-shahi

>-

No reviews yet
0 installs
15 views
0.0% view→install

Install

$ agentstack add skill-jiten-singh-shahi-salesforce-claude-code-sf-apex-best-practices

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Sf Apex Best Practices? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Apex Best Practices

Procedures for writing production-ready Apex. Constraint rules (never/always lists) live in sf-apex-constraints. This skill covers the how — class organization, error handling patterns, null safety techniques, and collection usage.

Reference files:

@../reference/GOVERNORLIMITS.md @../reference/NAMINGCONVENTIONS.md @../reference/SECURITYPATTERNS.md


When to Use

  • When writing new Apex classes, triggers, or test classes for a Salesforce org
  • When reviewing existing Apex code for structure or error handling issues
  • When onboarding new developers to Salesforce Apex coding standards
  • When refactoring legacy Apex code to improve readability and maintainability

Class Organization

Organize class members in this order:

  1. Constants (static final)
  2. Static variables
  3. Instance variables (fields)
  4. Constructors
  5. Public methods
  6. Private/Protected methods
  7. Inner classes
public with sharing class OrderProcessor {

    // 1. Constants
    private static final String STATUS_PENDING    = 'Pending';
    private static final String STATUS_PROCESSING = 'Processing';
    private static final String STATUS_COMPLETE   = 'Complete';
    private static final Integer MAX_LINE_ITEMS   = 500;

    // 2. Static variables
    private static Boolean isProcessing = false;

    // 3. Instance variables
    private List orders;
    private Map accountMap;
    private OrderValidator validator;

    // 4. Constructor
    public OrderProcessor(List orders) {
        this.orders      = orders;
        this.accountMap  = new Map();
        this.validator   = new OrderValidator();
    }

    // 5. Public methods
    public List processAll() {
        List results = new List();
        loadRelatedAccounts();
        for (Order__c order : orders) {
            results.add(processSingleOrder(order));
        }
        return results;
    }

    // 6. Private methods
    private void loadRelatedAccounts() {
        Set accountIds = new Set();
        for (Order__c order : orders) {
            if (order.AccountId != null) {
                accountIds.add(order.AccountId);
            }
        }
        for (Account acc : [SELECT Id, Name, CreditLimit__c FROM Account WHERE Id IN :accountIds]) {
            accountMap.put(acc.Id, acc);
        }
    }

    private ProcessResult processSingleOrder(Order__c order) {
        if (!validator.isValid(order)) {
            return new ProcessResult(order.Id, false, validator.getLastError());
        }
        order.Status__c = STATUS_PROCESSING;
        return new ProcessResult(order.Id, true, null);
    }

    // 7. Inner classes
    public class ProcessResult {
        public Id orderId     { get; private set; }
        public Boolean success { get; private set; }
        public String message  { get; private set; }

        public ProcessResult(Id orderId, Boolean success, String message) {
            this.orderId = orderId;
            this.success = success;
            this.message  = message;
        }
    }
}

Error Handling

Custom Exception Classes

Create domain-specific exception classes instead of using generic exceptions. This lets callers catch specifically what they care about.

// Define exceptions in their own files or as inner classes
public class AccountServiceException extends Exception {}
public class OrderValidationException extends Exception {}
public class IntegrationCalloutException extends Exception {}

// Inner exception (acceptable for tight coupling)
public class AccountService {
    public class AccountNotFoundException extends Exception {}
    public class DuplicateAccountException extends Exception {}
}

Catch Scope

Catch the most specific exception type available. Catching Exception hides programming errors.

// Correct — catch what you expect, let others propagate
try {
    processAccount(account);
} catch (DmlException e) {
    throw new AccountServiceException('Failed to save account: ' + e.getDmlMessage(0), e);
} catch (CalloutException e) {
    throw new IntegrationCalloutException('External service unavailable: ' + e.getMessage(), e);
}

Database.SaveResult Checking

When using partial-success DML, check every result.

List results = Database.insert(accounts, false);
List errors = new List();

for (Integer i = 0; i { account.Name, account.Id, dmlError.getMessage() }
    )
);

Single Responsibility Principle

Each class should have one reason to change. Split classes by responsibility, not by object type.

// Correct — each class has one job
public class AccountService        { public void createAccount() {} }
public class AccountNotificationService { public void sendWelcomeEmail() {} }
public class OpportunityService    { public void createFromAccount() {} }
public class ERPSyncService        { public void syncAccount() {} }
public class AccountDocumentService { public void generateOnboardingPDF() {} }

Method Length

Methods longer than ~50 lines are doing too much. Extract private helper methods.

// Correct — orchestrator calling focused helpers
public void processNewCustomer(Account account) {
    validateNewCustomer(account);
    enrichFromExternalData(account);
    Account inserted = insertAccount(account);
    createDefaultOpportunity(inserted);
    sendWelcomeNotification(inserted);
}

Access Modifiers

Start with private. Promote to protected, then public, only when necessary. Use global only for managed package APIs.

public with sharing class DiscountCalculator {

    // Private — internal state
    private Decimal baseRate;
    private Map tierRates;

    // Private — internal logic
    private Decimal lookupTierRate(String tier) {
        return tierRates.containsKey(tier) ? tierRates.get(tier) : baseRate;
    }

    // Protected — available to subclasses for extension
    protected Decimal applyMinimumDiscount(Decimal calculated) {
        return Math.max(calculated, 0.05);
    }

    // Public — the contract
    public Decimal calculateDiscount(String customerTier, Decimal orderAmount) {
        Decimal rate = lookupTierRate(customerTier);
        return applyMinimumDiscount(rate * orderAmount);
    }
}

Null Safety

Check for null before dereferencing. Salesforce returns null (not empty collections) for uninitialized parent relationship fields. Child relationship sub-queries return empty lists, not null.

// Preferred — null-safe navigation operator (?.)
String city = account?.BillingAddress?.City;
String ownerEmail = contact?.Account?.Owner?.Email;

// Null-safe Map retrieval with null coalescing (requires minimum API version — see @../_reference/API_VERSIONS.md)
String value = myMap.get('key')?.toLowerCase() ?? '';

> Note: The ?. operator prevents NullPointerException when the object reference is null. It does NOT prevent SObjectException when accessing fields not included in the SOQL query. Always ensure queried fields are in the SELECT clause.


Collection Patterns

Choosing List vs Set vs Map

// List — ordered, allows duplicates, use for DML and output
List accountsToInsert = new List();

// Set — unordered, no duplicates, use for Id lookup sets and deduplication
Set processedIds = new Set();

// Map — key-value lookup, use for joining data across queries
Map accountById = new Map(
    [SELECT Id, Name FROM Account WHERE Id IN :accountIds]
);

Build Maps Inline from Queries

// Idiomatic Apex — Map constructor from query
Map accountMap = new Map(
    [SELECT Id, Name, OwnerId FROM Account WHERE Id IN :accountIds]
);
Account acc = accountMap.get(someId);

> Note: In Apex, new List(n) creates a list pre-filled with n nulls (unlike Java). Use new List() for an empty list.


Comments

Javadoc-Style for Public Methods

Document the contract, not the implementation.

/**
 * Calculates the renewal opportunity amount based on the original contract value
 * and the customer's tier-based renewal discount.
 *
 * @param contract  The original contract record. Must not be null. Must have
 *                  Amount__c and Customer_Tier__c populated.
 * @param renewalDate  The target renewal date. Used to determine active pricing tiers.
 * @return  The calculated renewal amount. Never negative. Returns 0 if contract
 *          amount is null.
 * @throws RenewalCalculationException  If no pricing tier is found for the contract's
 *                                      customer tier value.
 */
public Decimal calculateRenewalAmount(Contract__c contract, Date renewalDate) {
    // implementation
}

Inline Comments

Only when logic is not obvious. Explain why, not what.

// Salesforce does not enforce uniqueness on Name by default; we enforce it
// here because duplicate account names break downstream ERP sync.
if (existingAccountNames.contains(acc.Name)) {
    acc.addError('An account with this name already exists. Use a unique trading name.');
}

Complete Well-Structured Class Example

/**
 * Service class for credit limit management operations on Account records.
 * Enforces sharing rules; operates within the running user's data visibility.
 */
public with sharing class CreditLimitService {

    private static final Decimal DEFAULT_CREDIT_LIMIT    = 10000.00;
    private static final Decimal PREMIUM_CREDIT_LIMIT    = 100000.00;
    private static final String  TIER_PREMIUM            = 'Premium';
    private static final String  TIER_STANDARD           = 'Standard';

    private final List accounts;

    public CreditLimitService(List accounts) {
        if (accounts == null || accounts.isEmpty()) {
            throw new CreditLimitException('Account list must not be null or empty.');
        }
        this.accounts = accounts;
    }

    public Map recalculateLimits() {
        Map results = new Map();
        for (Account acc : accounts) {
            results.put(acc.Id, calculateLimitForAccount(acc));
        }
        return results;
    }

    public List saveLimits(Map limitsByAccountId) {
        List toUpdate = buildUpdateRecords(limitsByAccountId);
        return executeDmlWithErrorCollection(toUpdate);
    }

    private Decimal calculateLimitForAccount(Account acc) {
        if (acc.Customer_Tier__c == TIER_PREMIUM) return PREMIUM_CREDIT_LIMIT;
        if (acc.Customer_Tier__c == TIER_STANDARD) return calculateStandardLimit(acc);
        return DEFAULT_CREDIT_LIMIT;
    }

    private Decimal calculateStandardLimit(Account acc) {
        if (acc.AnnualRevenue == null || acc.AnnualRevenue  buildUpdateRecords(Map limitsByAccountId) {
        List records = new List();
        for (Id accId : limitsByAccountId.keySet()) {
            records.add(new Account(
                Id              = accId,
                CreditLimit__c  = limitsByAccountId.get(accId),
                Last_Credit_Review_Date__c = Date.today()
            ));
        }
        return records;
    }

    private List executeDmlWithErrorCollection(List records) {
        List results = Database.update(records, false);
        List errors = new List();
        for (Integer i = 0; i < results.size(); i++) {
            if (!results[i].isSuccess()) {
                for (Database.Error err : results[i].getErrors()) {
                    errors.add('Account Id ' + records[i].Id + ': ' +
                        err.getStatusCode() + ' — ' + err.getMessage());
                }
            }
        }
        return errors;
    }

    public class CreditLimitException extends Exception {}
}

Related

  • Agent: sf-review-agent — For interactive, in-depth guidance

Guardrails

  • sf-apex-constraints — Enforces governor limits, naming rules, security requirements, and bulkification rules that apply to all Apex code

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.