Enforces best practices for production-ready Salesforce Apex classes: organization order, error handling, null safety, collection patterns. Not for tests or triggers.
npx claudepluginhub jiten-singh-shahi/salesforce-claude-code --plugin salesforce-claude-codeThis skill is limited to using the following tools:
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.
Enforces Salesforce Apex quality guardrails: bulk-safety (no SOQL/DML in loops), sharing declarations, CRUD/FLS security, SOQL injection prevention, PNB test coverage. Use for reviewing or generating classes, triggers, batches, tests.
Writes and debugs Apex code, builds Lightning Web Components, optimizes SOQL queries, implements triggers, batch jobs, platform events, and Salesforce integrations. Use for CRM workflows, governor limits, bulk processing, and Salesforce DX CI/CD.
Identifies Salesforce pitfalls like SOQL N+1 queries, governor limit violations, API overuse, and SOQL injection during code reviews, onboarding, and integration audits.
Share bugs, ideas, or general feedback.
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/GOVERNOR_LIMITS.md @../_reference/NAMING_CONVENTIONS.md @../_reference/SECURITY_PATTERNS.md
Organize class members in this order:
static final)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<Order__c> orders;
private Map<Id, Account> accountMap;
private OrderValidator validator;
// 4. Constructor
public OrderProcessor(List<Order__c> orders) {
this.orders = orders;
this.accountMap = new Map<Id, Account>();
this.validator = new OrderValidator();
}
// 5. Public methods
public List<ProcessResult> processAll() {
List<ProcessResult> results = new List<ProcessResult>();
loadRelatedAccounts();
for (Order__c order : orders) {
results.add(processSingleOrder(order));
}
return results;
}
// 6. Private methods
private void loadRelatedAccounts() {
Set<Id> accountIds = new Set<Id>();
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;
}
}
}
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 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);
}
When using partial-success DML, check every result.
List<Database.SaveResult> results = Database.insert(accounts, false);
List<String> errors = new List<String>();
for (Integer i = 0; i < results.size(); i++) {
Database.SaveResult result = results[i];
if (!result.isSuccess()) {
for (Database.Error err : result.getErrors()) {
errors.add(
'Record ' + accounts[i].Name + ': ' +
err.getStatusCode() + ' - ' + err.getMessage()
);
}
}
}
if (!errors.isEmpty()) {
throw new AccountServiceException(
'Partial DML failure. Errors:\n' + String.join(errors, '\n')
);
}
Include context in exception messages — what was being done, what record was involved, what the actual error was.
throw new AccountServiceException(
String.format(
'Failed to update Account {0} (Id: {1}) during credit limit recalculation. ' +
'DML error: {2}',
new List<Object>{ account.Name, account.Id, dmlError.getMessage() }
)
);
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() {} }
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);
}
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<String, Decimal> 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);
}
}
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.
// List — ordered, allows duplicates, use for DML and output
List<Account> accountsToInsert = new List<Account>();
// Set — unordered, no duplicates, use for Id lookup sets and deduplication
Set<Id> processedIds = new Set<Id>();
// Map — key-value lookup, use for joining data across queries
Map<Id, Account> accountById = new Map<Id, Account>(
[SELECT Id, Name FROM Account WHERE Id IN :accountIds]
);
// Idiomatic Apex — Map constructor from query
Map<Id, Account> accountMap = new Map<Id, Account>(
[SELECT Id, Name, OwnerId FROM Account WHERE Id IN :accountIds]
);
Account acc = accountMap.get(someId);
Note: In Apex,
new List<String>(n)creates a list pre-filled with n nulls (unlike Java). Usenew List<String>()for an empty list.
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
}
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.');
}
/**
* 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<Account> accounts;
public CreditLimitService(List<Account> accounts) {
if (accounts == null || accounts.isEmpty()) {
throw new CreditLimitException('Account list must not be null or empty.');
}
this.accounts = accounts;
}
public Map<Id, Decimal> recalculateLimits() {
Map<Id, Decimal> results = new Map<Id, Decimal>();
for (Account acc : accounts) {
results.put(acc.Id, calculateLimitForAccount(acc));
}
return results;
}
public List<String> saveLimits(Map<Id, Decimal> limitsByAccountId) {
List<Account> 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 <= 0) return DEFAULT_CREDIT_LIMIT;
return Math.min(acc.AnnualRevenue * 0.05, 50000.00);
}
private List<Account> buildUpdateRecords(Map<Id, Decimal> limitsByAccountId) {
List<Account> records = new List<Account>();
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<String> executeDmlWithErrorCollection(List<Account> records) {
List<Database.SaveResult> results = Database.update(records, false);
List<String> errors = new List<String>();
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 {}
}
sf-review-agent — For interactive, in-depth guidancesf-apex-constraints — Enforces governor limits, naming rules, security requirements, and bulkification rules that apply to all Apex code