Q1. Scenario: You need to restrict record access so that Sales Reps can only see their own Opportunities, but Managers can see their team’s Opportunities. How will you achieve this?
👉 Answer:
📌 Example:
Q2. Scenario: A user should be able to edit a record only if a custom checkbox “Editable__c” is TRUE. How will you enforce this?
👉 Answer:
AND(
ISCHANGED(Status__c),
NOT(Editable__c)
)
📌 Why this works: Validation rules are evaluated before save, ensuring no unwanted updates slip through.
Q3. Scenario: You need to send an email alert when a high-value Opportunity is created (> $1M). How will you do this?
👉 Answer:
Amount > 1000000
.📌 Best Practice: Don’t use Workflow (deprecated). Flows are future-proof and more powerful.
Q4. Scenario: You need to update a field on Case when related Account field changes. Which automation will you choose?
👉 Answer:
Update Records
element.📌 Why Flow instead of Trigger?
Q5. Scenario: You need to mass-update 100,000 records. Which tool will you use?
👉 Answer:
📌 Code Example (Batch Apex)
global class UpdateBatch implements Database.Batchable<sObject> {
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id FROM Account WHERE Status__c = \'Active\'');
}
global void execute(Database.BatchableContext bc, List<Account> accList) {
for(Account acc : accList){
acc.Rating = 'Hot';
}
update accList;
}
global void finish(Database.BatchableContext bc) {}
}
Q6. Scenario: You need to assign a task automatically when a new Lead is created.
👉 Answer:
📌 Why not Process Builder?
Q7. Scenario: Business wants to auto-assign Accounts to different queues based on Region.
👉 Answer:
📌 Pro Tip: Assignment Rules can also be used for Leads/Cases, but for Account you’ll need Flow.
Q8. Scenario: You need to track changes in a field (audit log).
👉 Answer:
Options:
📌 Example: Track StageName
changes in Opportunity to measure sales cycle.
Q9. Scenario: You need to restrict a picklist to only a few values for certain profiles.
👉 Answer:
📌 Example:
Q10. Scenario: Two users need different page layouts for the same object.
👉 Answer:
📌 Example:
Q11. Scenario: Insert Account & User in same transaction → Mix DML error. How do you fix?
👉 Answer:
📌 Code Example:
public class UserHandler {
@future
public static void createUser(Id accId) {
Account acc = [SELECT Name FROM Account WHERE Id = :accId LIMIT 1];
User u = new User(
FirstName = 'Test',
LastName = acc.Name,
Alias = 'tuser',
Email = 'test@test.com',
Username = 'testuser@test.com',
ProfileId = [SELECT Id FROM Profile WHERE Name='Standard User' LIMIT 1].Id,
TimeZoneSidKey = 'Asia/Kolkata',
LocaleSidKey = 'en_US',
EmailEncodingKey='UTF-8',
LanguageLocaleKey='en_US'
);
insert u;
}
}
Q12. Scenario: You need to stop users from deleting Accounts if related Opportunities exist.
👉 Answer:
before delete
Trigger → check related Opportunities.addError()
.📌 Code Example:
trigger AccountBeforeDelete on Account (before delete) {
for(Account acc : Trigger.old){
if([SELECT COUNT() FROM Opportunity WHERE AccountId = :acc.Id] > 0){
acc.addError('Cannot delete Account with related Opportunities.');
}
}
}
Q13. Scenario: When Account Industry changes, update all child Contacts with the same Industry.
👉 Answer:
after update
Trigger on Account.📌 Code Example:
trigger UpdateContactsIndustry on Account (after update) {
List<Contact> consToUpdate = new List<Contact>();
for(Account acc : Trigger.new){
Account oldAcc = Trigger.oldMap.get(acc.Id);
if(acc.Industry != oldAcc.Industry){
for(Contact con : [SELECT Id FROM Contact WHERE AccountId=:acc.Id]){
con.Industry__c = acc.Industry;
consToUpdate.add(con);
}
}
}
if(!consToUpdate.isEmpty()) update consToUpdate;
}
Q14. Scenario: Prevent duplicate Contacts with same Email.
👉 Answer:
trigger ContactDuplicateCheck on Contact (before insert, before update) {
Set<String> emailSet = new Set<String>();
for(Contact c : Trigger.new){
if(c.Email != null) emailSet.add(c.Email);
}
Map<String, Contact> dupMap = new Map<String, Contact>();
for(Contact existing : [SELECT Email FROM Contact WHERE Email IN :emailSet]){
dupMap.put(existing.Email, existing);
}
for(Contact c : Trigger.new){
if(dupMap.containsKey(c.Email)){
c.addError('Duplicate Email Found!');
}
}
}
Q15. Scenario: Trigger should run only once, even if multiple DMLs occur.
👉 Answer:
public class TriggerHelper {
public static Boolean firstRun = true;
}
trigger AccountTrigger on Account (after insert) {
if(TriggerHelper.firstRun){
TriggerHelper.firstRun = false;
// logic here
}
}
Q16. Scenario: You need to handle bulk insert of 200 Accounts and update related Contacts.
👉 Answer:
trigger AccountBulkHandler on Account (after insert) {
Map<Id, Account> accMap = new Map<Id, Account>(Trigger.new);
List<Contact> cons = [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accMap.keySet()];
for(Contact c : cons){
c.Description = 'Updated via bulk insert';
}
update cons;
}
Q17. Scenario: Need to log deleted records for auditing.
👉 Answer:
after delete
trigger with Trigger.old
.trigger AccountDeleteAudit on Account (after delete) {
List<Audit__c> audits = new List<Audit__c>();
for(Account acc : Trigger.old){
audits.add(new Audit__c(Name='Deleted '+acc.Name, AccountId__c=acc.Id));
}
insert audits;
}
Q18. Scenario: You must prevent recursion in Trigger calling a Flow.
👉 Answer:
if(TriggerHelper.firstRun){
TriggerHelper.firstRun = false;
Flow.Interview.MyFlow flow = new Flow.Interview.MyFlow(vars);
flow.start();
}
Q19. Scenario: Trigger should update related records but skip if update is already from automation.
👉 Answer:
Updated_By_Flow__c
to differentiate.Q20. Scenario: Insert Case when Account Type = “Customer”.
👉 Answer:
trigger CreateCaseOnAccount on Account (after insert) {
List<Case> cases = new List<Case>();
for(Account acc : Trigger.new){
if(acc.Type=='Customer'){
cases.add(new Case(Subject='New Customer Case', AccountId=acc.Id));
}
}
if(!cases.isEmpty()) insert cases;
}
Q21. Scenario: Update Account Rating when 5+ Opportunities are Closed Won.
👉 Answer:
trigger UpdateAccountRating on Opportunity (after insert, after update) {
Map<Id, Integer> accOppCount = new Map<Id, Integer>();
for(AggregateResult ar : [
SELECT AccountId accId, COUNT(Id) cnt
FROM Opportunity
WHERE StageName='Closed Won'
GROUP BY AccountId
]){
accOppCount.put((Id)ar.get('accId'), (Integer)ar.get('cnt'));
}
List<Account> accToUpdate = new List<Account>();
for(Id accId : accOppCount.keySet()){
if(accOppCount.get(accId) >= 5){
accToUpdate.add(new Account(Id=accId, Rating='Hot'));
}
}
update accToUpdate;
}
Q22. Scenario: Assign default Contact when new Account created.
👉 Answer:
trigger DefaultContact on Account (after insert) {
List<Contact> cons = new List<Contact>();
for(Account acc : Trigger.new){
cons.add(new Contact(LastName='Default Contact', AccountId=acc.Id));
}
insert cons;
}
Q23. Scenario: Prevent Opportunity close if Quote not approved.
👉 Answer:
trigger OppStageCheck on Opportunity (before update) {
for(Opportunity opp : Trigger.new){
if(opp.StageName=='Closed Won'){
Integer quoteCount = [SELECT COUNT() FROM Quote WHERE OpportunityId=:opp.Id AND Status='Approved'];
if(quoteCount==0){
opp.addError('Cannot close Opportunity until Quote approved.');
}
}
}
}
Q24. Scenario: Auto-create Renewal Opportunity when current Opp is Closed Won.
👉 Answer:
trigger RenewalOpp on Opportunity (after update) {
List<Opportunity> renewals = new List<Opportunity>();
for(Opportunity opp : Trigger.new){
if(opp.StageName=='Closed Won' && Trigger.oldMap.get(opp.Id).StageName!='Closed Won'){
Opportunity newOpp = opp.clone(false,true);
newOpp.StageName = 'Prospecting';
newOpp.CloseDate = System.today().addMonths(12);
renewals.add(newOpp);
}
}
insert renewals;
}
Q25. Scenario: Prevent delete of Product if used in Opportunity Line Item.
👉 Answer:
trigger PreventProductDelete on Product2 (before delete) {
for(Product2 p : Trigger.old){
if([SELECT COUNT() FROM OpportunityLineItem WHERE Product2Id=:p.Id] > 0){
p.addError('Cannot delete Product as it is used in Opportunity Line Items.');
}
}
}
Q26. Scenario: You need to bulk update 10k Accounts → CPU limit hit.
👉 Answer:
Q27. Scenario: Calculate average discount across Opportunity Line Items.
👉 Answer:
AggregateResult[] results = [
SELECT OpportunityId, AVG(Discount__c) avgDiscount
FROM OpportunityLineItem
GROUP BY OpportunityId
];
Q28. Scenario: Stop users from updating Opportunity Stage backward.
👉 Answer:
trigger PreventStageBackwards on Opportunity (before update) {
for(Opportunity opp : Trigger.new){
Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
if(oldOpp.StageName=='Closed Won' && opp.StageName!='Closed Won'){
opp.addError('Stage cannot move backwards.');
}
}
}
Q29. Scenario: Ensure Account Name always starts with Region Code.
👉 Answer:
trigger AccountNameCheck on Account (before insert, before update) {
for(Account acc : Trigger.new){
if(!acc.Name.startsWith(acc.Region__c)){
acc.Name = acc.Region__c + '-' + acc.Name;
}
}
}
Q30. Scenario: Cascade delete related records when Parent custom object deleted.
👉 Answer:
after delete
trigger.trigger ParentDelete on Parent__c (after delete) {
delete [SELECT Id FROM Child__c WHERE Parent__c IN :Trigger.oldMap.keySet()];
}
Answer: Use an anti-join (semi-join with NOT IN
) to find parents without children.
SOQL
SELECT Id, Name
FROM Account
WHERE Id NOT IN (SELECT AccountId FROM Contact)
Apex (render list)
List<Account> noContactAccs = [
SELECT Id, Name
FROM Account
WHERE Id NOT IN (SELECT AccountId FROM Contact)
LIMIT 5000
];
Best practices
Answer: Use aggregate SOQL and group by both AccountId
and StageName
.
SOQL
SELECT AccountId, StageName, COUNT(Id) cnt
FROM Opportunity
WHERE IsClosed = FALSE
GROUP BY AccountId, StageName
Apex (convert to a nested map)
Map<Id, Map<String, Integer>> byAccStage = new Map<Id, Map<String, Integer>>();
for (AggregateResult ar : [
SELECT AccountId acc, StageName stg, COUNT(Id) cnt
FROM Opportunity
WHERE IsClosed = FALSE
GROUP BY AccountId, StageName
]) {
Id accId = (Id) ar.get('acc');
String stage = (String) ar.get('stg');
Integer count = (Integer) ar.get('cnt');
byAccStage.putIfAbsent(accId, new Map<String, Integer>());
byAccStage.get(accId).put(stage, count);
}
Best practices
Answer: Use FOR UPDATE
to get a row lock.
Apex
List<Account> accs = [
SELECT Id, Name
FROM Account
WHERE Id IN :accIds
FOR UPDATE
];
// safe to modify now
for (Account a : accs) a.Rating = 'Hot';
update accs;
Best practices
UNABLE_TO_LOCK_ROW
errors (e.g., show a friendly message, retry via async).Answer: Sort + limit.
SELECT Id, Name, AnnualRevenue
FROM Account
WHERE AnnualRevenue != NULL
ORDER BY AnnualRevenue DESC
LIMIT 5
Tip: Use a selective WHERE to avoid scanning huge tables.
Answer: Group and HAVING.
SELECT Email, COUNT(Id) dupCount
FROM Contact
WHERE Email != NULL
GROUP BY Email
HAVING COUNT(Id) > 1
Apex (flag dupes)
Set<String> dupEmails = new Set<String>();
for (AggregateResult ar : [
SELECT Email em, COUNT(Id) c FROM Contact
WHERE Email != NULL GROUP BY Email HAVING COUNT(Id) > 1
]) dupEmails.add((String) ar.get('em'));
Best practices
Answer: Use date literals (fast & index-friendly).
SELECT Id, Name, Amount, CloseDate
FROM Opportunity
WHERE IsClosed = TRUE
AND CloseDate = LAST_MONTH
Tip: Date literals (e.g., YESTERDAY
, LAST_N_DAYS:30
, THIS_FISCAL_QUARTER
) are optimized.
Answer: Child-to-parent dot notation.
SELECT Id, LastName, Email, Account.Name, Account.Industry, Account.Owner.Name
FROM Contact
WHERE Account.Industry = 'Banking'
Best practices
Answer: Parent-to-child subquery.
SELECT Id, Name,
(SELECT Id, LastName, Email FROM Contacts)
FROM Account
WHERE Rating = 'Hot'
Apex
for (Account a : [
SELECT Id, Name, (SELECT Id, LastName, Email FROM Contacts)
FROM Account WHERE Rating = 'Hot'
]) {
for (Contact c : a.Contacts) {
// process
}
}
Note: Subqueries return a QueryResult list on the relationship name.
Answer: Use indexed fields and sargable predicates.
Do this
LIKE :term + '%'
) instead of %term%
.Example
SELECT Id, Name
FROM Account
WHERE IsActive__c = TRUE
AND Name LIKE :namePrefix // 'Acme%'
AND LastModifiedDate = LAST_N_DAYS:30
Best practices
Answer: Semi-join with IN
.
SELECT Id, Name
FROM Account
WHERE Id IN (SELECT AccountId FROM Opportunity WHERE IsClosed = FALSE)
Tip: Semi-joins are very efficient versus doing two queries and intersecting in Apex.
Answer: Use an anti-join with a filtered subquery.
SELECT Id, Name
FROM Account
WHERE Id NOT IN (
SELECT AccountId FROM Opportunity WHERE IsClosed = FALSE
)
Best practices
Answer: Prefer keyset pagination over OFFSET
(which is capped ~2K and slower).
Keyset approach
// First page
List<Account> page1 = [
SELECT Id, Name FROM Account
WHERE Name >= :startKey
ORDER BY Name ASC
LIMIT :pageSize
];
// Next page: pass last record’s Name as new startKey
Best practices
Answer: Put the parent predicate in the WHERE via dot notation.
SELECT Id, LastName, Email
FROM Contact
WHERE Account.Industry = 'Banking'
AND Account.Active__c = TRUE
Tip: This avoids an extra query and keeps logic declarative.
Answer: First gather keys, then bulk query once, then map.
Bad (don’t do)
for (Contact c : Trigger.new) {
Account a = [SELECT Name FROM Account WHERE Id = :c.AccountId]; // in loop ❌
}
Good
Set<Id> accIds = new Set<Id>();
for (Contact c : Trigger.new) if (c.AccountId != null) accIds.add(c.AccountId);
Map<Id, Account> accMap = new Map<Id, Account>([
SELECT Id, Name FROM Account WHERE Id IN :accIds
]);
for (Contact c : Trigger.new) {
Account a = accMap.get(c.AccountId);
// use a safely
}
Answer: Use SOSL for full-text, multi-object search.
List<List<SObject>> sr = [
FIND :searchTerm IN NAME FIELDS
RETURNING
Account(Id, Name ORDER BY Name LIMIT 10),
Contact(Id, Name, Email ORDER BY Name LIMIT 10),
Opportunity(Id, Name ORDER BY Name LIMIT 10)
];
List<Account> accs = (List<Account>) sr[0];
Best practices
Answer: Mark a field as External Id and use upsert
.
List<Product2> items = new List<Product2>{
new Product2(SKU__c='SKU-1001', Name='Cable'),
new Product2(SKU__c='SKU-1002', Name='Adapter')
};
Database.UpsertResult[] res = Database.upsert(items, Product2.SKU__c, false);
Best practices
Answer: Use Database.insert
/update
with allOrNone=false
.
Database.SaveResult[] results = Database.insert(records, /* allOrNone */ false);
for (Integer i=0; i<results.size(); i++) {
if (!results[i].isSuccess()) {
for (Database.Error e : results[i].getErrors()) {
System.debug('Row ' + i + ' failed: ' + e.getMessage());
}
}
}
Tip: Always log failed Ids/payloads for supportability.
Answer: Use Batch Apex with a QueryLocator (streams server-side).
global class BigQueryBatch implements Database.Batchable<SObject> {
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id FROM Contact WHERE LastModifiedDate = LAST_N_DAYS:365'
);
}
global void execute(Database.BatchableContext bc, List<Contact> scope) {
// process scope (<= 200 each)
}
global void finish(Database.BatchableContext bc) {}
}
Best practices
Answer: Mitigate ownership & lookup skew.
What to do
Detection
Answer: Make the query selective or move to async.
Fixes
IsActive__c
, CreatedDate
, External Id
).Example (selective)
SELECT Id FROM Case
WHERE Status = 'New'
AND Priority IN ('High','Critical')
AND CreatedDate = LAST_N_DAYS:7
Answer: Use a wired Apex/Ui API adapter and call refreshApex
after DML or a modal save.
Code (Apex)
public with sharing class AccountCtrl {
@AuraEnabled(cacheable=true)
public static List<Account> getHotAccounts() {
return [SELECT Id, Name, Rating FROM Account WHERE Rating = 'Hot' ORDER BY Name LIMIT 100];
}
}
Code (LWC JS)
import { LightningElement, wire, track } from 'lwc';
import getHotAccounts from '@salesforce/apex/AccountCtrl.getHotAccounts';
import { refreshApex } from '@salesforce/apex';
export default class HotAccountList extends LightningElement {
@track rows = [];
wiredResult;
@wire(getHotAccounts)
wiredAccs(result) {
this.wiredResult = result;
if (result.data) this.rows = result.data;
}
async handleSaved() {
await refreshApex(this.wiredResult);
}
}
Code (LWC HTML)
<template>
<lightning-button label="Refresh" >
Best practices
getRecord
, getListUi
), also use refreshApex
after updateRecord
.Answer: Prefer keyset pagination (cursor-based) over OFFSET
. Pass the last key to Apex and fetch the next slice.
Code (Apex)
public with sharing class AccountPageCtrl {
@AuraEnabled(cacheable=true)
public static List<Account> pageByName(String lastNameKey, Integer pageSize) {
String key = String.isBlank(lastNameKey) ? '' : lastNameKey;
return [
SELECT Id, Name, Phone
FROM Account
WHERE Name > :key
ORDER BY Name ASC
LIMIT :pageSize
];
}
}
Code (LWC JS)
import { LightningElement, track } from 'lwc';
import pageByName from '@salesforce/apex/AccountPageCtrl.pageByName';
export default class AccountPager extends LightningElement {
@track rows = [];
lastKey = '';
pageSize = 20;
hasMore = true;
connectedCallback() {
this.loadMore();
}
async loadMore() {
if (!this.hasMore) return;
const data = await pageByName({ lastNameKey: this.lastKey, pageSize: this.pageSize });
this.rows = [...this.rows, ...data];
this.lastKey = data.length ? data[data.length - 1].Name : this.lastKey;
this.hasMore = data.length === this.pageSize;
}
}
Best practices
Answer: Use Ui Object Info API: getObjectInfo
→ get defaultRecordTypeId
, then getPicklistValues
.
Code (LWC JS)
import { LightningElement, wire, track } from 'lwc';
import { getObjectInfo, getPicklistValues } from 'lightning/uiObjectInfoApi';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
export default class IndustryPicklist extends LightningElement {
@track options = [];
recordTypeId;
@wire(getObjectInfo, { objectApiName: ACCOUNT_OBJECT })
objInfo({ data }) {
if (data) this.recordTypeId = data.defaultRecordTypeId;
}
@wire(getPicklistValues, { recordTypeId: '$recordTypeId', fieldApiName: INDUSTRY_FIELD })
wiredPicklist({ data }) {
if (data) this.options = data.values.map(v => ({ label: v.label, value: v.value }));
}
}
Code (LWC HTML)
<template>
<lightning-combobox name="industry" label="Industry" options={options}></lightning-combobox>
</template>
Best practices
Answer: Use Lightning Message Service (LMS) with a custom message channel.
Metadata — messageChannels/AppBus.messageChannel-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
<masterLabel>AppBus</masterLabel>
<isExposed>true</isExposed>
<description>Cross-component bus</description>
<lightningMessageFields>
<fieldName>recordId</fieldName>
<description>Selected record Id</description>
</lightningMessageFields>
</LightningMessageChannel>
Publisher (LWC JS)
import { LightningElement, wire } from 'lwc';
import { MessageContext, publish } from 'lightning/messageService';
import APP_BUS from '@salesforce/messageChannel/AppBus__c';
export default class Publisher extends LightningElement {
@wire(MessageContext) context;
notifySelect(event) {
publish(this.context, APP_BUS, { recordId: event.detail.id });
}
}
Subscriber (LWC JS)
import { LightningElement, wire } from 'lwc';
import { MessageContext, subscribe, APPLICATION_SCOPE } from 'lightning/messageService';
import APP_BUS from '@salesforce/messageChannel/AppBus__c';
export default class Subscriber extends LightningElement {
@wire(MessageContext) context;
subscription;
connectedCallback() {
this.subscription = subscribe(
this.context,
APP_BUS,
(message) => this.handle(message),
{ scope: APPLICATION_SCOPE }
);
}
handle(message) {
// use message.recordId
}
}
Best practices
disconnectedCallback
if you create custom subscriptions.Answer: Toggle an isLoading
flag; show <lightning-spinner>
until promise resolves.
Code (LWC JS)
import { LightningElement, track } from 'lwc';
import doWork from '@salesforce/apex/HeavyCtrl.doWork';
export default class SpinnerDemo extends LightningElement {
@track isLoading = false;
async run() {
try {
this.isLoading = true;
await doWork();
} finally {
this.isLoading = false;
}
}
}
Code (LWC HTML)
<template>
<lightning-button label="Run" >
Best practices
finally
to avoid stuck UI.Answer: Import the resource with @salesforce/resourceUrl
and load with loadStyle
/loadScript
.
Code (LWC JS)
import { LightningElement } from 'lwc';
import { loadStyle, loadScript } from 'lightning/platformResourceLoader';
import APP_CSS from '@salesforce/resourceUrl/appStyles';
import CHARTJS from '@salesforce/resourceUrl/chartjs'; // zipped & namespaced
export default class StaticResDemo extends LightningElement {
rendered = false;
renderedCallback() {
if (this.rendered) return;
this.rendered = true;
Promise.all([
loadStyle(this, APP_CSS),
loadScript(this, CHARTJS + '/chart.umd.js')
]).catch(err => { /* handle */ });
}
}
Best practices
Answer: Expose @api
props and slots. Consumers pass content; the card keeps styling.
Reusable Card (LWC HTML)
<template>
<section class="card">
<header class="head">
<h2>{title}</h2>
<slot name="actions"></slot>
</header>
<div class="body">
<slot></slot>
</div>
</section>
</template>
Reusable Card (LWC JS)
import { LightningElement, api } from 'lwc';
export default class UiCard extends LightningElement {
@api title = 'Card';
}
Consumer
<c-ui-card title="Hot Accounts">
<lightning-button slot="actions" label="New"></lightning-button>
<ul>
<template for:each={rows} for:item="r">
<li key={r.Id}>{r.Name}</li>
</template>
</ul>
</c-ui-card>
Best practices
Answer: Use scoped CSS (.css
next to the component). For theming, use :host
and variants.
CSS
:host {
display: block;
}
:host([variant="brand"]) .btn {
font-weight: 600;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
}
HTML
<template>
<button class="btn">Click</button>
</template>
Best practices
Answer: Guard loading with a flag; then initialize the library.
Code (LWC JS)
import { LightningElement } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import CHARTJS from '@salesforce/resourceUrl/chartjs';
export default class ChartDemo extends LightningElement {
isLibReady = false;
chart;
async renderedCallback() {
if (this.isLibReady) return;
await loadScript(this, CHARTJS + '/chart.umd.js');
this.isLibReady = true;
this.initChart();
}
initChart() {
const ctx = this.template.querySelector('canvas').getContext('2d');
this.chart = new window.Chart(ctx, {
type: 'bar',
data: { labels: ['A','B','C'], datasets: [{ data: [3,7,5] }] },
options: { responsive: true }
});
}
}
HTML
<template>
<canvas style="max-width:600px;"></canvas>
</template>
Best practices
Answer: Use <lightning-record-form>
(quickest) or createRecord/updateRecord
for custom UX.
Quick form (HTML)
<template>
<lightning-record-form
record-id={recordId}
object-api-name="Account"
layout-type="Full"
mode="edit"
onsuccess={handleSuccess}>
</lightning-record-form>
</template>
Custom save (JS)
import { LightningElement, api } from 'lwc';
import { updateRecord } from 'lightning/uiRecordApi';
import NAME from '@salesforce/schema/Account.Name';
import ID from '@salesforce/schema/Account.Id';
export default class UiSaveDemo extends LightningElement {
@api recordId;
async save(name) {
const fields = {};
fields[ID.fieldApiName] = this.recordId;
fields[NAME.fieldApiName] = name;
await updateRecord({ fields });
// toast + refreshApex wired adapters if any
}
}
Best practices
onsuccess
to show toast and trigger refreshApex
.Answer: Use an After-Save Record-Triggered Flow on Account.
Design
ISCHANGED({!$Record.Phone})
and NOT(ISBLANK({!$Record.Phone}))
.AccountId = {!$Record.Id}
.Phone = {!$Record.Phone}
for all in the collection.Best practices
Answer: After-Save Flow on Opportunity.
Design
ISCHANGED(StageName) && StageName = 'Closed Won'
.{$Record.Id}
{$Record.OwnerId}
Best practices
Answer: Screen Flow launched from a button/quick action.
Design
Best practices
Answer: Use Flow → Invocable Apex that performs the callout and returns success status; implement retry in Apex or via Flow loop with a counter.
Invocable Apex (Named Credential recommended)
public with sharing class FxRateAction {
public class Request { @InvocableVariable public String base; public String target; }
public class Response { @InvocableVariable public Boolean success; @InvocableVariable public Decimal rate; @InvocableVariable public String message; }
@InvocableMethod(label='Get FX Rate' callout=true)
public static List<Response> getRate(List<Request> reqs){
List<Response> out = new List<Response>();
for (Request r : reqs) {
try {
HttpRequest h = new HttpRequest();
h.setEndpoint('callout:FX_API/rate?base=' + r.base + '&target=' + r.target);
h.setMethod('GET');
Http http = new Http();
HttpResponse res = http.send(h);
if (res.getStatusCode()==200) {
// parse JSON (pseudo)
Decimal rate = (Decimal) JSON.deserializeUntyped(res.getBody()).get('rate');
out.add(new Response(true, rate, 'OK'));
} else out.add(new Response(false, null, 'HTTP ' + res.getStatus()));
} catch (Exception e) {
out.add(new Response(false, null, e.getMessage()));
}
}
return out;
}
}
Flow
success = false
and attempts < 3
→ wait Scheduled Path (e.g., 5 minutes) → retry.Best practices
Answer: After-Save Flow with Collection Update.
Design
Tier__c
changedTier__c = {!$Record.Tier__c}
Best practices
Answer: Record-Triggered Flow on Case (Before-Save).
Design
Priority = 'High'
→ OwnerId = {!$Record.Queue_High_Id__c}
Priority = 'Medium'
→ set to medium queueBest practices
Answer: Scheduled-Triggered Flow (or Scheduled Path on an object if event-driven).
Design
Health_Score__c
Best practices
Answer: Build a Subflow and call it from parent flows.
Design
Best practices
Answer: Use Fault Paths on Actions/Get/Update elements and store error text in a variable; show via Screen (for Screen Flows) or Post a Platform Notification/Email (for background flows).
Pattern
varErrorMsg = FLOW_FAULT_MESSAGE
.Best practices
Answer: After-Save Flow on Account → Send Email or Post to Slack via Invocable Action.
Design
ISCHANGED(Risk__c) && Risk__c = 'High'
Best practices
Answer: Record-Triggered Flow on Lead with condition “When Converted = TRUE”.
Design
IsConverted = TRUE
and ConvertedContactId = null
Best practices
Answer: Use Invocable Apex (callout=true) + Named Credential. (Pattern similar to Q64, but returns KYC status + reason.)
Response contract
public class KycResult { @InvocableVariable public Boolean ok; @InvocableVariable public String reason; }
Flow
ok = false
show Screen to collect docs.Best practices
Answer: Use Component Visibility and Decision.
Design
{!isEnterprise} = TRUE
.Best practices
Answer: Gate with Custom Permission.
Design
Can_Run_Discount_Override
HasCustomPermission('Can_Run_Discount_Override') = TRUE
→ allow branch; else show message.Best practices
Answer: Use Scheduled Flow with paging (or Batch Apex for very large sets).
Design (Flow)
Best practices
Answer: Use Batch Apex with a QueryLocator (streams server-side) and small scope (e.g., 200).
Code
global class RecomputeScoresBatch implements Database.Batchable<SObject>, Database.Stateful {
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id FROM Account WHERE IsActive__c = TRUE');
}
global void execute(Database.BatchableContext bc, List<Account> scope) {
for (Account a : scope) { a.Health_Score__c = compute(a); }
update scope;
}
global void finish(Database.BatchableContext bc) {
// email summary, chain another batch, etc.
}
Decimal compute(Account a) { return 80; }
}
Best practices
Answer: Use Queueable Apex and enqueue the next job in finish
or at the end of execute
.
Code
public class Step1 implements Queueable {
public void execute(QueueableContext qc) {
// work...
System.enqueueJob(new Step2());
}
}
public class Step2 implements Queueable { public void execute(QueueableContext qc) { /* work */ } }
Best practices
Answer: Implement Schedulable and schedule with a CRON.
Code
global class NightlyJob implements Schedulable {
global void execute(SchedulableContext sc) {
Database.executeBatch(new RecomputeScoresBatch(), 200);
}
}
// One-time setup (e.g., in Anonymous Apex)
String cron = '0 0 2 * * ?'; // 2:00 AM every day (org time zone)
System.schedule('Nightly Recompute', cron, new NightlyJob());
Best practices
Answer: Use Queueable implements Database.AllowsCallouts (more flexible than @future
).
Code
public class PostToWebhook implements Queueable, Database.AllowsCallouts {
String payload;
public PostToWebhook(String json){ this.payload = json; }
public void execute(QueueableContext qc){
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:MY_WEBHOOK'); // Named Credential
req.setMethod('POST'); req.setHeader('Content-Type','application/json');
req.setBody(payload);
new Http().send(req);
}
}
Best practices
Answer: Use Continuation (supported in Visualforce and Aura).
Code (Apex)
public with sharing class LongCallController {
public Continuation cont;
@AuraEnabled
public static Object doCallout() {
Continuation c = new Continuation(120); // timeout seconds
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:LONG_API'); req.setMethod('GET');
String label = c.addHttpRequest(req);
return c; // framework resumes later and returns the response
}
}
Best practices
Answer: Use Platform Events and publish from Apex/Flow.
Code (Publish)
Opportunity_Won__e evt = new Opportunity_Won__e(
OpportunityId__c = opp.Id,
Amount__c = opp.Amount,
OwnerEmail__c = opp.Owner.Email
);
Database.SaveResult sr = EventBus.publish(evt);
Best practices
Answer: Log attempts in a custom object and re-enqueue Queueable/Schedulable.
Pattern
Attempts__c
, compute Next_Run_At__c
(e.g., 5m, 15m, 1h).Code (retry calculator)
Integer nextDelayMins(Integer attempts) {
Integer[] backoff = new Integer[]{5, 15, 60, 180}; // cap at 3h
return backoff[Math.min(attempts, backoff.size()-1)];
}
Best practices
sleep()
in Apex. Use Schedules or Platform Events.Answer: Use Batch Apex + Messaging.SingleEmailMessage
or Email Alerts via Flow.
Code (batch snippet)
Messaging.SingleEmailMessage m = new Messaging.SingleEmailMessage();
m.setToAddresses(new String[]{'user@x.com'});
m.setSubject('Hello'); m.setPlainTextBody('Body');
Messaging.sendEmail(new Messaging.SingleEmailMessage[]{ m }, false);
Best practices
Answer: Mark ExternalId__c
on your object and upsert.
Code
List<Order__c> orders = new List<Order__c>();
orders.add(new Order__c(ExternalId__c='ORD-1001', Amount__c=500));
Database.UpsertResult[] res = Database.upsert(orders, Order__c.ExternalId__c, false);
Best practices
Answer: Publish Platform Events on business milestones and have ERP subscribe (Pub/Sub API or CometD).
Pattern
Opportunity_Won__e
Best practices
Answer: Use Bulk API v2 (preferred for large loads) or Apex + Batch if transforming in-org.
Tips
Answer: Use Named Credentials + Auth Provider. Call with callout:NAME
.
Code
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:ERP_API/orders'); // Named Credential handles OAuth
req.setMethod('GET');
HttpResponse res = new Http().send(req);
Best practices
Answer: Use Database.insert/update
with allOrNone=false
and capture SaveResults.
Code
Database.SaveResult[] results = Database.update(records, false);
Integer ok=0, fail=0;
for (Integer i=0; i<results.size(); i++) {
if (results[i].isSuccess()) ok++;
else {
fail++;
for (Database.Error e : results[i].getErrors()) {
// log record Id + e.getStatusCode() + e.getMessage()
}
}
}
// email ok/fail counts in finish()
Best practices
Answer: Queueable callout to Slack Incoming Webhook (via Named Credential).
Code
public class SlackNotify implements Queueable, Database.AllowsCallouts {
Case c; public SlackNotify(Case c){ this.c = c; }
public void execute(QueueableContext qc){
HttpRequest r = new HttpRequest();
r.setEndpoint('callout:SLACK_WEBHOOK');
r.setMethod('POST'); r.setHeader('Content-Type','application/json');
r.setBody('{"text":"Escalated Case ' + c.CaseNumber + '"}');
new Http().send(r);
}
}
Best practices
Answer: Implement Database.AllowsCallouts
on the batch class.
Code
global class SyncBatch implements Database.Batchable<SObject>, Database.AllowsCallouts {
global Database.QueryLocator start(Database.BatchableContext bc){ /* ... */ }
global void execute(Database.BatchableContext bc, List<Account> scope){
// safe to call out per chunk
}
global void finish(Database.BatchableContext bc){ }
}
Best practices
Answer: Implement idempotency with a locking row or Processed_Job__c keyed by external id.
Pattern
if (!JobLock.tryAcquire('INVOICE:123')) return; // already running
System.enqueueJob(new ProcessInvoiceQueueable('123'));
Best practices
finish
or after success/failure.Answer: Store Event Id (or payload hash) in a Processed_Event__c table; skip if exists.
Code
Boolean seen = [SELECT COUNT() FROM Processed_Event__c WHERE ExternalEventId__c = :evtId] > 0;
if (!seen) {
// process
insert new Processed_Event__c(ExternalEventId__c = evtId);
}
Best practices
ExternalEventId__c
unique to enforce at DB level.Answer: Configure Named Credential with a Client Certificate or JWT auth provider.
Tips
UNABLE_TO_LOCK_ROW
gracefully in async processing.Answer: Catch and retry later.
Code
try {
update records;
} catch (DmlException e) {
if (e.getMessage().contains('UNABLE_TO_LOCK_ROW')) {
// schedule/queue for retry
} else { throw e; }
}
Best practices
Answer: Use lightning/empApi
.
LWC JS
import { LightningElement } from 'lwc';
import { subscribe, onError, setDebugFlag } from 'lightning/empApi';
export default class PeSubscriber extends LightningElement {
channelName = '/event/Opportunity_Won__e';
subscription;
connectedCallback() {
setDebugFlag(true);
subscribe(this.channelName, -1, (msg) => this.handle(msg))
.then(res => this.subscription = res);
onError(err => { /* log */ });
}
handle(message) {
// update UI with message.data.payload
}
}
Best practices
Answer: Subscribe to /data/AccountChangeEvent
(LWC or external consumer).
LWC (empApi channel)
const channel = '/data/AccountChangeEvent';
subscribe(channel, -1, (msg) => {
const change = msg.data.payload.ChangeEventHeader.changeType; // CREATE/UPDATE/DELETE
// react to change
});
Best practices
Answer: Use Pub/Sub API (gRPC/HTTP) with replay and durable subscriptions.
Tips
Answer: Use ContentVersion + callout to external API; link with ContentDocumentLink.
Code (create file)
ContentVersion v = new ContentVersion(
Title='Invoice', PathOnClient='invoice.pdf', VersionData=blobBody
);
insert v;
ContentDocumentLink l = new ContentDocumentLink(
ContentDocumentId = [SELECT ContentDocumentId FROM ContentVersion WHERE Id = :v.Id].ContentDocumentId,
LinkedEntityId = recordId, ShareType='V'
);
insert l;
// Callout to external to mirror (send presigned URL or binary)
Best practices
Answer: Build an Ops Dashboard:
AsyncApexJob
, ApexTestQueueItem
, PlatformEventUsageMetric
.Code (sample)
List<AsyncApexJob> jobs = [SELECT Id, JobType, Status, NumberOfErrors, TotalJobItems, CreatedDate
FROM AsyncApexJob ORDER BY CreatedDate DESC LIMIT 50];
Best practices
Answer: Use Change Data Capture (CDC) for near real-time streaming of inserts/updates/deletes; consumers process deltas and upsert to targets.
Pattern
Best practices
Source__c = 'ERP'
).Here are some resources to go deeper:
🔗 100 Scenarios (1–4 YOE)
🔗 100 Scenarios (4–8 YOE)
đź”—Â LWC Q&A (Real Answers Explained)
đź”—Â 600+ Qs from Recruiter Calls
đź”—Â TCS, Infosys, EY Interview Qs
🔗 Salesforce Project – Hindi + English