Salesforce Development
Expert patterns for Salesforce platform development including Lightning Web Components (LWC), Apex triggers and classes, REST/Bulk APIs, Connected Apps, and Salesforce DX with scratch orgs and 2nd generation packages (2GP).
Patterns
Lightning Web Component with Wire Service
Use @wire decorator for reactive data binding with Lightning Data Service or Apex methods. @wire fits LWC's reactive architecture and enables Salesforce performance optimizations.
// myComponent.js import { LightningElement, wire, api } from 'lwc'; import { getRecord, getFieldValue } from 'lightning/uiRecordApi'; import getRelatedRecords from '@salesforce/apex/MyController.getRelatedRecords'; import ACCOUNT_NAME from '@salesforce/schema/Account.Name'; import ACCOUNT_INDUSTRY from '@salesforce/schema/Account.Industry';
const FIELDS = [ACCOUNT_NAME, ACCOUNT_INDUSTRY];
export default class MyComponent extends LightningElement { @api recordId; // Passed from parent or record page
// Wire to Lightning Data Service (preferred for single records) @wire(getRecord, { recordId: '$recordId', fields: FIELDS }) account;
// Wire to Apex method (for complex queries) @wire(getRelatedRecords, { accountId: '$recordId' }) wiredRecords({ error, data }) { if (data) { this.relatedRecords = data; this.error = undefined; } else if (error) { this.error = error; this.relatedRecords = undefined; } }
get accountName() { return getFieldValue(this.account.data, ACCOUNT_NAME); }
get isLoading() { return !this.account.data && !this.account.error; }
// Reactive: changing recordId automatically re-fetches }
// myComponent.html <template> <lightning-card title={accountName}> <template if:true={isLoading}> <lightning-spinner alternative-text="Loading"></lightning-spinner> </template>
<template if:true={account.data}>
<p>Industry: {industry}</p>
</template>
<template if:true={error}>
<p class="slds-text-color_error">{error.body.message}</p>
</template>
</lightning-card>
</template>
// MyController.cls public with sharing class MyController { @AuraEnabled(cacheable=true) public static List<Contact> getRelatedRecords(Id accountId) { return [ SELECT Id, Name, Email, Phone FROM Contact WHERE AccountId = :accountId WITH SECURITY_ENFORCED LIMIT 100 ]; } }
Context
- building LWC components
- fetching Salesforce data
- reactive UI
Bulkified Apex Trigger with Handler Pattern
Apex triggers must be bulkified to handle 200+ records per transaction. Use handler pattern for separation of concerns, testability, and recursion prevention.
// AccountTrigger.trigger trigger AccountTrigger on Account ( before insert, before update, before delete, after insert, after update, after delete, after undelete ) { new AccountTriggerHandler().run(); }
// TriggerHandler.cls (base class) public virtual class TriggerHandler { // Recursion prevention private static Set<String> executedHandlers = new Set<String>();
public void run() { String handlerName = String.valueOf(this).split(':')[0];
// Prevent recursion
String contextKey = handlerName + '_' + Trigger.operationType;
if (executedHandlers.contains(contextKey)) {
return;
}
executedHandlers.add(contextKey);
switch on Trigger.operationType {
when BEFORE_INSERT { this.beforeInsert(); }
when BEFORE_UPDATE { this.beforeUpdate(); }
when BEFORE_DELETE { this.beforeDelete(); }
when AFTER_INSERT { this.afterInsert(); }
when AFTER_UPDATE { this.afterUpdate(); }
when AFTER_DELETE { this.afterDelete(); }
when AFTER_UNDELETE { this.afterUndelete(); }
}
}
// Override in child classes protected virtual void beforeInsert() {} protected virtual void beforeUpdate() {} protected virtual void beforeDelete() {} protected virtual void afterInsert() {} protected virtual void afterUpdate() {} protected virtual void afterDelete() {} protected virtual void afterUndelete() {} }
// AccountTriggerHandler.cls public class AccountTriggerHandler extends TriggerHandler { private List<Account> newAccounts; private List<Account> oldAccounts; private Map<Id, Account> newMap; private Map<Id, Account> oldMap;
public AccountTriggerHandler() { this.newAccounts = (List<Account>) Trigger.new; this.oldAccounts = (List<Account>) Trigger.old; this.newMap = (Map<Id, Account>) Trigger.newMap; this.oldMap = (Map<Id, Account>) Trigger.oldMap; }
protected override void afterInsert() { createDefaultContacts(); notifySlack(); }
protected override void afterUpdate() { handleIndustryChange(); }
// BULKIFIED: Query once, update once private void createDefaultContacts() { List<Contact> contactsToInsert = new List<Contact>();
for (Account acc : newAccounts) {
if (acc.Type == 'Prospect') {
contactsToInsert.add(new Contact(
AccountId = acc.Id,
LastName = 'Primary Contact',
Email = 'contact@' + acc.Website
));
}
}
if (!contactsToInsert.isEmpty()) {
insert contactsToInsert; // Single DML for all
}
}
private void handleIndustryChange() { Set<Id> changedAccountIds = new Set<Id>();
for (Account acc : newAccounts) {
Account oldAcc = oldMap.get(acc.Id);
if (acc.Industry != oldAcc.Industry) {
changedAccountIds.add(acc.Id);
}
}
if (!changedAccountIds.isEmpty()) {
// Queue async processing for heavy work
System.enqueueJob(new IndustryChangeQueueable(changedAccountIds));
}
}
private void notifySlack() { // Offload callouts to async List<Id> accountIds = new List<Id>(newMap.keySet()); System.enqueueJob(new SlackNotificationQueueable(accountIds)); } }
Context
- apex triggers
- data operations
- automation
Queueable Apex for Async Processing
Use Queueable Apex for async processing with support for non-primitive types, monitoring via AsyncApexJob, and job chaining. Limit: 50 jobs per transaction, 1 child job when chaining.
// IndustryChangeQueueable.cls public class IndustryChangeQueueable implements Queueable, Database.AllowsCallouts { private Set<Id> accountIds; private Integer retryCount;
public IndustryChangeQueueable(Set<Id> accountIds) { this(accountIds, 0); }
public IndustryChangeQueueable(Set<Id> accountIds, Integer retryCount) { this.accountIds = accountIds; this.retryCount = retryCount; }
public void execute(QueueableContext context) { try { // Query with fresh data List<Account> accounts = [ SELECT Id, Name, Industry, OwnerId FROM Account WHERE Id IN :accountIds WITH SECURITY_ENFORCED ];
// Process and make callout
for (Account acc : accounts) {
syncToExternalSystem(acc);
}
// Update records
updateRelatedOpportunities(accountIds);
} catch (Exception e) {
handleError(e);
}
}
private void syncToExternalSystem(Account acc) { HttpRequest req = new HttpRequest(); req.setEndpoint('callout:ExternalCRM/accounts'); req.setMethod('POST'); req.setHeader('Content-Type', 'application/json'); req.setBody(JSON.serialize(new Map<String, Object>{ 'salesforceId' => acc.Id, 'name' => acc.Name, 'industry' => acc.Industry }));
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() != 200 && res.getStatusCode() != 201) {
throw new CalloutException('Sync failed: ' + res.getBody());
}
}
private void updateRelatedOpportunities(Set<Id> accIds) { List<Opportunity> oppsToUpdate = [ SELECT Id, Industry__c, AccountId FROM Opportunity WHERE AccountId IN :accIds WITH SECURITY_ENFORCED ];
Map<Id, Account> accountMap = new Map<Id, Account>([
S