Home Interview Q&A

70 PwC Salesforce Interview Questions & Answers (2026) | SF Interview Pro

📅
70 PwC Salesforce Interview Questions & Answers (2026) | SF Interview Pro
🏢 Company-Wise · PwC

70 PwC Salesforce Interview Questions & Answers (2025–2026)

Real PwC Salesforce Developer interview questions with years of experience tagged — plus scenario-based rounds and LWC deep-dive questions to round out your prep

70Questions
9Rounds Covered
100%Free
⚡ Complete Index — All 70 Questions
1Trigger to count Contacts on Account using aggregate query... 2Approaches to count Contacts besides aggregate query... 34 batch jobs — total records processed and total failed... 4Recent Subscriber checkbox scenario on delete... 5Trigger to prevent Account deletion if Opportunity exists... 6Trigger: most sold product picklist on Account... 7Optimize a Map-based Contact count trigger... 8Trigger: owner change cascades to related Contacts... 9Trigger: concatenate Opportunity field onto Account... 10Predict the output of 6 pasted code snippets... 11Can refreshApex() be called inside renderedCallback?... 12Why do we use a constructor in LWC?... 13What is Lightning Message Service and when to use it?... 14When should you use @wire with Apex?... 15Wire method vs Imperative Apex call... 16Explain LWC lifecycle hooks in order... 17querySelectorAll syntax in LWC... 18Select all / deselect all datatable component... 19User can't see a record they should have access to... 20Manual Share button not visible on record... 21What is manual sharing in Salesforce?... 22Apex default sharing is "without sharing" — why declare it?... 23How does org-wide security layering work?... 24Community user should only see records they created... 25Security considerations on the Experience Cloud side... 26Stop trigger/flow execution during a data load... 27Return type of the start method in Batch Apex... 28Skip a specific record inside a running Batch job... 29Bypass a Validation Rule for one specific user... 30Why does Apex CPU Time Limit exceeded happen?... 31Design a daily batch to expire records past a date... 32Why use Map over List in trigger optimization?... 33Difference between Future method and Queueable Apex... 34Why can't a future method accept a list of sObjects?... 35OAuth 1.0 vs OAuth 2.0 in Salesforce... 36Remote Site Settings vs Named Credentials... 37What is a Refresh Token used for?... 38What are Platform Events and where do you use them?... 39Ensure an integration isn't leaking data... 40What is Person Account and when is it needed?... 41Query Person Accounts created in the last 4 days... 42What is Action Plan / ARC in Financial Services Cloud?... 43Display full Email-to-Case thread on the Case record... 44Flow vs Trigger — when do you pick which?... 45When and why would you use renderedCallback()?... 46How does reactive data binding work in LWC?... 47Conditional rendering based on dynamic data... 48Communicate between sibling LWC components... 49Make an LWC datatable editable and persist changes... 50Handle pagination in an LWC component... 51Optimize component performance with large datasets... 52Strategies to enhance overall LWC performance... 53Handling server responses and errors from Apex... 54Debug performance issues in LWC components... 55Tools and techniques for testing LWC components... 56Security considerations building custom LWC features... 57Ensure compliance with CRUD, FLS, and sharing... 58Scenario: multi-brand org, one Salesforce instance... 59Scenario: territory realignment mid-quarter... 60Scenario: idempotent retry for a flaky integration... 61Scenario: holiday-aware SLA countdown on Cases... 62Scenario: merging duplicate Leads mid-campaign... 63Scenario: mass account reparenting without breaking rollups... 64Scenario: dynamic approval routing by deal size and region... 65Scenario: multi-currency rollups going stale... 66Scenario: partner portal users seeing each other's deals... 67Scenario: nightly batch colliding with business hours... 68Deep-cloning an Opportunity with its Contact Roles... 69Can a future method call another future method?... 70Custom Settings vs Custom Metadata for per-environment config...
⚙️

Triggers & Coding Round

Q1–Q10 · Write-the-code questions asked at PwC across 2025–2026 rounds

Q0012.6–4.8 YOE

Write a trigger to count the number of Contacts associated with an Account, using an aggregate query specifically.

Group Contacts by AccountId with an aggregate SOQL query inside the trigger, then update each Account's count field in bulk in a single DML statement.
🔑 Key Points
PwC interviewers specifically ask for the aggregate query version first, then immediately follow up asking for a non-aggregate alternative — so prepare both. Always bulkify by collecting AccountIds from Trigger.new/old into a Set first.
🌍 Example
A Total_Contacts__c rollup field on Account that stays accurate whenever Contacts are inserted, updated (re-parented), or deleted — used for account health dashboards.
trigger ContactCountTrigger on Contact (after insert, after update, after delete, after undelete) { Set<Id> accIds = new Set<Id>(); for (Contact c : (Trigger.isDelete ? Trigger.old : Trigger.new)) { if (c.AccountId != null) accIds.add(c.AccountId); } List<AggregateResult> results = [SELECT AccountId, COUNT(Id) cnt FROM Contact WHERE AccountId IN :accIds GROUP BY AccountId]; Map<Id, Integer> countMap = new Map<Id, Integer>(); for (AggregateResult ar : results) { countMap.put((Id) ar.get('AccountId'), (Integer) ar.get('cnt')); } List<Account> accsToUpdate = new List<Account>(); for (Id accId : accIds) { accsToUpdate.add(new Account(Id = accId, Total_Contacts__c = countMap.containsKey(accId) ? countMap.get(accId) : 0)); } if (!accsToUpdate.isEmpty()) update accsToUpdate; }
🎤 “Aggregate query groups Contacts by AccountId with COUNT(Id), and I bulk-update the parent Accounts in one DML.”
Q0022.6–4.8 YOE

Other than an aggregate query, what are the different approaches to find the number of Contacts associated with an Account?

Three alternatives: a roll-up summary field (only works if it's a master-detail relationship), a plain SOQL sub-query with .size() on the child relationship, or a manually maintained counter field updated by the trigger itself.
🔑 Key Points
Account-Contact is a lookup, not master-detail, so a native roll-up summary field is NOT an option here — this is the twist PwC is testing for. acc.Contacts.size() works but only after querying the parent with a subquery, and doesn't scale well for very large child counts.
🌍 Example
SELECT Id, (SELECT Id FROM Contacts) FROM Account WHERE Id = :accId — then check the size of the nested list in Apex.
Account acc = [SELECT Id, (SELECT Id FROM Contacts) FROM Account WHERE Id = :accId]; Integer contactCount = acc.Contacts.size();
🎤 “Since Account-Contact is a lookup not master-detail, I can't use a roll-up summary — so I'd use a sub-query with .size(), or maintain the count manually via trigger.”
Q0032.6–4.8 YOE

You have 4 Batch Apex jobs running. Write code that gives you the total number of records processed and total failed records across all 4 batches combined.

Query the AsyncApexJob object for all 4 job Ids and sum JobItemsProcessed / NumberOfErrors across them.
🔑 Key Points
AsyncApexJob tracks batch execution metadata automatically — no custom logging object is required for basic counts. For record-level failure details (not just counts), you'd additionally need custom error logging inside the execute() method since AsyncApexJob only gives aggregate numbers.
🌍 Example
A nightly reconciliation job chains 4 batches (Accounts, Contacts, Opportunities, Cases) and a final summary email reports combined success/failure counts to the admin team.
List<Id> jobIds = new List<Id>{ jobId1, jobId2, jobId3, jobId4 }; Integer totalProcessed = 0; Integer totalFailed = 0; for (AsyncApexJob job : [SELECT JobItemsProcessed, NumberOfErrors FROM AsyncApexJob WHERE Id IN :jobIds]) { totalProcessed += job.JobItemsProcessed; totalFailed += job.NumberOfErrors; }
🎤 “I'd query AsyncApexJob for all 4 job Ids and sum JobItemsProcessed and NumberOfErrors.”
Q0042.6–4.8 YOE

Account and Subscriber are related objects. Subscriber has a checkbox "Recent Subscriber" — the most recently added Subscriber per Account should be true, all others false. When that Subscriber is deleted, the next most recent one should get flagged true instead.

On after insert, set the new Subscriber as Recent and unset the previous one via trigger. On after delete, re-query the remaining Subscribers per affected Account and flag the newest one true.
🔑 Key Points
This is really two triggers in one design: an insert-time "unset the old, set the new" step, and a delete-time "recompute the newest survivor" step. Both must be bulkified across multiple Accounts in one transaction — a very common PwC gotcha is candidates writing this for a single record only.
🌍 Example
A newsletter platform where each Account's "currently active" subscriber badge needs to always point at the latest signup, even after cancellations.
// After insert — unset old "recent" flags for the same Account, keep only the newest trigger SubscriberTrigger on Subscriber__c (after insert, after delete) { if (Trigger.isInsert) { Set<Id> accIds = new Set<Id>(); for (Subscriber__c s : Trigger.new) accIds.add(s.Account__c); List<Subscriber__c> toUnset = [SELECT Id FROM Subscriber__c WHERE Account__c IN :accIds AND Recent_Subscriber__c = true AND Id NOT IN :Trigger.newMap.keySet()]; for (Subscriber__c s : toUnset) s.Recent_Subscriber__c = false; update toUnset; } if (Trigger.isDelete) { Set<Id> accIds = new Set<Id>(); for (Subscriber__c s : Trigger.old) accIds.add(s.Account__c); // Re-flag latest remaining Subscriber per Account (grouped in Apex after query, ordered by CreatedDate DESC) } }
🎤 “Insert unsets the old flag and sets the new one; delete re-queries the remaining Subscribers per Account and flags the most recent survivor.”
Q0052.6–4.8 YOE

Write a trigger to prevent a user from deleting an Account if any Opportunity record is associated with it.

On before delete, check Trigger.old for related Opportunities and call addError() on any Account that still has one, blocking the delete transaction.
🔑 Key Points
Use before delete, not after — you need to block the delete before it happens. Query Opportunities in bulk using the Account Ids from Trigger.old, not one-by-one inside the loop.
🌍 Example
Sales ops wants to prevent accidental deletion of Accounts that still have open pipeline value tied to them.
trigger PreventAccountDelete on Account (before delete) { Set<Id> accIds = Trigger.oldMap.keySet(); Set<Id> accIdsWithOpps = new Set<Id>(); for (Opportunity o : [SELECT AccountId FROM Opportunity WHERE AccountId IN :accIds]) { accIdsWithOpps.add(o.AccountId); } for (Account acc : Trigger.old) { if (accIdsWithOpps.contains(acc.Id)) { acc.addError('Cannot delete an Account that has related Opportunities.'); } } }
🎤 “Before delete, I query related Opportunities in bulk and call addError() on any Account that still has one.”
Q0062.6–4.8 YOE

Write a trigger on Order Product so the Account's "Most Sold Item" picklist gets updated with whichever product has the highest total quantity sold for that Account.

On after insert/update/delete of OrderItem, aggregate SUM(Quantity) grouped by Product per Account, find the max, and update the Account picklist field to match.
🔑 Key Points
Since the target is a picklist field, the value being written back must exactly match an existing picklist API value — dynamically writing to a picklist means the incoming product name has to already exist as a valid option, or the DML throws an error.
🌍 Example
A distributor's Account record shows "Keyboard" as Most_Sold_Item__c because more keyboard units have shipped to that customer than any other SKU.
// Aggregate query grouped by Account and Product, ordered by total quantity descending, // then keep only the top row per Account before updating the picklist field. List<AggregateResult> results = [SELECT Order.AccountId acc, Product2.Name prodName, SUM(Quantity) totalQty FROM OrderItem WHERE Order.AccountId IN :accIds GROUP BY Order.AccountId, Product2.Name ORDER BY Order.AccountId, SUM(Quantity) DESC];
🎤 “I aggregate quantity sold grouped by Product per Account, then take the top row per Account and write that product name into the picklist.”
Q0074.8 YOE

Trigger — whenever a Contact is inserted, updated, or deleted (related to an Account), update a field Total_Contacts_Count__c with the latest count. Candidate used a List with multiple if-statements to find AccountIds; interviewer pushed back and asked to use a Map to optimize.

Replace the multiple List/if-chain approach with a single Map<Id, Integer> built in one pass, so AccountId-to-count lookups are O(1) instead of repeatedly scanning a List.
🔑 Key Points
Interviewers here specifically probe whether you default to List+nested-if patterns or reach for a Map right away. Using a Map avoids repeated linear scans and keeps the trigger fast even with thousands of Contacts in one transaction.
🌍 Example
Bulk data load of 5,000 Contacts across 800 Accounts — a Map-based approach processes this in one pass instead of re-scanning a List thousands of times.
Set<Id> accIds = new Set<Id>(); for (Contact c : Trigger.isDelete ? Trigger.old : Trigger.new) accIds.add(c.AccountId); Map<Id, Integer> accIdToCount = new Map<Id, Integer>(); for (AggregateResult ar : [SELECT AccountId, COUNT(Id) cnt FROM Contact WHERE AccountId IN :accIds GROUP BY AccountId]) { accIdToCount.put((Id) ar.get('AccountId'), (Integer) ar.get('cnt')); } List<Account> toUpdate = new List<Account>(); for (Id accId : accIds) { toUpdate.add(new Account(Id = accId, Total_Contacts_Count__c = accIdToCount.containsKey(accId) ? accIdToCount.get(accId) : 0)); } update toUpdate;
🎤 “I collect AccountIds into a Set, build one Map of AccountId to count via an aggregate query, then update Accounts in a single bulk DML — no nested loops or repeated List scans.”
Q0084.8 YOE

Write a trigger — if the Owner of an Account is changed, the Owner of the related Contacts should also be updated to match.

On Account after update, detect where OwnerId changed by comparing Trigger.new to Trigger.oldMap, then bulk-update related Contacts' OwnerId to match.
🔑 Key Points
Always compare against oldMap to detect an actual change — don't blindly update Contacts on every Account update, only when OwnerId specifically changed, or you waste DML and risk hitting limits unnecessarily.
🌍 Example
A rep leaves the company and their Accounts are mass-reassigned to a new rep — the related Contacts should follow the same ownership change automatically.
trigger AccountOwnerSync on Account (after update) { Set<Id> changedAccIds = new Set<Id>(); for (Account acc : Trigger.new) { if (acc.OwnerId != Trigger.oldMap.get(acc.Id).OwnerId) { changedAccIds.add(acc.Id); } } if (changedAccIds.isEmpty()) return; Map<Id, Id> accIdToNewOwner = new Map<Id, Id>(); for (Account acc : Trigger.new) { if (changedAccIds.contains(acc.Id)) accIdToNewOwner.put(acc.Id, acc.OwnerId); } List<Contact> contactsToUpdate = new List<Contact>(); for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :changedAccIds]) { c.OwnerId = accIdToNewOwner.get(c.AccountId); contactsToUpdate.add(c); } update contactsToUpdate; }
🎤 “I compare Trigger.new against oldMap to find Accounts whose Owner actually changed, then bulk-update only the related Contacts for those.”
Q0094.8 YOE

On Account and Opportunity there's a Test__c field. Whenever an Opportunity related to the Account is inserted or deleted, concatenate Opportunity.Test__c onto Account.Test__c.

On Opportunity after insert/delete, gather affected AccountIds, build a concatenated string per Account from all related Opportunity.Test__c values, and update the Account field.
🔑 Key Points
Because this needs to handle delete as well as insert, you can't just append the new value — you have to rebuild the full concatenated string from the current set of related Opportunities each time, otherwise deleted values would linger in the Account field.
🌍 Example
A rollup tag field on Account that always reflects the current combined set of tags from all its open Opportunities.
trigger OppTestSync on Opportunity (after insert, after delete) { Set<Id> accIds = new Set<Id>(); for (Opportunity o : Trigger.isDelete ? Trigger.old : Trigger.new) accIds.add(o.AccountId); Map<Id, List<String>> accToValues = new Map<Id, List<String>>(); for (Opportunity o : [SELECT AccountId, Test__c FROM Opportunity WHERE AccountId IN :accIds]) { if (!accToValues.containsKey(o.AccountId)) accToValues.put(o.AccountId, new List<String>()); if (o.Test__c != null) accToValues.get(o.AccountId).add(o.Test__c); } List<Account> toUpdate = new List<Account>(); for (Id accId : accIds) { String combined = accToValues.containsKey(accId) ? String.join(accToValues.get(accId), ', ') : ''; toUpdate.add(new Account(Id = accId, Test__c = combined)); } update toUpdate; }
🎤 “I rebuild the concatenated string from the current related Opportunities each time, so deletes correctly drop out of the combined value too.”
Q0104.8 YOE

PwC pastes 6 short code snippets and asks: what is the output/behavior of each? (list index access, SOQL WHERE with no match, before-vs-after trigger record mutation, nested query inside a loop, after-delete field access, Map.get() returning null.)

This is a rapid-fire "spot the bug" exercise — walk through each snippet and identify the runtime exception or unexpected behavior it produces.
🔑 Key Points
accs[0] on an empty List → ListException: index out of bounds.
SOQL with no matching row and no LIMIT wrapper assigned directly to a single sObject variable → List has no rows for assignment exception.
Mutating Trigger.new fields in an after update trigger then calling update again → throws because records are read-only in after-context; that update must happen in before-context instead.
SOQL inside a for-loop over Trigger.new → classic SOQL-in-a-loop governor limit risk (101 query error at scale).
Accessing a field on a record inside an after delete trigger works fine for read (Trigger.old is available), but attempting DML on the deleted record itself fails.
Map.get() on a missing key returns null, not an exception — but the very next line dereferencing that null (e.g. .Industry) throws a NullPointerException.
🌍 Example
This "predict the output" style round is run rapid-fire — six snippets pasted one after another with no time to actually execute them, testing whether you can trace execution mentally.
// Example snippet type asked: trigger OppMapTrig on Opportunity (before insert) { Map<Id, Account> accMap = new Map<Id, Account>(); for (Opportunity o : Trigger.new) { o.Description = accMap.get(o.AccountId).Industry; // NullPointerException — map is never populated } }
🎤 “Each of these fails for a specific, predictable reason — empty-list index access, no-row SOQL assignment, after-context immutability, SOQL-in-loop limits, or a null dereference off an empty Map.”
🧩

Lightning Web Components (LWC) Round

Q11–Q18 · Life cycle, wire vs imperative, and hands-on component building

Q0114.8 YOE

Can we call refreshApex() inside the renderedCallback() lifecycle hook in LWC? Why or why not?

Technically callable, but it's a bad practice — renderedCallback() fires on every re-render, so calling refreshApex() there can trigger repeated re-fetching and even an infinite render loop.
🔑 Key Points
renderedCallback() runs every single time the component re-renders — including as a side effect of the data refresh itself, which is exactly the trap. The correct place to trigger a refresh is in response to a specific user action or event handler, not inside a lifecycle hook that fires unpredictably often.
🌍 Example
A "Refresh" button's click handler calling refreshApex(this.wiredResult) is the safe pattern — calling it inside renderedCallback() risks the component re-rendering itself into a loop.
handleRefreshClick() { refreshApex(this.wiredAccounts); }
🎤 “You can call it, but you shouldn't — renderedCallback fires on every render, so it risks a re-render loop. Trigger refreshApex from a specific event handler instead.”
Q0124.8 YOE

Why do we use a constructor in Lightning Web Components?

The constructor runs first in the component's lifecycle, before the element is attached to the DOM — used to initialize private JavaScript state, not to touch the DOM or query child elements.
🔑 Key Points
You cannot access @api-decorated public properties reliably inside the constructor because the parent hasn't necessarily set them yet at that point — that logic belongs in connectedCallback() instead. Always call super() first if you override the constructor.
🌍 Example
Setting a default internal counter or initializing a private array before any rendering happens.
export default class MyComponent extends LightningElement { constructor() { super(); this.internalCounter = 0; } }
🎤 “The constructor runs before DOM attachment, so it's for initializing private JS state only — not for reading @api properties or touching the DOM.”
Q0132.6–4.8 YOE

What is Lightning Message Service (LMS) and when do we use it? How is it different from the old pub-sub model?

LMS is a declared Message Channel that lets LWC, Aura, and Visualforce components communicate across the entire page — the old pub-sub library only worked between Aura components on the same page.
🔑 Key Points
Use LMS specifically when two components have no direct parent-child relationship and might even live in different frameworks (LWC talking to a Visualforce page, for example). It's the Salesforce-recommended replacement for pub-sub going forward.
🌍 Example
A record highlights panel built in Aura needs to refresh whenever an unrelated LWC component elsewhere on the same Lightning page updates a related record.
import { publish, subscribe, MessageContext } from 'lightning/messageService'; import MY_CHANNEL from '@salesforce/messageChannel/MyChannel__c';
🎤 “LMS uses a declared Message Channel and works across LWC, Aura, and Visualforce — pub-sub only worked between Aura components on the same page.”
Q0144.8 YOE

When should you use @wire with Apex versus calling it imperatively?

Use @wire for read-only data that should reactively refresh when its reactive parameters change; use an imperative call when the action is user-triggered, needs manual error handling control, or performs a write operation.
🔑 Key Points
A @wire property re-fires automatically whenever a reactive ($-prefixed) parameter changes — you don't control exactly when it runs. An imperative call runs exactly when you call it, giving you full control over timing, which matters for button clicks or sequenced operations.
🌍 Example
Wiring an Account's related Contact list so it auto-refreshes as the user navigates — versus an imperative "Save" button that calls Apex only on click.
@wire(getContactsForAccount, { accountId: '$recordId' }) contacts;
🎤 “Wire for reactive read-only data, imperative when I need precise control over when the call fires — especially for writes and button clicks.”
Q0154.8 YOE

Explain the difference between a Wire method and an Imperative Apex call in more depth — including caching behavior.

Wire methods marked cacheable=true benefit from client-side caching across components automatically; imperative calls always hit the server fresh unless you build your own caching layer.
🔑 Key Points
Wire adapters cannot perform DML — cacheable=true methods are strictly read-only. Imperative Apex methods can perform DML and are the only option for save/update/delete actions from LWC.
🌍 Example
Two unrelated components on the same page both wiring the same cacheable Apex method reuse the cached result instead of double-querying the server.
@AuraEnabled(cacheable=true) public static List<Account> getAccounts() { return [SELECT Id, Name FROM Account]; }
🎤 “Wire with cacheable=true is read-only but benefits from shared client-side caching; imperative calls always go to the server and are the only way to perform DML.”
Q0162.6–3 YOE

Explain the LWC lifecycle in order, and mention where the DOM actually gets updated.

constructor() → connectedCallback() → render() → renderedCallback(), and the actual DOM update happens internally during the render() phase, right before renderedCallback() fires.
🔑 Key Points
render() is a protected method, not a public lifecycle hook you typically override — most developers only ever touch constructor, connectedCallback, and renderedCallback directly. disconnectedCallback() fires when the component is removed from the DOM, useful for cleanup like unsubscribing from LMS channels.
🌍 Example
A component subscribes to an LMS channel in connectedCallback() and unsubscribes in disconnectedCallback() to avoid memory leaks.
connectedCallback() { /* subscribe to LMS */ } disconnectedCallback() { /* unsubscribe from LMS */ }
🎤 “constructor, then connectedCallback, then the internal render phase updates the actual DOM, then renderedCallback fires.”
Q0172.6–4.8 YOE

Write the syntax for querySelectorAll in LWC.

Use this.template.querySelectorAll() with a CSS-style selector to grab multiple matching elements from the component's own shadow DOM.
🔑 Key Points
Always call it on this.template, not the global document — LWC's shadow DOM boundary means document-level selectors won't reach into the component. Use data-* attributes rather than id, since ids can get transformed at render time.
🌍 Example
Selecting all lightning-input elements on a dynamic form to run a validation pass before saving.
const inputs = this.template.querySelectorAll('lightning-input'); inputs.forEach(input => { input.reportValidity(); });
🎤 “this.template.querySelectorAll(selector) — always scoped to this.template because of the shadow DOM boundary.”
Q0182.6–4.8 YOE

Write an LWC component with two buttons — "Select All" and "Deselect" — that toggle checkbox selection across an HTML table or datatable of records.

Track a reactive property holding "selected" state per row; the button handlers loop over the current data array and flip that property for every row, which re-renders all checkboxes automatically.
🔑 Key Points
Never mutate array items directly in place — create a fresh mapped array so LWC's change detection actually picks up the update. If using lightning-datatable, selectedRows can be driven directly by setting the array of record Ids instead of manual checkbox tracking.
🌍 Example
A bulk-action screen where a user selects multiple Contacts to add to a Campaign in one click.
handleSelectAll() { this.records = this.records.map(rec => ({ ...rec, isSelected: true })); } handleDeselectAll() { this.records = this.records.map(rec => ({ ...rec, isSelected: false })); }
🎤 “I map a fresh array with isSelected flipped for every row, so LWC's reactivity picks up the change and re-renders all checkboxes.”
🔐

Security Model & Sharing

Q19–Q26 · Record visibility, manual sharing, and Experience Cloud access

Q0192.6–4.8 YOE

A user says they can't see a record they should have access to. What's your debugging approach?

If the question is vague, ask clarifying cross-questions first — then systematically check OWD, role hierarchy, sharing rules, manual/Apex sharing, and profile/permission set object access in that order.
🔑 Key Points
PwC explicitly notes this as a question where you're expected to ask cross-questions if it's underspecified, rather than guess — that itself is part of what's being evaluated. Check the "Why" panel on the record's Sharing detail page as a fast diagnostic starting point.
🌍 Example
A rep escalates that a newly transferred Account is invisible — turns out OWD is Private and no sharing rule or role hierarchy path grants them access.
-- Diagnostic order: -- 1. Object/Field permissions (Profile + Permission Sets) -- 2. OWD for the object -- 3. Role hierarchy (if grant-access-using-hierarchy is on) -- 4. Sharing rules (criteria or ownership-based) -- 5. Manual sharing / Apex-managed sharing
🎤 “I'd clarify the scenario first if it's vague, then check permissions, OWD, role hierarchy, sharing rules, and manual/Apex sharing in that order.”
Q0202.6–4.8 YOE

The manual Share button isn't visible on a record's page layout. What could be the reason?

Either the OWD for that object is already Public Read/Write (nothing left to manually share), the object doesn't support manual sharing, or the user's profile lacks the permission needed to see the Sharing action.
🔑 Key Points
Manual sharing only makes sense when OWD is restrictive (Private or Public Read Only) — if it's already wide open, Salesforce hides the button because there's nothing to grant. Some standard objects also simply don't support manual sharing at all.
🌍 Example
A custom object with OWD = Public Read/Write shows no Share button because every user already has full access by default.
-- Checklist when Share button is missing: -- OWD already Public R/W? -> button hidden by design -- Object supports sharing at all? -- Profile permission for sharing action present?
🎤 “Most likely the OWD is already Public Read/Write so there's nothing to manually share, or the profile lacks the sharing permission.”
Q0212.6–4.8 YOE

What is manual sharing in Salesforce?

A one-off, record-level grant of access made directly from a record's Sharing page — used for exceptions that sharing rules and role hierarchy don't cover.
🔑 Key Points
Manual sharing only applies when OWD is more restrictive than Public Read/Write. It's record-specific — it doesn't scale to "every record matching a criteria," which is what sharing rules are for instead.
🌍 Example
A manager temporarily grants a colleague Edit access to one specific deal Opportunity while they're covering for someone on leave.
-- Record detail page -> Sharing -> Add -> select User/Role/Group -> Access Level
🎤 “Manual sharing is a one-off, record-level access grant made from the Sharing detail page, for exceptions that broader rules don't cover.”
Q0222.6–4.8 YOE

Apex classes run "without sharing" by default anyway — so why bother explicitly writing the "without sharing" keyword?

Explicit declaration is a self-documenting best practice — it removes ambiguity about intent for future developers and protects the code's behavior if a calling context or platform default ever changes.
🔑 Key Points
"Inherited sharing" classes behave differently depending on the caller — an omitted declaration only defaults to without sharing if the class is the transaction's entry point. Being explicit avoids subtle bugs where a class's actual sharing behavior depends on how and where it's invoked from.
🌍 Example
A security-sensitive utility class explicitly marked without sharing, so a code reviewer instantly understands it deliberately bypasses record access checks — instead of wondering if that was intentional.
public without sharing class DataMigrationHelper { // Explicitly documented: intentionally bypasses sharing for admin migration tasks }
🎤 “Being explicit removes ambiguity — an omitted declaration's actual behavior can depend on the calling context, so I always declare it directly.”
Q0232.6–4.8 YOE

Explain the Salesforce security model at a high level, and how organization-level security fits in.

Security in Salesforce layers from broad to narrow: Org-level (login hours, IP ranges, password policies) → Object-level (profiles/permission sets) → Field-level (FLS) → Record-level (OWD, role hierarchy, sharing rules, manual/Apex sharing).
🔑 Key Points
Org-level security is the first gate — it controls who can even log in and from where, before object/field/record permissions ever come into play. This layered model is a very common "explain in one breath" question at PwC's Round 1.
🌍 Example
Login IP Ranges restrict all access to a corporate VPN range at the org level, on top of whatever record-level sharing is configured underneath.
-- Layers, broad to narrow: -- Org-level -> Object-level -> Field-level -> Record-level
🎤 “Security layers from org-level login controls down through object, field, and finally record-level access.”
Q0244.8 YOE (Experience Cloud)

A Community/Experience Cloud user should only be able to see records they personally created — by default, if they create a Case, the Owner might be someone else. How do you achieve this?

Use a Sharing Set based on a matching field (e.g., a Contact lookup on the Case matching the logged-in user's associated Contact), combined with OWD Private for that object in the external org-wide defaults.
🔑 Key Points
Sharing Sets are the standard mechanism for granting external/community users access based on a relationship to their own Account/Contact record, rather than strict record ownership. This is distinct from internal sharing rules, which don't apply the same way to high-volume community users.
🌍 Example
A self-service portal where customers can log support Cases that get auto-assigned to a support queue, but the customer should still see only their own submitted Cases.
-- Setup -> Sharing Sets -> New -- Access Mapping: Case.Contact = User's Contact (Related to logged-in user)
🎤 “A Sharing Set mapped on the Contact relationship, not the Owner field, so community users see records tied to their own Contact regardless of who owns them internally.”
Q0254.8 YOE (Experience Cloud)

What security considerations come up specifically on the Experience Cloud (community) side of an implementation?

Guest user profile permissions need extra scrutiny (they're publicly exposed), Sharing Sets need correct field mappings, and OWD for externally-visible objects usually needs to be more restrictive than the internal org's defaults.
🔑 Key Points
The Guest User profile is a common misconfiguration source — over-permissioning it exposes data to unauthenticated visitors. Since Winter '21, Salesforce enforces stricter default restrictions on Guest User record access specifically to prevent this class of mistake.
🌍 Example
An unauthenticated visitor to a public knowledge-base site should never be able to query unrelated Account or Contact data through an exposed LWC on that page.
-- Review: Guest User Profile -> Object permissions -- Review: Sharing Set field mappings -- Review: OWD for externally-exposed objects
🎤 “Guest user permissions are the highest-risk area, followed by Sharing Set mappings and making sure OWD is tight enough for anything exposed externally.”
Q0264.8 YOE

How do you stop triggers or Flows from firing during a large data load, such as a migration?

Use a Custom Setting or Custom Metadata "kill switch" flag checked at the top of triggers/Flows, or run the load through a dedicated integration user whose profile/permission set is excluded from the automation's entry condition.
🔑 Key Points
A Hierarchy Custom Setting is a common pattern — toggle it off org-wide (or for a specific data-load user) right before the migration and toggle it back on after. For Flows, an entry condition checking $Permission or a custom field on the running user achieves the same bypass.
🌍 Example
A one-time historical Account migration of 200,000 records that shouldn't re-trigger email alerts, rollup recalculations, or downstream integrations meant for real-time changes only.
// Trigger bypass check if (!BypassSettings__c.getInstance().Bypass_Triggers__c) { // run trigger logic }
🎤 “A Custom Setting toggle checked at the top of triggers and in Flow entry conditions — flipped on for the load, then off again afterward.”
⏱️

Async Apex & Batch Processing

Q27–Q34 · Batch internals, CPU limits, and the FSC-round batch scenario

Q027PwC SDC, Bangalore

What is the return type of the start() method in Batch Apex?

public Iterable<sObject> start(Database.BatchableContext bc) — or Database.QueryLocator when you're driving the batch off a straightforward SOQL query.
🔑 Key Points
Database.QueryLocator can handle up to 50 million records, far beyond the normal 50,000-row SOQL limit — this is exactly why it exists as a special-case return type for start(). Use plain Iterable when your source data isn't a straightforward query (e.g., a custom iterator over a callout response).
🌍 Example
A batch cleaning up 2 million stale Lead records uses Database.QueryLocator, since a plain List would blow past the standard 50,000-row governor limit.
global class CleanupBatch implements Database.Batchable<sObject> { global Database.QueryLocator start(Database.BatchableContext bc) { return Database.getQueryLocator([SELECT Id FROM Lead WHERE IsConverted = false AND CreatedDate < LAST_N_DAYS:365]); } global void execute(Database.BatchableContext bc, List<sObject> scope) { } global void finish(Database.BatchableContext bc) { } }
🎤 “start() returns Database.QueryLocator (or Iterable) — QueryLocator specifically because it can handle up to 50 million records, way past the normal SOQL row limit.”
Q0285 YOE, FSC round

You have a scheduled Batch job. How do you skip one specific batch execution, or skip a specific record within a batch, without any manual intervention?

Add a Custom Metadata or Custom Setting flag checked at the start of execute() that lets you flag a specific job run or record to be excluded programmatically, without needing to touch the schedule itself.
🔑 Key Points
To skip an entire run, check a "skip next execution" flag inside start() and simply return an empty QueryLocator/scope for that run. To skip individual records, filter them out of the SOQL in start() using a Custom Metadata-driven exclusion list, or check-and-continue inside the execute() loop.
🌍 Example
A scheduled batch that expires stale Custom Object records daily, but needs to be paused for one day during a planned maintenance window without disabling the entire schedule.
global Database.QueryLocator start(Database.BatchableContext bc) { if (Batch_Control__mdt.getInstance('ExpireRecordsBatch').Skip_Next_Run__c) { return Database.getQueryLocator([SELECT Id FROM MyObject__c WHERE Id = null]); // empty scope } return Database.getQueryLocator([SELECT Id, Expire_Date__c FROM MyObject__c WHERE Status__c = 'Active']); }
🎤 “A Custom Metadata flag checked inside start() — either returns an empty scope to skip the whole run, or filters out specific records from the query itself.”
Q0295 YOE, FSC round

How do you bypass a Validation Rule for one specific user, without disabling it for everyone?

Add a condition to the Validation Rule formula checking $Permission or $Profile so it only fires when the running user does NOT hold a specific bypass Custom Permission.
🔑 Key Points
Custom Permission is the cleanest approach because it's assignable via Permission Set to exactly the users who need the bypass, without hardcoding a Profile check that becomes brittle if profiles change later.
🌍 Example
A data migration integration user needs to insert historical records that would normally fail a "CloseDate cannot be in the past" validation rule.
AND( CloseDate < TODAY(), NOT($Permission.Bypass_Close_Date_Validation) )
🎤 “I pwc-wrap the rule's condition with NOT($Permission.BypassRuleName), and assign that Custom Permission only to the specific user via a Permission Set.”
Q0305 YOE, FSC round

Why does "Apex CPU time limit exceeded" happen, and what kinds of operations actually count against it?

It's thrown when a single transaction's actual CPU computation time exceeds 10 seconds (synchronous) or 60 seconds (asynchronous) — database wait time (SOQL/DML/callouts) does NOT count, but loops, string manipulation, and business logic execution do.
🔑 Key Points
This is a common misconception — candidates often assume slow SOQL causes the CPU limit error, but database round-trip time is excluded from the CPU clock entirely. The real cause is almost always inefficient in-memory logic: nested loops, excessive string concatenation in a loop, or unbounded recursive calls.
🌍 Example
A trigger with a nested for-loop comparing every record against every other record in a large batch (O(n²)) blows past the CPU limit even though every individual SOQL query returns fast.
// Anti-pattern that burns CPU time, not DB time: for (Account a : accountList) { for (Contact c : contactList) { // O(n^2) nested loop -- pure CPU cost if (c.AccountId == a.Id) { /* ... */ } } }
🎤 “Database wait time is excluded from the CPU clock — the real cost is in-memory logic like nested loops or heavy string operations inside triggers or batch execute methods.”
Q0315 YOE, FSC round

A custom object has a Status field (Active/Inactive) and an Expire Date field. Whenever the Expire Date is in the past, Status should flip to Inactive — checked daily via a scheduled batch. Walk through your approach.

A Schedulable class fires a Batchable class daily; start() queries all Active records with Expire_Date__c < TODAY(), and execute() bulk-updates Status to Inactive.
🔑 Key Points
Filtering the expired records directly inside the start() query (rather than querying everything and filtering in Apex) keeps the batch scope small and efficient. Schedule it via System.schedule() with a daily cron expression, ideally during off-peak hours.
🌍 Example
Promotional pricing records or temporary access grants that should automatically deactivate the day after their expiry date, without any manual admin cleanup.
global class ExpireRecordsBatch implements Database.Batchable<sObject>, Schedulable { global Database.QueryLocator start(Database.BatchableContext bc) { return Database.getQueryLocator([SELECT Id, Status__c FROM My_Object__c WHERE Status__c = 'Active' AND Expire_Date__c < TODAY()]); } global void execute(Database.BatchableContext bc, List<My_Object__c> scope) { for (My_Object__c rec : scope) rec.Status__c = 'Inactive'; update scope; } global void finish(Database.BatchableContext bc) {} global void execute(SchedulableContext sc) { Database.executeBatch(new ExpireRecordsBatch()); } }
🎤 “Filter for expired-but-still-Active records directly in start(), flip Status in execute(), and drive it daily via System.schedule().”
Q0324.8 YOE

Why should you favor a Map over a List in many trigger optimization scenarios?

A Map gives O(1) key-based lookups, while repeatedly scanning a List for a matching item is O(n) — at bulk data volumes that difference compounds fast and risks CPU time limits.
🔑 Key Points
Any time you need to correlate two sets of related records (e.g., parent Ids to child records), build a Map keyed by the join field in a single pass instead of nested looping — this is the single most common trigger-optimization pattern interviewers probe for.
🌍 Example
Correlating 5,000 Opportunities back to their parent Accounts — a Map lookup does this in one pass; a nested List scan does it in up to 5,000 × N comparisons.
Map<Id, Account> accMap = new Map<Id, Account>([SELECT Id, Industry FROM Account WHERE Id IN :accIds]); for (Opportunity o : opps) { o.Description = accMap.get(o.AccountId).Industry; // O(1) lookup }
🎤 “A Map gives constant-time lookups instead of repeatedly scanning a List — at bulk volumes that's the difference between passing and failing governor limits.”
Q0332.6–4.8 YOE

Difference between a Future method and Queueable Apex?

Queueable Apex accepts complex/non-primitive parameters (like sObjects), can be chained, and is monitorable via Job Id — a future method only accepts primitive parameters, can't chain, and isn't trackable the same way.
🔑 Key Points
Queueable is generally the modern preferred choice over future methods for exactly these reasons. Neither can be called from inside a batch's execute() method the same way — future methods specifically cannot be invoked from a batch context at all.
🌍 Example
Chaining three sequential callouts where each step depends on the previous response is only cleanly achievable with Queueable, not future methods.
public class MyQueueable implements Queueable { public void execute(QueueableContext context) { // can enqueue another Queueable here -- chaining System.enqueueJob(new NextStepQueueable()); } }
🎤 “Queueable accepts non-primitive parameters, supports chaining, and gives me a trackable Job Id — future methods can't do any of those three things.”
Q0342.6–4.8 YOE

Why is it not possible to pass a List of sObjects to a future method? What's the actual reason behind this restriction?

Future method parameters get serialized and queued for later execution — by the time it actually runs, the original sObject data could be stale or already changed, so Salesforce restricts parameters to primitive types (or collections of primitives) to avoid working with outdated record state.
🔑 Key Points
The workaround is to pass a List of record Ids (a primitive collection) instead, then re-query fresh data for those Ids at the start of the future method's execution — guaranteeing you're working with current values, not a stale snapshot.
🌍 Example
A future method that sends a welcome email to newly created Contacts takes a List<Id>, then re-queries the Contacts fresh inside the method before sending.
@future public static void sendWelcomeEmails(List<Id> contactIds) { List<Contact> freshContacts = [SELECT Id, Email FROM Contact WHERE Id IN :contactIds]; // send emails using current data, not a stale snapshot }
🎤 “sObjects would be a stale snapshot by the time the future method actually executes — passing Ids and re-querying fresh guarantees current data.”
🔗

Integration & Platform Events

Q35–Q39 · OAuth, Named Credentials, and secure data exchange

Q0352.6–4.8 YOE

Difference between OAuth 1.0 and OAuth 2.0 in Salesforce.

OAuth 1.0 required every request to be individually cryptographically signed; OAuth 2.0 uses simpler bearer tokens over HTTPS and offers multiple grant-type flows suited to different integration scenarios.
🔑 Key Points
Salesforce integrations today are essentially all built on OAuth 2.0 — OAuth 1.0 is largely legacy. Know at least the Web Server Flow (user-present, refresh token) and JWT Bearer Flow (server-to-server, no user interaction) as the two most commonly discussed grant types.
🌍 Example
A nightly server-to-server integration syncing ERP data uses the JWT Bearer Flow since there's no human available to log in interactively.
-- OAuth 2.0 grant types commonly discussed: -- Web Server Flow, User-Agent Flow, JWT Bearer Flow, Device Flow, Client Credentials Flow
🎤 “OAuth 2.0 replaced request-by-request signing with bearer tokens and multiple grant-type flows — it's what virtually all modern Salesforce integrations use.”
Q0362.6–4.8 YOE

Difference between Remote Site Settings and Named Credentials in Salesforce.

Remote Site Settings only whitelist an endpoint URL for callouts with no authentication handling; Named Credentials store the endpoint AND authentication details, and Salesforce automatically injects auth on every callout.
🔑 Key Points
A URL covered by a Named Credential doesn't need a separate Remote Site Setting entry — the Named Credential already implicitly whitelists it. Named Credentials keep auth details out of your Apex code entirely, which is both more secure and easier to maintain.
🌍 Example
An HTTP callout to a payment gateway uses a Named Credential so the OAuth token refresh logic never has to be hand-written in Apex.
HttpRequest req = new HttpRequest(); req.setEndpoint('callout:My_Named_Credential/api/orders'); req.setMethod('GET');
🎤 “Remote Site Settings just whitelist a URL — Named Credentials store the endpoint plus auth, and Salesforce handles the authentication automatically on every callout.”
Q0372.6–4.8 YOE

What is a Refresh Token used for in an integration?

A long-lived credential used to silently obtain a new short-lived access token once the current one expires, without requiring the user to log in again.
🔑 Key Points
Access tokens are intentionally short-lived for security; refresh tokens let a server-side integration stay authenticated indefinitely (until explicitly revoked) without repeated manual logins. This is essential for scheduled/unattended integrations that run with no human present.
🌍 Example
A middleware platform syncing Salesforce data every hour uses its stored refresh token to silently mint a new access token each cycle, indefinitely.
-- Access token: short-lived, used on every API call -- Refresh token: long-lived, exchanged for a fresh access token when needed
🎤 “It's a long-lived credential that mints a fresh access token when the current one expires, so an unattended integration never has to re-prompt for login.”
Q0382.6–4.8 YOE

What are Platform Events and where do you use them?

A lightweight publish-subscribe messaging framework built into Salesforce, used to notify other systems or components of an event in near-real time without polling.
🔑 Key Points
Unlike a standard trigger reacting to a DML operation, Platform Events are explicitly published and can be consumed by both internal Apex/Flow subscribers and external systems via the Streaming API/CometD — making them ideal for decoupled, event-driven integration architectures.
🌍 Example
An external order-management system publishes a Platform Event whenever a shipment status changes, and Salesforce subscribes to update the related Case in near-real time.
Order_Status_Change__e event = new Order_Status_Change__e(Status__c = 'Shipped', Order_Id__c = orderId); EventBus.publish(event);
🎤 “A built-in publish-subscribe framework for near-real-time, decoupled communication between Salesforce and external systems — no polling required.”
Q0392.6–4.8 YOE

How do you make sure your integration isn't leaking data to a system that shouldn't have access to it?

Scope the integration user's profile/permission set to only the objects and fields actually required (least-privilege), enforce field-level security with WITH SECURITY_ENFORCED or WITH USER_MODE in queries, and restrict outbound payloads to only necessary fields.
🔑 Key Points
A dedicated integration user with a tightly scoped Permission Set is safer than reusing an admin-level account for the connection. Explicitly select only the fields the external system actually needs in outbound payloads rather than serializing entire sObjects, which can accidentally expose sensitive fields.
🌍 Example
A marketing automation integration should only be able to read Contact email and name fields, not sensitive financial fields on the same object.
List<Contact> contacts = [SELECT Id, Email, FirstName, LastName FROM Contact WITH USER_MODE]; // deliberately excludes sensitive custom fields not needed by the external system
🎤 “Least-privilege integration user, field-level enforcement on every query, and outbound payloads limited to exactly the fields the external system actually needs.”
🏦

Financial Services Cloud & Experience Cloud

Q40–Q44 · Industry-cloud-specific rounds PwC runs for FSC/Community projects

Q0405 YOE, FSC round

What is a Person Account, and why would a client need it?

A Person Account merges Account and Contact into a single record, used when you're dealing with individual consumers rather than business entities — very common in Financial Services Cloud for retail banking/wealth management clients.
🔑 Key Points
Enabling Person Accounts is irreversible at the org level, and PwC specifically probes candidates on this — it's a one-way door decision that must be made carefully during implementation planning, not something you casually enable.
🌍 Example
A retail bank's individual checking-account customers are modeled as Person Accounts, while their business/commercial clients remain standard Business Accounts with separate Contacts.
-- IsPersonAccount field distinguishes Person Accounts from Business Accounts on the same object
🎤 “A Person Account merges Account and Contact into one record for individual clients — common in FSC, and enabling it org-wide is a one-way, irreversible decision.”
Q0415 YOE, FSC round

Out of 100 Accounts in the org, how do you identify which ones are Person Accounts, and query for Person Accounts created in the last 4 days?

Filter on the standard IsPersonAccount boolean field combined with a CreatedDate date-literal filter.
🔑 Key Points
IsPersonAccount exists on the Account object automatically once the feature is enabled org-wide — no custom field needed. LAST_N_DAYS is the correct SOQL date literal for a rolling window rather than hardcoding an actual date.
🌍 Example
A daily onboarding report showing only new individual client sign-ups from the past few days, excluding business Accounts.
SELECT Id, Name FROM Account WHERE IsPersonAccount = true AND CreatedDate = LAST_N_DAYS:4
🎤 “IsPersonAccount = true filters the type, and CreatedDate = LAST_N_DAYS:4 gives the rolling 4-day window.”
Q0424.8 YOE, FSC round

Explain ARC (Actionable Relationship Center) in FSC, and what an Action Plan is used for.

ARC is a visual, interactive graph showing an Account's relationships to related Contacts and household members; an Action Plan is a templated, repeatable checklist of tasks tied to a client-facing process like onboarding.
🔑 Key Points
ARC is configured through Setup metadata defining which relationship types render on the graph and how far it traverses. Action Plans are built from Action Plan Templates so advisors don't manually recreate the same task checklist for every new client.
🌍 Example
A wealth advisor uses ARC to visually see a client's spouse and financial power-of-attorney relationships at a glance, and an Action Plan to walk through a standardized new-client onboarding checklist.
-- ARC config: Setup -> Actionable Relationship Center Settings -- Action Plan: built from an Action Plan Template, applied to a record
🎤 “ARC visualizes an Account's relationship graph, and Action Plans give advisors a repeatable, templated task checklist for standard processes like onboarding.”
Q0434.8 YOE, FSC round

Explain Email-to-Case, and how you'd display the complete email thread directly on the Case record page.

Email-to-Case auto-creates Cases from inbound emails to a routing address; the full thread is displayed using the standard EmailMessage related list component (or a custom LWC querying EmailMessage records related to the Case) on the record page.
🔑 Key Points
Every inbound/outbound message on that Case gets stored as an EmailMessage child record automatically — the "thread view" experience is really just a well-ordered display of these related records, not a separate feature.
🌍 Example
A support agent opens a Case and sees the entire back-and-forth email conversation with the customer inline, in chronological order, without leaving the record.
SELECT Id, Subject, TextBody, FromAddress, MessageDate FROM EmailMessage WHERE ParentId = :caseId ORDER BY MessageDate ASC
🎤 “Email-to-Case auto-creates the Case, and every message becomes an EmailMessage child record — the thread view is just those records rendered in chronological order on the page.”
Q0442.6–4.8 YOE

Flow vs Trigger — walk through exactly when you'd use one over the other.

Use Flow for admin-maintainable, moderately complex automation that doesn't need heavy custom logic or bulk-optimized loops; use Apex Triggers when you need fine-grained bulkification control, complex conditional logic, or integration-heavy operations that Flow can't cleanly express.
🔑 Key Points
Record-Triggered Flows now run before-save for simple field updates, which is actually more performant than an equivalent before-insert trigger in some cases — this changes the classic "Flow is always slower" assumption interviewers used to expect. That said, deeply nested conditional business logic is still usually cleaner and more maintainable in Apex.
🌍 Example
A simple "set Description to a default value on insert" belongs in a before-save Flow; a complex multi-object rollup with recursive-safe bulk logic belongs in a Trigger with a handler class.
-- Simple field default -> Record-Triggered Flow (before-save) -- Complex multi-object rollups, recursion control -> Apex Trigger + Handler class
🎤 “Flow for admin-maintainable, moderate automation — Apex Trigger when I need fine bulkification control or logic too complex for a formula-driven tool.”
🎛️

LWC Deep Dive

Q45–Q57 · Reactive binding, performance, testing, and security in custom components

Q0453–5 YOE

When and why would you use renderedCallback() specifically, instead of connectedCallback()?

Use renderedCallback() when logic depends on the DOM actually existing — like measuring an element's size, integrating a third-party JS library that needs a real DOM node, or focusing an input — since connectedCallback() fires before the template is rendered.
🔑 Key Points
renderedCallback() fires after every render, not just the first one, so any one-time setup logic (like loading a static resource) needs a guard flag to avoid running repeatedly. connectedCallback() is for setup that doesn't need the DOM — subscriptions, initial data fetch, reading @api values.
🌍 Example
Initializing a third-party charting library that needs an actual canvas element present in the DOM has to happen in renderedCallback(), guarded by a boolean so it only runs once.
renderedCallback() { if (this.chartInitialized) return; this.chartInitialized = true; const canvas = this.template.querySelector('canvas'); // initialize third-party library against the real DOM node }
🎤 “renderedCallback runs after the DOM exists, so it's for anything that needs a real element to attach to — guarded with a flag since it fires on every re-render.”
Q0463–5 YOE

How does reactive data binding work in LWC?

Any field on a component is reactive by default — reassigning it (or a @track-decorated nested property in legacy code) triggers the template to automatically re-render wherever that value is referenced.
🔑 Key Points
Since Spring '20, primitive fields are reactive without needing @track at all — @track is now only required for mutating a nested property inside an object or array in place, and even then the better practice is to reassign a new object/array entirely.
🌍 Example
Updating this.recordCount = 5 automatically re-renders any part of the template displaying {recordCount}, with no manual DOM manipulation required.
// Reassigning triggers reactivity automatically -- no @track needed for this this.records = [...this.records, newRecord];
🎤 “Fields are reactive by default now — reassigning a value or creating a fresh array/object automatically triggers a re-render wherever it's used in the template.”
Q0473–5 YOE

How would you implement conditional rendering based on dynamic data?

Use the lwc:if / lwc:elseif / lwc:else template directives (or the older if:true / if:false) bound to a getter or reactive property that evaluates the condition.
🔑 Key Points
lwc:if/elseif/else (introduced in Summer '23) replaced if:true/if:false as the recommended syntax and supports proper else-if branching, which the older directives never did cleanly. Prefer a getter over inline template logic for anything beyond a simple boolean check.
🌍 Example
Showing an "Approved" badge, a "Pending" spinner, or a "Rejected" banner depending on a record's Status field value.
<template lwc:if={isApproved}> <lightning-badge label="Approved"></lightning-badge> </template> <template lwc:elseif={isPending}> <lightning-spinner></lightning-spinner> </template> <template lwc:else> <lightning-badge label="Rejected" variant="error"></lightning-badge> </template>
🎤 “lwc:if / lwc:elseif / lwc:else bound to getters — that's the current recommended syntax over the older if:true/if:false directives.”
Q0483–5 YOE

How do you communicate between sibling components in LWC that share a common parent?

Sibling A dispatches a CustomEvent that bubbles up to the shared parent; the parent's handler updates a property it passes down as an @api input to Sibling B.
🔑 Key Points
Siblings never talk to each other directly — everything routes through the parent, which is the "single source of truth" pattern LWC is built around. For siblings on entirely different parts of the page (not sharing an immediate parent), LMS is the right tool instead.
🌍 Example
A filter component and a results-list component sit side by side under a shared parent — selecting a filter dispatches an event the parent catches, then passes the new filter value down to the results component as a public property.
// In the parent, listening to sibling A's event and updating sibling B's input handleFilterChange(event) { this.selectedFilter = event.detail.value; // passed to sibling B via @api }
🎤 “Siblings never talk directly — the event bubbles up to the shared parent, which passes the new value down to the other sibling as a public property.”
Q0493–5 YOE

How can you make an LWC datatable editable and persist the changes back to Salesforce?

Set editable: true on the relevant lightning-datatable columns, capture the ondave event's draftValues, and pass them to an Apex method (or the LDS updateRecord wire function) to commit the changes.
🔑 Key Points
draftValues only contains the changed fields per row, not the entire record — build your update payload from exactly that diff. After a successful save, clear draftValues and call refreshApex() so the table reflects the committed state.
🌍 Example
A sales manager inline-edits five Opportunity Amounts directly in a datatable and clicks Save once to commit all five changes in a single bulk Apex call.
async handleSave(event) { const updatedFields = event.detail.draftValues; await updateOpportunities({ opps: updatedFields }); this.draftValues = []; await refreshApex(this.wiredOpps); }
🎤 “editable columns plus the onsave event's draftValues, sent to Apex as a bulk update, then draftValues cleared and refreshApex called to sync the UI.”
Q0503–5 YOE

Describe how you'd handle pagination in an LWC component showing thousands of records.

Query in fixed-size pages from Apex using LIMIT/OFFSET (or a keyset/cursor-based approach for very large datasets), track the current page number as component state, and fetch only the next page on demand rather than loading everything upfront.
🔑 Key Points
OFFSET-based pagination degrades in performance the deeper you paginate (OFFSET 50000 still has to skip 50,000 rows internally) — for very large datasets, keyset pagination (WHERE Id > :lastSeenId ORDER BY Id LIMIT n) scales much better. lightning-datatable's built-in infinite scroll (enable-infinite-loading) is often simpler than building custom Previous/Next buttons.
🌍 Example
A Case list view showing 50,000 records loads only 50 at a time, fetching the next batch as the user scrolls near the bottom of the table.
@AuraEnabled(cacheable=true) public static List<Case> getCasesPage(Id lastId, Integer pageSize) { String query = 'SELECT Id, Subject FROM Case '; if (lastId != null) query += 'WHERE Id > :lastId '; query += 'ORDER BY Id LIMIT :pageSize'; return Database.query(query); }
🎤 “Fixed-size pages fetched on demand — keyset pagination for very large datasets since OFFSET gets slower the deeper you page.”
Q0513–5 YOE

How would you optimize component performance when working with large datasets in LWC?

Paginate or virtualize the list rather than rendering everything at once, avoid deeply nested iteration in the template, and minimize the number of reactive property reassignments that trigger re-renders.
🔑 Key Points
Rendering thousands of DOM nodes at once is almost always the real bottleneck, not the data fetch itself — virtualization (rendering only visible rows) solves this directly. Batching multiple state changes together before triggering a single re-render avoids redundant render cycles.
🌍 Example
Switching a 10,000-row custom table from rendering every row via for:each to a virtualized list that only renders the ~30 rows currently visible in the viewport.
-- Techniques: pagination/virtualization, lazy-loading, avoiding nested for:each, -- batching reactive updates, cacheable wire methods to avoid redundant server calls
🎤 “Virtualize or paginate rather than rendering everything at once — that's almost always the real bottleneck with large datasets, not the data fetch.”
Q0523–5 YOE

Beyond large datasets specifically, what general strategies would you employ to enhance LWC performance?

Use cacheable=true wire methods wherever possible, lazy-load heavy child components, debounce rapid user input (like search-as-you-type), and keep component trees shallow rather than deeply nested.
🔑 Key Points
Every level of component nesting adds render overhead — flatter component hierarchies generally outperform deeply nested ones for the same amount of content. Debouncing search input prevents firing an Apex call on every single keystroke.
🌍 Example
A live search box waits 300ms after the user stops typing before firing the Apex search call, instead of querying on every keypress.
handleSearchInput(event) { window.clearTimeout(this.delayTimeout); const searchTerm = event.target.value; this.delayTimeout = setTimeout(() => { this.performSearch(searchTerm); }, 300); }
🎤 “Cacheable wire methods, debounced input, lazy-loaded heavy children, and flatter component trees — those four cover most performance wins.”
Q0533–5 YOE

What are the considerations for handling server responses and errors when calling Apex imperatively from LWC?

Always pwc-wrap the imperative call in try/catch (or .then/.catch for the Promise form), surface a user-friendly toast message rather than the raw error, and distinguish between handled Apex exceptions (with a clear message) versus unexpected system errors.
🔑 Key Points
The error object's shape differs depending on the failure type — a thrown AuraHandledException gives a clean message via error.body.message, while other failures may need deeper inspection of error.body.pageErrors or fieldErrors. Never show a raw stack trace to an end user.
🌍 Example
A Save button shows a specific "This Opportunity is already Closed Won" toast (from an AuraHandledException) rather than a generic "An error occurred" message.
try { await saveOpportunity({ opp: this.record }); } catch (error) { const message = error.body?.message || 'An unexpected error occurred'; this.dispatchEvent(new ShowToastEvent({ title: 'Error', message, variant: 'error' })); }
🎤 “Try/catch around every imperative call, AuraHandledException for clean user-facing messages, and a toast instead of ever exposing a raw stack trace.”
Q0543–5 YOE

How do you debug performance issues in LWC components?

Use the browser's Performance tab to profile render/script time, the Lightning Component Inspector for component-tree-level insight, and check the Network tab for redundant or slow Apex calls.
🔑 Key Points
A common finding is a wire method re-firing more often than expected because a reactive parameter is changing unintentionally — the Inspector's re-render tracking helps pinpoint exactly which property change triggered it. Chrome DevTools' Performance recording shows exactly where script time is being spent, frame by frame.
🌍 Example
Profiling reveals a wire-adapted Apex call is firing on every keystroke because its reactive parameter was accidentally bound to an unstable object reference instead of a primitive value.
-- Tools: Chrome DevTools Performance tab, Lightning Component Inspector, -- Network tab (look for duplicate/redundant Apex calls), console.time()/console.timeEnd()
🎤 “Chrome DevTools Performance tab plus the Lightning Component Inspector — most performance bugs trace back to a wire method re-firing more often than it should.”
Q0553–5 YOE

What tools and techniques do you use for testing LWC components?

Jest with the sfdx-lwc-jest test runner for unit testing component logic and rendering, combined with mocked wire adapters/Apex responses so tests don't depend on a live org.
🔑 Key Points
You mock @wire and imperative Apex calls entirely in Jest — tests never actually hit Salesforce, which keeps them fast and deterministic. Test both the "happy path" data response and the error/empty-state rendering explicitly, since those are easy to leave uncovered.
🌍 Example
A Jest test mocks getAccounts to return a fixed list of Accounts, then asserts the rendered component shows the correct number of rows.
import { createElement } from 'lwc'; import MyComponent from 'c/myComponent'; import getAccounts from '@salesforce/apex/AccountController.getAccounts'; jest.mock('@salesforce/apex/AccountController.getAccounts', () => ({ default: jest.fn() }), { virtual: true });
🎤 “Jest via sfdx-lwc-jest, with wire adapters and Apex calls fully mocked so tests run fast and don't depend on a live org.”
Q0563–5 YOE

What security considerations should be taken when building custom functionality in LWC?

Never trust client-side validation alone — always re-validate on the Apex side, enforce CRUD/FLS/sharing on every server-side query, and avoid exposing sensitive data in component properties that a user could inspect via browser dev tools.
🔑 Key Points
Anything sent to the client — including data in @api properties — is visible to a technically curious user via browser dev tools, so never pass sensitive fields down "just in case" they're needed later. Apex methods called from LWC still need explicit sharing enforcement; LWC itself provides zero security on its own.
🌍 Example
A component that displays partial Account financials should query only the fields actually rendered, not fetch the entire record and simply hide unwanted fields in the template.
-- Never rely on hiding a field in the template as "security" -- Query only what's needed, enforce FLS server-side with WITH USER_MODE
🎤 “Client-side is never trusted — every check gets re-enforced in Apex, and I never pass more data to the component than what's actually rendered.”
Q0573–5 YOE

How do you ensure compliance with CRUD, FLS, and sharing rules specifically when an LWC calls Apex?

Use WITH USER_MODE (or WITH SECURITY_ENFORCED) on every SOQL query, keep Apex controllers declared with sharing rather than without sharing unless there's a deliberate reason otherwise, and use Schema methods like isAccessible()/isUpdateable() before DML where dynamic field access is involved.
🔑 Key Points
WITH USER_MODE is the currently recommended approach over WITH SECURITY_ENFORCED — it enforces checks across the entire query including WHERE clauses and polymorphic fields, and surfaces all access errors at once rather than failing on just the first one. This applies to both @AuraEnabled imperative methods and cacheable wire-adapted ones.
🌍 Example
An Apex method exposed to a Community/Experience Cloud LWC absolutely must enforce FLS and sharing, since an external user's access should never exceed what their profile and sharing rules actually permit.
@AuraEnabled(cacheable=true) public static List<Contact> getContacts(Id accountId) { return [SELECT Id, Name, Email FROM Contact WHERE AccountId = :accountId WITH USER_MODE]; }
🎤 “WITH USER_MODE on every query, with sharing on the class, and explicit Schema access checks anywhere field access is dynamic.”
🧠

Real-World Scenario Round

Q58–Q67 · Architecture-style scenarios worth rehearsing before any senior Salesforce interview

Q058Scenario

Your client runs three different retail brands on one Salesforce org, and each brand wants its own picklist values, page layouts, and approval process — but shares the same Account and Opportunity objects. How do you design this?

Use Record Types per brand to drive distinct picklist value sets and page layouts, with brand-specific approval processes each entered via a matching Record Type criteria condition.
🔑 Key Points
Record Types are the correct lever here, not separate objects — separate objects would break reporting and rollups across the shared customer base. Keep a single unified Opportunity Stage set where possible; only fork picklists that genuinely differ per brand, or reporting gets fragmented later.
🌍 Example
A multi-brand apparel retailer wants Brand A's Opportunities to skip a discount-approval step that Brand B always requires — Record Type-scoped approval processes handle this cleanly without duplicating the object.
-- One Opportunity object, three Record Types (BrandA, BrandB, BrandC) -- Approval Process entry criteria: RecordType.DeveloperName = 'BrandB_Opportunity'
🎤 “Record Types per brand driving layouts, picklists, and approval process entry criteria — keeping it one object so cross-brand reporting still works.”
Q059Scenario

Sales leadership wants to realign territories mid-quarter, reassigning 3,000 Accounts to new owners overnight without disrupting active deals. Walk through your rollout plan.

Stage the reassignment in a sandbox first, run it via a bulk data tool (Data Loader/Bulk API) during off-hours with automation temporarily bypassed, then validate a sample of reassigned Accounts before re-enabling automation.
🔑 Key Points
Bypass triggers/Flows during the mass update to avoid firing thousands of unnecessary email alerts or rollup recalculations meant for real-time single-record changes. Communicate the exact cutover window to sales reps in advance so they're not confused by ownership changes mid-conversation with a customer.
🌍 Example
A CSV mapping old-owner-to-new-owner drives a Bulk API update job scheduled for 2 AM, with a Custom Setting bypass flag flipped on beforehand and off again after validation.
-- 1. Test in sandbox with production data volume -- 2. Bypass automation via Custom Setting -- 3. Bulk API update during off-hours -- 4. Validate sample records, re-enable automation -- 5. Notify affected sales reps of the cutover
🎤 “Sandbox test first, automation bypassed during the load, Bulk API off-hours, validate a sample, then re-enable automation and notify the affected reps.”
Q060Scenario

An outbound integration to a shipping partner's API fails intermittently due to timeouts, and you're seeing duplicate shipment records created on retry. How do you fix this?

Implement an idempotency key (a unique identifier generated once per logical request) that the external system uses to recognize and reject duplicate retries, combined with exponential backoff on the retry logic itself.
🔑 Key Points
The duplicate-creation problem happens because a timeout doesn't tell you whether the original request actually succeeded server-side before failing to respond — retrying blindly risks creating it twice. An idempotency key generated client-side (a GUID stored with the record before the first attempt) lets the receiving system safely ignore a repeated request with the same key.
🌍 Example
A Queueable Apex job generates a UUID for each shipment request before the first callout attempt, storing it on the record so any retry — even a manual one — reuses the same key.
Shipment__c ship = new Shipment__c(Idempotency_Key__c = new Uuid().toString()); insert ship; // same key reused on every retry attempt for this shipment
🎤 “An idempotency key generated once and reused on every retry, so the receiving system can safely reject duplicates — plus exponential backoff on the retry itself.”
Q061Scenario

Support SLAs are calculated as a simple 24-hour countdown on Case, but leadership now wants the countdown to pause on weekends and company holidays. How do you implement this without breaking existing reports?

Use Salesforce's built-in Business Hours object (with holidays configured) combined with Entitlement Processes/Milestones, which natively calculate elapsed time against business hours rather than wall-clock time.
🔑 Key Points
Rebuilding this with custom Apex date math is a common over-engineering trap — Business Hours plus Entitlements already solve exactly this problem out of the box, including holiday exceptions. Keep the existing SLA field as a formula referencing the Milestone's calculated time remaining, so existing reports keep working without restructuring.
🌍 Example
A Case logged Friday at 5 PM against a 24-business-hour SLA doesn't breach until well into the following week, correctly skipping the weekend and any configured holidays.
-- Setup -> Business Hours -> configure holidays -- Setup -> Entitlement Process -> Milestone using that Business Hours record
🎤 “Business Hours with holidays configured, paired with Entitlements and Milestones — that's the native mechanism, no custom date-math trigger needed.”
Q062Scenario

Marketing just ran a campaign that created 2,000 duplicate Leads for people who already exist as Contacts. How do you clean this up without losing campaign attribution?

Use Matching Rules and Duplicate Rules to identify the overlaps, then merge or convert the duplicate Leads into the existing Contacts using a process that explicitly re-parents the CampaignMember record rather than deleting it outright.
🔑 Key Points
Straight-up deleting duplicate Leads destroys the CampaignMember association and loses the attribution data marketing actually cares about — the fix has to preserve that link, typically by converting the Lead against the existing Contact/Account rather than deleting. For future prevention, tighten the Matching Rule criteria and enable duplicate warnings on the Lead object before the next campaign.
🌍 Example
A batch job identifies Leads whose email matches an existing Contact, converts each one against that Contact's Account, and re-associates the CampaignMember to preserve the "responded to campaign" attribution.
-- Identify duplicates via Matching Rule -- For matches: Lead Convert against existing Account/Contact (not delete) -- Re-parent CampaignMember to preserve attribution
🎤 “Convert the duplicate Leads against the existing Contact rather than deleting them, and explicitly re-parent the CampaignMember so attribution data survives the cleanup.”
Q063Scenario

You need to mass-reparent 10,000 Contacts to different Accounts as part of a data cleanup, but Account has several roll-up summary fields that depend on Contact counts. How do you avoid corrupting those rollups?

Batch the reparenting through Batch Apex rather than a raw data load, letting standard roll-up summary recalculation fire naturally per chunk instead of in one giant uncontrolled burst, and validate rollup totals on a sample of both old and new parent Accounts afterward.
🔑 Key Points
A raw Bulk API load with automation disabled will leave the old and new parent Accounts' rollups stale, since disabling automation to speed up the load also skips the rollup recalculation — you have to explicitly recalculate afterward if you go that route. Batch Apex naturally re-triggers rollups per chunk since it goes through normal DML, avoiding that gap.
🌍 Example
After reparenting, both the old Account (now with fewer Contacts) and the new Account (now with more) need their Total_Contacts__c rollups verified against an actual COUNT() query to confirm nothing drifted.
-- Batch Apex reparents Contacts in scope-sized chunks (normal DML, rollups fire naturally) -- Post-migration: validate rollup totals via aggregate query on a sample of old + new parents
🎤 “Batch Apex rather than a raw bulk load, so standard rollup recalculation fires naturally — then validate both old and new parent totals afterward with an aggregate query.”
Q064Scenario

Approval routing today is one hardcoded approver, but the business now wants routing to vary by deal size AND region — small EMEA deals need one approver, large APAC deals need a different chain entirely. How do you design this to stay maintainable?

Use a single Approval Process with multiple approval steps gated by step-level criteria, or better, drive the approver assignment dynamically via a Custom Metadata Type lookup table (Region + Amount Tier → Approver) referenced by a Flow, rather than hardcoding logic into the approval process itself.
🔑 Key Points
Hardcoding approver combinations directly into multiple Approval Processes becomes an unmaintainable mess as region/tier combinations grow — a Custom Metadata-driven lookup keeps the business rule data-driven and editable by admins without a deployment. This pattern scales far better than a Process Builder-era "one approval process per combination" approach.
🌍 Example
Adding a new region next year is just a new row in the Custom Metadata Type, not a new Approval Process requiring a full deployment cycle.
-- Custom Metadata Type: Approval_Routing__mdt -- Fields: Region__c, Min_Amount__c, Max_Amount__c, Approver__c -- Flow queries this table to dynamically set the Submit for Approval assignee
🎤 “A Custom Metadata-driven lookup table for Region and Amount Tier to Approver, referenced dynamically by the approval flow — keeps it data-driven instead of a maintenance nightmare.”
Q065Scenario

A multi-currency org's Opportunity roll-up totals on Account look wrong for international customers — the numbers don't match what finance expects. What's likely going on, and how do you fix it?

Roll-up summary fields sum in the child record's own currency by default rather than converting to a common currency first — the fix is to roll up the Amount (converted) field instead, or handle the conversion explicitly via a formula/Apex-based rollup using current exchange rates.
🔑 Key Points
This is a very common and non-obvious multi-currency gotcha — a native roll-up summary field literally cannot convert currency, so mixed-currency child records get summed as if they were the same currency, producing meaningless totals. The dated exchange rate used matters too — "converted" fields use the rate as of the record's own date field, which can differ from today's rate.
🌍 Example
An Account with one €50,000 Opportunity and one $50,000 Opportunity shows a native rollup of "100,000" with no currency meaning at all — using the Amount (converted) field and rolling up in the Account's own currency fixes this.
-- Native rollup: sums raw Amount, ignoring currency -- broken for multi-currency orgs -- Fix: roll up Amount (converted) field, or use an Apex/Flow-based rollup -- that explicitly converts each child to the Account's currency
🎤 “Native roll-up summary fields don't convert currency at all — the fix is rolling up the converted Amount field, or handling conversion explicitly in Apex.”
Q066Scenario

Partner portal users are reporting they can see Opportunities belonging to other partner companies, not just their own. Where do you start investigating, and how do you fix it?

Check the Partner community's Sharing Set/Sharing Rule configuration first — most likely OWD for Opportunity is more permissive than intended for the external org-wide defaults, or a sharing rule is scoped too broadly (e.g., shared to a Role instead of scoped per-Account).
🔑 Key Points
This is a genuinely serious data-exposure bug, not a cosmetic issue — partner portals need to be treated with the same rigor as Guest User security, since a misconfiguration here directly leaks one customer's pipeline data to a competitor. Confirm the fix in a sandbox with two test partner users from different companies before pushing to production.
🌍 Example
A sharing rule intended to give partner users access to "their own Account's Opportunities" was accidentally built as a broad Role-based rule instead of an Account-team-based one, exposing every partner's pipeline to every other partner.
-- Checklist: -- 1. External OWD for Opportunity -- should be Private -- 2. Sharing Set / Sharing Rule scope -- must be Account-relationship-based, not Role-based -- 3. Partner Super User checkbox -- verify it's not over-granted
🎤 “This is a real data-exposure issue — I'd check external OWD and Sharing Set scoping first, since this pattern is almost always an over-broad sharing rule rather than a code bug.”
Q067Scenario

A nightly batch job that recalculates pricing across 500,000 Product records sometimes overlaps with the start of business hours in APAC, causing Row Lock errors for live users editing Opportunities. How do you resolve this?

Tighten the batch's scheduled window so it reliably finishes before APAC business hours begin, reduce the batch scope size to shorten total run time, and consider using FOR UPDATE row locking deliberately with a shorter lock window rather than letting contention happen unpredictably.
🔑 Key Points
Row Lock (UNABLE_TO_LOCK_ROW) errors happen when two transactions try to update the same record at the same time — the real fix is scheduling around peak usage windows per timezone, not just catching and retrying the error after the fact. If the batch touches records users are actively editing, splitting it into smaller, faster-committing chunks reduces the window where a lock conflict can occur.
🌍 Example
Rescheduling the batch from 6 AM to 2 AM local APAC time, well before the first regional users log in, eliminates the overlap entirely without touching any code.
-- 1. Reschedule batch to run fully before regional business hours -- 2. Reduce batch scope size for faster per-chunk commit -- 3. Add retry-with-backoff for the rare remaining lock conflict
🎤 “First fix is scheduling — move the batch window earlier so it's done before APAC logs in — then reduce scope size and add retry-with-backoff as a safety net.”
🎯

A Few More From the Field

Q68–Q70 · Additional questions worth having ready

Q0684–5 YOE

You want to clone an Opportunity along with all of its related Contact Roles. How do you handle the deep clone?

Clone the parent Opportunity with Opportunity.clone(), insert it to get a new Id, then query the original's OpportunityContactRole child records, clone each one, repoint them at the new Opportunity Id, and insert them in bulk.
🔑 Key Points
sObject.clone() only performs a shallow clone of the record itself — child/related records are never carried over automatically and always need to be explicitly queried, cloned, and re-parented in code. Remember to reset system fields (Id, CreatedDate, IsClosed if applicable) since clone() strips most of these by default but it's worth being explicit for fields that matter to your logic.
🌍 Example
A sales rep wants to duplicate a complex, multi-stakeholder Opportunity as a template for a similar new deal, keeping the same set of Contact Roles without manually re-adding each one.
Opportunity newOpp = originalOpp.clone(false, true, false, false); insert newOpp; List<OpportunityContactRole> newRoles = new List<OpportunityContactRole>(); for (OpportunityContactRole ocr : [SELECT ContactId, Role, IsPrimary FROM OpportunityContactRole WHERE OpportunityId = :originalOpp.Id]) { newRoles.add(new OpportunityContactRole(OpportunityId = newOpp.Id, ContactId = ocr.ContactId, Role = ocr.Role, IsPrimary = ocr.IsPrimary)); } insert newRoles;
🎤 “clone() only handles the parent record shallowly — I explicitly query, clone, and re-parent the child Contact Roles against the new Opportunity Id myself.”
Q0694–5 YOE

Can you call a @future method from another @future method?

No — Salesforce explicitly disallows a future method invoking another future method, and doing so throws a runtime exception.
🔑 Key Points
If you need chained asynchronous steps, Queueable Apex is the correct tool instead — a Queueable job can enqueue another Queueable job from within its execute() method, which future methods were never designed to support. This restriction exists specifically to prevent uncontrolled recursive queuing.
🌍 Example
A future method that sends a notification, then needs to trigger a second async step, has to be redesigned as Queueable Apex to support that chaining legally.
@future public static void stepOne() { // stepTwo(); -- NOT ALLOWED if stepTwo is also @future, throws an exception }
🎤 “No, that's explicitly disallowed and throws at runtime — if I need chaining, I redesign it as Queueable Apex instead.”
Q0704–5 YOE

You're building a configurable solution whose behavior needs to differ across Dev, QA, and Production. Would you use Custom Settings or Custom Metadata, and why?

Custom Metadata Types, because they're deployable via change sets/packages like real metadata, whereas Custom Settings data has to be manually re-entered (or loaded separately) in each environment since it doesn't move with a deployment.
🔑 Key Points
This distinction trips people up constantly: Custom Metadata records ARE metadata and deploy with your package; Custom Setting records are just data and stay behind in each org. Custom Metadata also supports being queried in a test class without needing @isTest(SeeAllData=true), which Custom Settings historically required more workarounds for.
🌍 Example
An integration endpoint URL that's genuinely different per environment (Dev sandbox endpoint vs. Production endpoint) is a textbook Custom Metadata use case, deployed automatically as part of the release.
-- Custom Metadata: deploys with the package, ideal for per-environment config that moves with releases -- Custom Setting: data only, must be populated separately per org after deployment
🎤 “Custom Metadata — it deploys as part of the package across Dev, QA, and Prod, whereas Custom Setting data has to be manually re-populated in every environment separately.”

SF Interview Pro is a free, full-stack Salesforce career platform — no signup, no paywall. Visit sfinterviewpro.com →

Test yourself on this topic
2,244 practice MCQs across 27 quizzes — 5 quizzes free, no signup
Open Practice Zone →
RK
Written by
Rajnish Kumar
Salesforce Developer · Apex, LWC, Data Cloud & AI · Building SF Interview Pro
Connect on LinkedIn ↗
Testimonials

Real feedback from real candidates

People using SF Interview Pro to prepare for Salesforce interviews
★★★★★
"Crashed multiple interviews thanks to this. Better than the paid courses I tried."
SP
Salesforce Professional
Developer
★★★★★
"No signup, no paywall. The Apex Trigger series alone got me through two rounds."
AK
Aditya K.
Salesforce Admin
★★★★★
"LWC Zero to Hero is more practical than any paid course I found."
RM
Riya M.
LWC Developer
★★★★★
"Practice Zone MCQs matched my actual interview difficulty almost exactly."
PS
Priya S.
Business Analyst
★★★★★
"Started knowing nothing. Three weeks later I had two offers."
MK
Mohit K.
Salesforce Admin
★★★★★
"Company-wise Accenture prep was scarily accurate."
SN
Sneha N.
Consultant
★★★★★
"Explains identity resolution better than official Trailhead modules."
VR
Vikram R.
Data Cloud Consultant
★★★★★
"Free and better than the paid prep I bought earlier."
TJ
Tanvi J.
Fresher
★★★★★
"Reports and Dashboards guide made a tricky topic finally click."
KP
Karan P.
Salesforce Admin
★★★★★
"SOQL Part 2 questions came up almost word for word in my interview."
AN
Ananya N.
Developer
★★★★★
"Field Service Lightning guide is the only good free resource I found."
RD
Rohan D.
FSL Consultant