The developer scenario round does not ask you to define Batch Apex. It describes a system that is breaking or growing, then waits to see which tool you reach for and whether you can defend the choice when the alternative is put in front of you. The nineteen scenarios below come from real developer interview rounds and cover Apex and trigger design, callouts and integration, deployment between environments, and Lightning Web Components. Each answer gives the approach, the code where code is the real answer, the reason for picking it over the obvious alternative, and the question the interviewer usually asks straight after.
Apex, triggers and governor limits
You must process 1 million records. What is the best approach?
Batch Apex. The decision is not "batch is for big data". It is that a synchronous transaction can query 50,000 rows and update 10,000 rows, so one million records cannot fit in any single transaction no matter how tightly you write it. Batch Apex splits the work into chunks and gives each chunk a fresh set of governor limits.
public class AccountRatingBatch implements Database.Batchable<SObject>, Database.Stateful {
public Integer recordsUpdated = 0;
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, AnnualRevenue, Rating FROM Account WHERE Rating = null'
);
}
public void execute(Database.BatchableContext bc, List<Account> scope) {
List<Account> toUpdate = new List<Account>();
for (Account a : scope) {
a.Rating = (a.AnnualRevenue != null && a.AnnualRevenue > 1000000) ? 'Hot' : 'Warm';
toUpdate.add(a);
}
for (Database.SaveResult sr : Database.update(toUpdate, false)) {
if (sr.isSuccess()) { recordsUpdated++; }
}
}
public void finish(Database.BatchableContext bc) {
System.debug('Records updated: ' + recordsUpdated);
}
}
Two details carry the answer. Database.getQueryLocator is what lets the job reach far past the synchronous query row limit, which is why you return a QueryLocator rather than an Iterable. And Database.update(toUpdate, false) is a partial save, so one bad record kills one row instead of the whole chunk.
Why not a scheduled Flow or a simple @future method. A Flow still runs inside transaction limits per batch of records and gives you no partial-failure control. A future method is a single transaction, so it dies at the same wall you were trying to avoid.
The follow-up: "your batch is timing out on a heavy execute, what do you change?" The answer is the scope size in Database.executeBatch(new AccountRatingBatch(), 50). Smaller scope means more executions but less CPU time in each. The second follow-up is Database.Stateful, which is the only reason recordsUpdated survives from one chunk to the next.
Triggers are hitting governor limits. What should you do?
Bulkify. In practice that means one thing: no SOQL and no DML inside a loop. Every developer says this in the interview. Fewer can show the rewrite, and showing the rewrite is what the interviewer is waiting for. As our trainer puts it, candidates fail on demonstration, not on knowledge, because they know the topic and cannot show it.
Here is the pattern that fails on a data load of 200 records:
// Fails at 101 records: one query and one update per iteration
for (Opportunity opp : Trigger.new) {
List<Account> accts = [SELECT Id, Type FROM Account WHERE Id = :opp.AccountId];
accts[0].Type = 'Customer';
update accts[0];
}
And the rewrite:
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : Trigger.new) {
if (opp.AccountId != null) { accountIds.add(opp.AccountId); }
}
Map<Id, Account> accountMap = new Map<Id, Account>(
[SELECT Id, Type FROM Account WHERE Id IN :accountIds]
);
List<Account> toUpdate = new List<Account>();
for (Opportunity opp : Trigger.new) {
Account acct = accountMap.get(opp.AccountId);
if (acct != null && acct.Type != 'Customer') {
acct.Type = 'Customer';
toUpdate.add(acct);
}
}
if (!toUpdate.isEmpty()) { update toUpdate; }
One query, one DML, regardless of whether the trigger fires on 1 record or 200. The acct.Type != 'Customer' check does double duty, because it also stops the same account being added twice when two opportunities in the same batch share a parent.
Why not just raise the limit or move the logic to a Flow. Limits are not raisable, and a record-triggered Flow running for each record has its own ceiling. The fix is the shape of the code, not the container.
The follow-up: "which limit exactly were you hitting?" Have the numbers ready. 100 SOQL queries, 150 DML statements, 50,000 query rows and 10,000 DML rows per synchronous transaction, with 10,000 ms of CPU time. If you are not sure in the room, say which one you hit in your own project and how you proved it, using Limits.getQueries() and Limits.getCpuTime() in the debug log.
You must execute logic asynchronously after record save. What option is available?
Queueable Apex, with @future only as a fallback on legacy code. Both run after the transaction commits, so the decision comes down to what you need to pass in and what happens next.
public class InvoiceSyncQueueable implements Queueable, Database.AllowsCallouts {
private List<Id> invoiceIds;
public InvoiceSyncQueueable(List<Id> invoiceIds) {
this.invoiceIds = invoiceIds;
}
public void execute(QueueableContext context) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Billing_System/invoices');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(invoiceIds));
req.setTimeout(30000);
HttpResponse res = new Http().send(req);
if (res.getStatusCode() != 200) {
Logger.logError('InvoiceSyncQueueable', res.getBody());
}
}
}
Called from the handler as System.enqueueJob(new InvoiceSyncQueueable(new List<Id>(invoiceIds)));.
Why Queueable over @future. A future method only accepts primitives and collections of primitives, so an sObject or a wrapper class has to be flattened into IDs and re-queried. Queueable takes real object state through the constructor, returns a job ID you can monitor in AsyncApexJob, and can chain another job from inside execute. A future method gives you none of that.
Our trainer's line on this is that candidates know the syntax but not the execution: when it runs, where it runs, and what data it can reach. Be ready to say that the queued job runs in its own transaction with its own limits, that it does not see uncommitted data from the calling transaction, and that Database.AllowsCallouts is what makes the HTTP call legal here.
The follow-up: "the job failed, how does the user find out?" There is no user session attached to an async job, so the answer is a log record, a platform event, or an email from the finish phase of a batch. Never a System.debug.
Large datasets must be queried efficiently. What should you ensure?
Make the query selective, which means the filter uses an indexed field and returns a small enough fraction of the object for the optimizer to use that index. If it is not selective, Salesforce scans the table and a large object throws a non-selective query error.
Indexed out of the box: Id, Name, OwnerId, CreatedDate, SystemModstamp, RecordTypeId, master-detail and lookup fields, and any field marked External ID or Unique. Anything else needs a custom index raised through Salesforce support.
What quietly destroys selectivity:
| Pattern | Why the index is skipped |
|---|---|
WHERE Name LIKE '%tech' |
Leading wildcard cannot use an index |
WHERE Status__c != 'Closed' |
Negative operators are not indexed |
WHERE Region__c = null |
Nulls are not indexed unless the index is built for them |
WHERE Score__c > 0 on a cross-object formula |
Non-deterministic formula fields cannot be indexed |
| Filtering in Apex after querying everything | The database never sees the filter |
The documented selectivity thresholds are roughly 30 percent of the first million rows for a standard index and roughly 10 percent for a custom index, with an absolute row ceiling on top. Treat those as order of magnitude rather than exact numbers, and say so in the interview. Getting the principle right matters more than quoting a percentage that shifts between releases.
Why not just add LIMIT 100. A LIMIT caps what comes back, not what the database examines. The scan still happens.
The follow-up: "the query is still slow after indexing." Then you talk about skinny tables for wide objects with many fields, archiving old data, and splitting the work into Batch Apex so the query runs through a QueryLocator instead of a single synchronous read.
Multiple triggers exist on the same object causing issues. What is best practice?
One trigger per object, and that trigger contains no business logic. It only routes context to a handler class. Multiple triggers on one object are a real problem because Salesforce does not guarantee their execution order, so the behaviour depends on which trigger happens to run first.
trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
AccountTriggerHandler handler = new AccountTriggerHandler();
switch on Trigger.operationType {
when BEFORE_INSERT { handler.beforeInsert(Trigger.new); }
when BEFORE_UPDATE { handler.beforeUpdate(Trigger.new, Trigger.oldMap); }
when AFTER_INSERT { handler.afterInsert(Trigger.new); }
when AFTER_UPDATE { handler.afterUpdate(Trigger.new, Trigger.oldMap); }
}
}
The handler holds the logic, which makes it unit testable without DML and reusable from Batch Apex or a controller. Add a static guard so a recursive update does not re-enter:
public class TriggerControl {
public static Boolean accountUpdateRun = false;
}
Static variables live for the length of the transaction, which is exactly the scope a recursion guard needs.
Why not keep separate triggers and just be careful. Because order is not yours to control, and the next developer adds a fifth trigger. A handler also lets you switch logic off per environment through a custom metadata type without editing Apex.
The follow-up: "where do you put the before-save Flow in this picture?" Order of execution. Before-save Flows run before before triggers, and the interviewer wants to know that you place field defaulting in the Flow and cross-object logic in the trigger, rather than letting the two fight over the same field.
How would you log errors in Apex for debugging?
System.debug is for the developer console during development. It is not a logging strategy, because an async job that fails at 2 am leaves no log anyone will read, and debug logs expire.
The pattern that holds up in production is a custom logging framework built on a platform event, because a platform event published with the Publish Immediately behaviour survives a rollback. That matters: if your transaction throws and you logged to a custom object, the log record rolls back with everything else and you are left with nothing.
public class Logger {
public static void logError(String source, String message) {
EventBus.publish(new Log__e(
Source__c = source,
Message__c = message,
Severity__c = 'ERROR',
User__c = UserInfo.getUserId()
));
}
public static void logError(String source, Exception e) {
logError(source, e.getTypeName() + ': ' + e.getMessage()
+ '\n' + e.getStackTraceString());
}
}
A trigger on Log__e then writes Log__c records in a separate transaction, and those survive.
Wrap the risky work rather than the whole class:
try {
update accounts;
} catch (DmlException e) {
Logger.logError('AccountTriggerHandler.afterUpdate', e);
throw e;
}
Log and rethrow, so the user still sees the failure. A swallowed exception is worse than no logging.
The follow-up is almost always a debugging question rather than a definition one, and this is where interviews are lost. Our trainer's version is that candidates cannot debug, and "your trigger is not firing, what do you check?" beats "what is a trigger?" every time. Have the sequence ready: is the trigger active, is the user's profile bypassed by a custom setting, is a validation rule failing first, is the record being changed by a process that runs after, and what does the debug log with the Apex level raised to FINEST actually show.
What the interviewer is actually checking in this section
- Whether you can rewrite unbulkified code on a whiteboard, not just name the rule
- Whether you know which limit applies to which context, synchronous against asynchronous
- Whether you can say when your code runs and what data it can reach at that moment
- Whether your first instinct on a failure is a diagnostic sequence rather than a guess
Integration and callouts
An external system must send data to Salesforce. What integration method would you choose?
Inbound, so Salesforce is the server. REST API for anything modern, SOAP API when the calling system needs a WSDL contract or is an older middleware product that speaks only SOAP.
The real decision is between the standard APIs and a custom Apex REST service. Use the standard REST API when the external system is doing plain record operations and can handle Salesforce object names and IDs. Write an Apex REST class when one inbound call has to do several things at once.
@RestResource(urlMapping='/orders/*')
global with sharing class OrderService {
@HttpPost
global static Map<String, String> createOrder(String externalId, String accountCode,
List<OrderLineDTO> lines) {
Account acct = [SELECT Id FROM Account WHERE Account_Code__c = :accountCode LIMIT 1];
Order__c ord = new Order__c(
External_Id__c = externalId,
Account__c = acct.Id
);
upsert ord External_Id__c;
List<Order_Line__c> newLines = new List<Order_Line__c>();
for (OrderLineDTO l : lines) {
newLines.add(new Order_Line__c(Order__c = ord.Id,
Product_Code__c = l.code,
Quantity__c = l.qty));
}
insert newLines;
return new Map<String, String>{ 'orderId' => ord.Id };
}
global class OrderLineDTO {
public String code;
public Decimal qty;
}
}
Two points worth making out loud. upsert ord External_Id__c on an External ID field makes the call idempotent, so a retry from the external system does not create a duplicate order. And for high volume loads, the answer changes to Bulk API 2.0 rather than either of these, because thousands of individual REST calls will exhaust the org's API request allocation.
The follow-up: "how does the external system authenticate?" Connected App with OAuth 2.0. For server to server with no user present, the JWT bearer flow or the client credentials flow, with a dedicated integration user whose permission set grants only the objects the integration touches.
You must call an external API from Salesforce. What should be used?
Apex HTTP callouts, with the endpoint reached through a Named Credential rather than a hard-coded URL and a stored key.
public class CreditScoreService {
public class CreditScoreException extends Exception {}
public static Decimal fetchScore(String customerRef) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Credit_Bureau/score?ref='
+ EncodingUtil.urlEncode(customerRef, 'UTF-8'));
req.setMethod('GET');
req.setTimeout(20000);
HttpResponse res = new Http().send(req);
if (res.getStatusCode() == 200) {
Map<String, Object> body =
(Map<String, Object>) JSON.deserializeUntyped(res.getBody());
return (Decimal) body.get('score');
}
throw new CreditScoreException('Credit bureau returned ' + res.getStatusCode());
}
}
Why the Named Credential over storing the URL and token in a custom setting. The platform handles the authentication header and token refresh, the secret never appears in Apex or in a deployment, and moving from sandbox to production becomes a config change instead of a code change. The callout:Credit_Bureau prefix is the whole point of the pattern.
Two constraints you should raise before the interviewer does. A callout cannot run after DML in the same transaction, because uncommitted work is pending, so anything that saves first has to hand the callout to a Queueable. And callouts do not execute in tests, so the test class must set a mock:
@IsTest
static void scoreIsParsed() {
Test.setMock(HttpCalloutMock.class, new CreditBureauMock());
Test.startTest();
Decimal score = CreditScoreService.fetchScore('CUST-1001');
Test.stopTest();
System.assertEquals(740, score);
}
The follow-up: "where have you actually used an integration?" Our trainer hears this exchange constantly. "Do you know integrations?" "Yes." "Where have you used it?" Silence. Specificity creates credibility, so name the system, the direction, the trigger point and the failure handling. One concrete example of a nightly invoice push to a billing system beats a correct definition of REST.
What the interviewer is actually checking in this section
- Whether you separate inbound from outbound before answering
- Whether you reach for a Named Credential without being prompted
- Whether you know callouts and DML cannot share a transaction, and what you do about it
- Whether you can name a real integration you built and go five levels deep on it
Deployment
How would you deploy code between environments?
The answer depends on team size and how often you release, and the interviewer wants to hear you pick rather than list.
| Method | Use when | The catch |
|---|---|---|
| Change Sets | Small org, occasional release, admin-heavy changes | Manual component picking, no version history, cannot delete components |
| Salesforce CLI with Git | Any team with more than one developer | Needs source control discipline and a branching model |
| Unlocked packages | Modular org, several teams owning separate features | Higher setup cost, needs clean dependency boundaries |
For a developer role, the expected answer is the CLI with Git, and the expected proof is that you can name the commands:
sf project deploy validate --source-dir force-app --test-level RunLocalTests --target-org prod
sf project deploy quick --job-id <validation-job-id> --target-org prod
Validate first, then quick deploy. The validation runs the tests, and if it passes within the last few days the quick deploy pushes the same validated set in minutes instead of rerunning the full test suite during a release window. That single detail separates someone who has released to production from someone who has read about it.
Know the gate too. A production deployment needs 75 percent Apex code coverage across the org, every trigger needs some coverage, and all tests must pass. Coverage is measured across the org, not per class, so a single badly tested class can still be blocked by the aggregate.
Why not Change Sets for a developer team. They cannot delete metadata, they carry no history of who changed what, and two developers working in parallel will overwrite each other with no record of it. Git gives you the diff, the reviewer and the rollback.
The follow-up: "your deployment failed on a test class that passes in sandbox." Usually missing test data, because production has validation rules, required fields or triggers the sandbox data never exercised. The fix is @IsTest(SeeAllData=false) with a TestDataFactory that builds its own records, rather than tests that lean on whatever happens to be in the org.
What the interviewer is actually checking in this section
- Whether you have deployed to production or only read about it
- Whether you know the coverage gate is org-wide rather than per class
- Whether you understand validate and quick deploy, not just deploy
- Whether your tests create their own data
Lightning Web Components
A component must receive data from a parent component. How is it done?
Decorate the child's property with @api. That makes it public, and the parent sets it as an HTML attribute in kebab case.
// invoiceRow.js
import { LightningElement, api } from 'lwc';
export default class InvoiceRow extends LightningElement {
@api invoice;
@api readOnly = false;
handleSelect() {
this.dispatchEvent(new CustomEvent('rowselect', {
detail: { invoiceId: this.invoice.Id }
}));
}
}
<template for:each={invoices} for:item="inv">
<c-invoice-row key={inv.Id} invoice={inv} read-only
onrowselect={handleRowSelect}></c-invoice-row>
</template>
The rule that catches people: the child must never mutate an @api property. Data flows down through @api and back up through a CustomEvent. If the child writes to this.invoice, the parent and child fall out of sync and the change is lost on the next re-render.
Why not have the child call Apex itself. Because then two components query the same record twice and one of them shows stale data. One owner of the data, passed down.
The follow-up: "now the child needs to tell the parent something." CustomEvent with a detail payload, handled by onrowselect on the parent. Event names are lowercase with no hyphens or camel case.
A component must reactively update UI when data changes. What is used?
Every field in an LWC class is already reactive. @track is only needed when you mutate the inside of an object or array instead of replacing it.
import { LightningElement, track } from 'lwc';
export default class FilterPanel extends LightningElement {
filters = { region: 'West', status: 'Open' };
@track deepFilters = { region: 'West' };
updateRegion(event) {
// No re-render: same object reference, LWC sees no change
// this.filters.region = event.target.value;
// Re-renders: new reference assigned to the field
this.filters = { ...this.filters, region: event.target.value };
// Also re-renders, because @track watches inside the object
this.deepFilters.region = event.target.value;
}
}
Why prefer the spread over @track. Reassigning a new object keeps the data immutable and makes the change obvious to anyone reading the code, and it avoids the class of bug where a tracked object is shared between components and mutates under both.
The follow-up: "you changed the array and the table did not refresh." That is exactly this. this.rows.push(newRow) will not re-render; this.rows = [...this.rows, newRow] will.
You need to fetch Salesforce data without writing Apex. What should you use?
Lightning Data Service, either through the base components or through the lightning/uiRecordApi wire adapters.
<lightning-record-form
record-id={recordId}
object-api-name="Account"
layout-type="Full"
mode="view">
</lightning-record-form>
Or with the adapter, when you need the values in JavaScript rather than a rendered form:
import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import PHONE_FIELD from '@salesforce/schema/Account.Phone';
const FIELDS = [NAME_FIELD, PHONE_FIELD];
export default class AccountSummary extends LightningElement {
@api recordId;
@wire(getRecord, { recordId: '$recordId', fields: FIELDS })
account;
get accountName() {
return getFieldValue(this.account.data, NAME_FIELD);
}
}
Why this over a small Apex controller that does the same query. LDS shares one client-side cache across every component on the page, so five components reading the same Account cause one server trip, and an update in one refreshes the others automatically. It also enforces field level security and sharing without you writing a line for it, and it needs no test class. Apex gives you none of that for free.
Where LDS stops: it works on a single record at a time. Multiple records, aggregates, or anything crossing objects needs Apex.
The follow-up: "the user has no access to the Phone field, what happens?" The wire returns the record without that field rather than throwing, so the component has to handle a missing value. LDS respects FLS; your component still has to be written for it.
Data must be retrieved from Apex in LWC. How is this done?
Two ways, and the choice is the answer.
public with sharing class CaseService {
@AuraEnabled(cacheable=true)
public static List<Case> getOpenCases(Id accountId) {
return [SELECT Id, CaseNumber, Subject, Status
FROM Case
WHERE AccountId = :accountId AND IsClosed = false
WITH SECURITY_ENFORCED
ORDER BY CreatedDate DESC
LIMIT 200];
}
@AuraEnabled
public static void closeCase(Id caseId) {
update new Case(Id = caseId, Status = 'Closed');
}
}
import { LightningElement, api, wire } from 'lwc';
import { refreshApex } from '@salesforce/apex';
import getOpenCases from '@salesforce/apex/CaseService.getOpenCases';
import closeCase from '@salesforce/apex/CaseService.closeCase';
export default class CaseList extends LightningElement {
@api recordId;
@wire(getOpenCases, { accountId: '$recordId' })
cases;
async handleClose(event) {
await closeCase({ caseId: event.target.dataset.id });
await refreshApex(this.cases);
}
}
Use @wire for read-only data that should load automatically and refresh when a reactive parameter changes. The $recordId syntax is what makes it reactive. The method must be cacheable=true, which also means it cannot perform DML.
Use an imperative call when the work happens on a user action, when it writes data, or when you need to control the timing. closeCase cannot be wired at all, because it updates a record.
The catch that shows experience: a wired result is cached, so after an imperative DML the wire will still show the old list until you call refreshApex on the wired property. Assigning the wire to a property rather than a function is what gives you the object refreshApex needs.
The follow-up: "your wire is not firing." Check that the Apex method is cacheable=true, that the parameter uses the $ prefix, and that the parameter is not undefined, because a wire with an undefined reactive parameter simply does not call the server.
You must communicate between unrelated components. What approach works?
Lightning Message Service. Use it only when the components have no parent and child relationship, for example two components sitting in different regions of the same record page, or a component in an Aura tab talking to an LWC in a utility bar.
// publisher
import { LightningElement, wire } from 'lwc';
import { publish, MessageContext } from 'lightning/messageService';
import ORDER_SELECTED from '@salesforce/messageChannel/OrderSelected__c';
export default class OrderList extends LightningElement {
@wire(MessageContext) messageContext;
handleSelect(event) {
publish(this.messageContext, ORDER_SELECTED, { orderId: event.detail.orderId });
}
}
// subscriber
import { LightningElement, wire } from 'lwc';
import { subscribe, unsubscribe, MessageContext } from 'lightning/messageService';
import ORDER_SELECTED from '@salesforce/messageChannel/OrderSelected__c';
export default class OrderDetail extends LightningElement {
subscription = null;
orderId;
@wire(MessageContext) messageContext;
connectedCallback() {
this.subscription = subscribe(this.messageContext, ORDER_SELECTED,
(message) => { this.orderId = message.orderId; });
}
disconnectedCallback() {
unsubscribe(this.subscription);
this.subscription = null;
}
}
The message channel is a metadata file, OrderSelected__c.messageChannel-meta.xml, and it deploys like any other component.
Why not a pubsub module. That was the community workaround before LMS existed, it only works within a single Lightning page and it cannot cross the boundary between LWC, Aura and Visualforce. LMS is the platform answer and works across all three.
Why not LMS for parent and child. Because it is global, so every subscriber on the page receives the message. For a direct relationship, @api down and CustomEvent up stays clearer and cheaper.
The follow-up: "what happens when the subscribing component is destroyed?" Unsubscribe in disconnectedCallback, otherwise the subscription leaks and the callback fires against a component that no longer exists.
Component should load data when page opens. What lifecycle hook is used?
connectedCallback, which fires when the component is inserted into the DOM.
connectedCallback() {
this.isLoading = true;
getDashboardData({ accountId: this.recordId })
.then((data) => { this.rows = data; })
.catch((error) => { this.error = error; })
.finally(() => { this.isLoading = false; });
}
Why not the constructor. The constructor runs before the component has its public properties set and before it is attached to the DOM, so this.recordId is still undefined there and you cannot touch child elements.
Why not renderedCallback. It fires after every single render, so an Apex call placed there will run repeatedly and can loop forever when the response changes a rendered property.
The detail that earns marks: connectedCallback can fire more than once if the component is removed and reinserted, for example when a tab is switched away and back. If the call is expensive, guard it with a flag.
The follow-up: "why not just use a wire?" A wire is the better choice here when the data is a simple cacheable read. connectedCallback is for a one-time imperative load, for initialising state, or for subscribing to a message channel.
You must display a spinner during data loading. What should you implement?
Conditional rendering driven by a boolean, with lightning-spinner as the visual.
<template>
<lightning-card title="Open Cases">
<template lwc:if={isLoading}>
<lightning-spinner alternative-text="Loading cases" size="small"></lightning-spinner>
</template>
<template lwc:else>
<lightning-datatable key-field="Id" data={rows} columns={columns}>
</lightning-datatable>
</template>
</lightning-card>
</template>
isLoading = false;
loadRows() {
this.isLoading = true;
getRows()
.then((data) => { this.rows = data; })
.catch((error) => { this.error = error; })
.finally(() => { this.isLoading = false; });
}
The finally block is the part interviewers look for. Set the flag false in finally, not in then, otherwise a failed call leaves the spinner running forever and the user stares at it. Older code uses if:true and if:false instead of lwc:if and lwc:else, and both still work, but the newer directives are what current components use.
Why a boolean and not CSS visibility. Conditional rendering removes the element from the DOM instead of hiding it, so the spinner is genuinely gone and not sitting invisible over your table catching clicks.
The follow-up: "how do you show the spinner only over part of the page?" lightning-spinner positions itself against the nearest ancestor with relative positioning, so scope it by wrapping the target region in a container with slds-is-relative.
Users must upload files from an LWC component. What base component helps?
lightning-file-upload.
<lightning-file-upload
label="Attach KYC documents"
name="kycUploader"
accept={acceptedFormats}
record-id={recordId}
multiple
onuploadfinished={handleUploadFinished}>
</lightning-file-upload>
acceptedFormats = ['.pdf', '.png', '.jpg'];
handleUploadFinished(event) {
const files = event.detail.files; // each item has documentId and name
this.uploadedIds = files.map((f) => f.documentId);
}
The record-id attribute is the important one. It links the uploaded file to that record automatically by creating a ContentDocumentLink, which is the work you would otherwise have to write in Apex.
Why not build a custom uploader with an input of type file and a base64 Apex insert. Because you then own the chunking for large files, the heap limit, the ContentVersion insert and the link creation, and you gain nothing. The base component is also accessible and matches the Lightning styling by default.
The constraint to raise: lightning-file-upload is not supported for guest users on a public Experience Cloud page without additional configuration, so a public upload form is the one case where you build something custom.
The follow-up: "the file uploaded but nobody can see it." Check sharing on the ContentDocument and whether the record link was created, then check whether the file was uploaded against a record the viewer can access.
Navigation to record page from LWC is required. What module is used?
NavigationMixin from lightning/navigation.
import { LightningElement } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
export default class CaseCreator extends NavigationMixin(LightningElement) {
navigateToCase(caseId) {
this[NavigationMixin.Navigate]({
type: 'standard__recordPage',
attributes: {
recordId: caseId,
objectApiName: 'Case',
actionName: 'view'
}
});
}
}
Note the class declaration. NavigationMixin(LightningElement) wraps the base class, and the method is called with bracket notation because NavigationMixin.Navigate is a symbol, not a plain string. Getting that syntax right on the spot is a small signal that you have written this rather than read it.
Why not window.location.href with a record URL. Because a hard-coded URL breaks in the Salesforce mobile app, in an Experience Cloud site with a different path, and in a console tab where it would reload the whole application instead of opening a subtab. NavigationMixin resolves the correct destination for whatever container the component is running in.
Use NavigationMixin.GenerateUrl instead when you need a real href on a link so the user can middle-click or copy it.
The follow-up: "navigate to a new record page with fields prefilled." Same pattern with actionName: 'new' and a state object carrying defaultFieldValues, encoded with encodeDefaultFieldValues from the same module.
A component should work on record pages automatically. What must be configured?
The metadata file. A component is invisible in the Lightning App Builder until isExposed is true and the record page target is declared.
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>61.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__RecordPage</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordPage">
<objects>
<object>Case</object>
</objects>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
With lightning__RecordPage as a target, the platform injects the current record into a public property:
@api recordId;
@api objectApiName;
Both must be @api for the platform to set them, and they are not available in the constructor.
Why the targetConfigs block matters. Without the objects restriction, the component appears on every record page including ones where its Apex will fail, and an admin will place it somewhere it was never designed for. Restricting it is design, not decoration.
The follow-up: "make a property configurable by the admin." Add a property element inside targetConfig with a name, label, type and default, and expose the matching @api field in the class. That is how one component serves three pages without a code change.
What the interviewer is actually checking in this section
- Whether you know which direction data flows and what a child is not allowed to do
- Whether you can pick between wire and imperative for a stated reason
- Whether you have hit the stale cache problem and know
refreshApex - Whether you configure the metadata file deliberately rather than copying it
- Whether your error paths are written, not just your happy path
Preparing for the developer round
The pattern across all nineteen scenarios is the same. The interviewer names a situation, you name the tool, and then you defend it against the option you did not pick. Memorised definitions collapse the moment the question becomes "give me a real project example", because a definition has no decision inside it. Answer in the shape of Situation, Approach, Reason, Result, and keep the reason short and concrete.
Build two or three of these yourself in a Developer Edition org before the interview. Write the batch class and run it on 5,000 records so you have seen the chunking. Break the wire deliberately and fix it with refreshApex. Deploy something with the CLI and watch a test fail in the validate step. An hour of that is worth more than a week of reading, because it gives you the specific detail that an interviewer cannot rehearse against.
If you want structured practice with a trainer reviewing your code and your answers, our Salesforce training in Pune with placement support covers Apex, integration and LWC through build exercises and mock scenario rounds of exactly this type.