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
✓ PassedNo 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.
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:
- Constants (
static final) - Static variables
- Instance variables (fields)
- Constructors
- Public methods
- Private/Protected methods
- 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.
- Author: jiten-singh-shahi
- Source: jiten-singh-shahi/salesforce-claude-code
- License: MIT
- Homepage: https://www.npmjs.com/package/scc-universal
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.