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). Use when: salesforce, sfdc, apex, lwc, lightning web components.
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;
}
// 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(); }
}
// 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 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);
}
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>([
SELECT Id, Industry FROM Account WHERE Id IN :accIds
]);
for (Opportunity opp : oppsToUpdate) {
opp.Industry__c = accountMap.get(opp.AccountId).Industry;
}
if (!oppsToUpdate.isEmpty()) {
update oppsToUpdate;
}
// Retry with exponential backoff (max 3 retries)
if (retryCount < 3) {
// Chain new job for retry
System.enqueueJob(new IndustryChangeQueueable(accountIds, retryCount + 1));
} else {
// Create error record for monitoring
insert new Integration_Error__c(
Type__c = 'Industry Sync',
Message__c = e.getMessage(),
Stack_Trace__c = e.getStackTraceString(),
Record_Ids__c = String.join(new List<Id>(accountIds), ',')
);
}
}
}
Context
async processing
long-running operations
callouts from triggers
REST API Integration with Connected App
External integrations use Connected Apps with OAuth 2.0. JWT Bearer flow
for server-to-server, Web Server flow for user-facing apps. Always use
Named Credentials for secure callout configuration.
// Node.js - JWT Bearer Flow (server-to-server)
import jwt from 'jsonwebtoken';
import fs from 'fs';
async authenticate(): Promise<void> {
// Check if token is still valid (5 min buffer)
if (this.accessToken && Date.now() < this.tokenExpiry - 300000) {
return;
}
if (response.status === 401) {
// Token expired, clear and retry
this.accessToken = null;
throw new Error('Session expired, retry required');
}
throw new Error(`API Error: ${JSON.stringify(error)}`);
}
}
// Usage
const sf = new SalesforceClient(
process.env.SF_CLIENT_ID!,
process.env.SF_USERNAME!,
'./certificates/server.key'
);
const accounts = await sf.query(
"SELECT Id, Name FROM Account WHERE CreatedDate = TODAY"
);
Context
external integration
REST API access
connected apps
Bulk API 2.0 for Large Data Operations
Use Bulk API 2.0 for operations on 10K+ records. Asynchronous processing
with job-based workflow. Part of REST API with streamlined interface
compared to original Bulk API.
// Node.js - Bulk API 2.0 insert
class SalesforceBulkClient extends SalesforceClient {
Source-driven development with disposable scratch orgs for isolated
testing. Scratch orgs exist 7-30 days and can be created throughout
the day, unlike sandbox refresh limits.
sf org assign permset --name MyApp_Admin --target-org myapp-dev
4. Import sample data
sf data import tree --plan data/sample-data-plan.json --target-org myapp-dev
5. Open org
sf org open --target-org myapp-dev
6. Run tests
sf apex run test
--code-coverage
--result-format human
--wait 10
--target-org myapp-dev
7. Pull changes back
sf project retrieve start --target-org myapp-dev
Context
development workflow
CI/CD
testing
2nd Generation Package (2GP) Development
2GP replaces 1GP with source-driven, modular packaging. Requires Dev Hub
with 2GP enabled, namespace linked, and 75% code coverage for promoted
packages.
Enable Dev Hub and 2GP in Setup:
Setup > Dev Hub > Enable Dev Hub
Setup > Dev Hub > Enable Unlocked Packages and 2GP