Prepare for LWC interviews with practical, scenario-based questions that test both component concepts and problem-solving skills.
Question: You have a parent component that displays a list of accounts. When a user clicks an account, a child component should display its details. If the user edits the details in the child, the parent list should update. How would you implement this?
Answer: Use @api property for parent-to-child and event dispatch for child-to-parent:
// Parent Component
import { LightningElement, track, wire } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
export default class AccountList extends LightningElement {
@track selectedAccount;
@track accounts;
@wire(getAccounts)
wiredAccounts({ error, data }) {
if (data) {
this.accounts = data;
}
}
handleSelectAccount(event) {
const accountId = event.currentTarget.dataset.id;
this.selectedAccount = this.accounts.find(acc => acc.Id === accountId);
}
handleAccountUpdate(event) {
const updatedAccount = event.detail;
const index = this.accounts.findIndex(acc => acc.Id === updatedAccount.Id);
if (index > -1) {
this.accounts[index] = { ...updatedAccount };
this.accounts = [...this.accounts]; // Trigger reactivity
}
this.selectedAccount = null;
}
}
// Child Component
import { LightningElement, api } from 'lwc';
import updateAccount from '@salesforce/apex/AccountController.updateAccount';
export default class AccountDetails extends LightningElement {
@api account;
@api
accountName;
handleSave() {
updateAccount({ accountId: this.account.Id, accountName: this.accountName })
.then(() => {
this.dispatchEvent(new CustomEvent('accountupdated', {
detail: { ...this.account, Name: this.accountName }
}));
});
}
}
@api to receive data from parentdispatchEvent with CustomEvent to send data to parentQuestion: Your component needs to show different UI elements based on the user's role. Admins see all data, managers see their team's data, and regular users see only their own data. How do you implement this?
Answer: Use sharing rules and conditional rendering:
import { LightningElement, track, wire } from 'lwc';
import { isGuest } from 'lightning/platformShowToastEvent';
import getUserInfo from '@salesforce/apex/UserController.getUserInfo';
import getRecordsForUser from '@salesforce/apex/RecordController.getRecordsForUser';
export default class RoleBasedView extends LightningElement {
@track userRole;
@track records = [];
@track isAdmin = false;
@track isManager = false;
@wire(getUserInfo)
wiredUserInfo({ data, error }) {
if (data) {
this.userRole = data.UserRole.Name;
this.isAdmin = this.userRole.includes('Admin');
this.isManager = this.userRole.includes('Manager');
}
}
@wire(getRecordsForUser, { userRole: '$userRole' })
wiredRecords({ data, error }) {
if (data) {
this.records = data;
}
}
get hasAdminAccess() {
return this.isAdmin;
}
get hasManagerAccess() {
return this.isManager;
}
}
<template>
<lightning-card title="Records">
<!-- Admin view -->
<template if:true={hasAdminAccess}>
<lightning-datatable
key-field="Id"
data={records}
columns={allColumns}
></lightning-datatable>
</template>
<!-- Manager view -->
<template if:true={hasManagerAccess}>
<lightning-datatable
key-field="Id"
data={records}
columns={teamColumns}
></lightning-datatable>
</template>
<!-- Regular user view -->
<template if:false={hasAdminAccess}>
<template if:false={hasManagerAccess}>
<lightning-card title="My Records">
<lightning-datatable
key-field="Id"
data={records}
columns={userColumns}
></lightning-datatable>
</lightning-card>
</template>
</template>
</lightning-card>
</template>
with sharing keywordif:true and if:false for role-based renderingQuestion: You need to build a form that validates email, phone, and required fields before submission. If validation fails, show inline error messages. How do you implement this?
Answer: Use validation handlers and showToastEvent:
import { LightningElement, track } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import saveContact from '@salesforce/apex/ContactController.saveContact';
export default class ContactForm extends LightningElement {
@track contact = {
firstName: '',
lastName: '',
email: '',
phone: ''
};
@track errors = {};
@track isLoading = false;
handleInputChange(event) {
const field = event.target.name;
this.contact[field] = event.target.value;
this.errors[field] = this.validateField(field, this.contact[field]);
}
validateField(field, value) {
const errors = {};
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const phoneRegex = /^\d{10}$/;
switch (field) {
case 'firstName':
case 'lastName':
if (!value || value.trim() === '') {
return field + ' is required';
}
break;
case 'email':
if (!value || !emailRegex.test(value)) {
return 'Enter a valid email address';
}
break;
case 'phone':
if (value && !phoneRegex.test(value.replace(/\D/g, ''))) {
return 'Phone must be 10 digits';
}
break;
}
return '';
}
handleSubmit() {
// Validate all fields
let hasErrors = false;
for (const field in this.contact) {
const error = this.validateField(field, this.contact[field]);
if (error) {
this.errors[field] = error;
hasErrors = true;
}
}
if (hasErrors) {
this.showToast('Validation Error', 'Please fix all errors', 'error');
return;
}
this.isLoading = true;
saveContact({ contact: this.contact })
.then(() => {
this.showToast('Success', 'Contact saved', 'success');
this.resetForm();
})
.catch(error => {
this.showToast('Error', error.body.message, 'error');
})
.finally(() => {
this.isLoading = false;
});
}
resetForm() {
this.contact = {
firstName: '',
lastName: '',
email: '',
phone: ''
};
this.errors = {};
}
showToast(title, message, variant) {
this.dispatchEvent(
new ShowToastEvent({
title,
message,
variant,
mode: 'dismissable'
})
);
}
}
<template>
<lightning-card title="Contact Form">
<div class="slds-p-around_medium">
<lightning-input
type="text"
name="firstName"
label="First Name"
value={contact.firstName}
onchange={handleInputChange}
message-when-bad-input={errors.firstName}
></lightning-input>
<lightning-input
type="text"
name="lastName"
label="Last Name"
value={contact.lastName}
onchange={handleInputChange}
message-when-bad-input={errors.lastName}
></lightning-input>
<lightning-input
type="email"
name="email"
label="Email"
value={contact.email}
onchange={handleInputChange}
message-when-bad-input={errors.email}
></lightning-input>
<lightning-input
type="tel"
name="phone"
label="Phone"
value={contact.phone}
onchange={handleInputChange}
message-when-bad-input={errors.phone}
></lightning-input>
<lightning-button
variant="brand"
label={isLoading ? 'Saving...' : 'Submit'}
onclick={handleSubmit}
disabled={isLoading}
></lightning-button>
</div>
</lightning-card>
</template>
message-when-bad-input on inputs to show inline errorsShowToastEvent to display toast notificationsQuestion: You have a search box that should fetch accounts based on user input. Should you use @wire or imperative Apex? Why?
Answer: Use imperative for user-triggered searches:
// ❌ WASTEFUL: @wire with parameter
@wire(searchAccounts, { searchTerm: '$searchTerm' })
wiredAccounts;
// ✅ BETTER: Imperative call on user input
import { LightningElement, track } from 'lwc';
import searchAccounts from '@salesforce/apex/AccountController.searchAccounts';
export default class AccountSearch extends LightningElement {
@track searchTerm = '';
@track results = [];
@track isLoading = false;
handleSearch(event) {
this.searchTerm = event.target.value;
if (this.searchTerm.length < 2) {
this.results = [];
return;
}
this.isLoading = true;
searchAccounts({ searchTerm: this.searchTerm })
.then(data => {
this.results = data;
})
.catch(error => {
console.error('Search failed:', error);
})
.finally(() => {
this.isLoading = false;
});
}
}
@wire for data that changes reactively and loads on component initimperative for user-triggered actions (search, filter, pagination)Question: Your component displays a list of 1000+ records with pagination. The page loads slowly. How do you optimize?
Answer: Use pagination with lazy loading:
import { LightningElement, track } from 'lwc';
import getRecords from '@salesforce/apex/RecordController.getRecords';
export default class LazyLoadList extends LightningElement {
@track records = [];
@track pageNumber = 1;
@track pageSize = 20;
@track totalRecords = 0;
@track isLoading = false;
connectedCallback() {
this.loadRecords();
}
loadRecords() {
this.isLoading = true;
getRecords({
pageNumber: this.pageNumber,
pageSize: this.pageSize
})
.then(data => {
this.records = data.records;
this.totalRecords = data.totalCount;
})
.finally(() => {
this.isLoading = false;
});
}
handleNextPage() {
this.pageNumber++;
this.loadRecords();
}
handlePreviousPage() {
if (this.pageNumber > 1) {
this.pageNumber--;
this.loadRecords();
}
}
get hasNextPage() {
return (this.pageNumber * this.pageSize) < this.totalRecords;
}
get hasPreviousPage() {
return this.pageNumber > 1;
}
}
Question: You need to show data from Apex on load and refresh automatically when the record changes. Do you use @wire or imperative call, and why?
Answer: Use @wire for reactive auto-refresh, and imperative for user-triggered actions:
import { LightningElement, api, wire, track } from 'lwc';
import getAccountDetails from '@salesforce/apex/AccountController.getAccountDetails';
import updateAccount from '@salesforce/apex/AccountController.updateAccount';
export default class AccountViewer extends LightningElement {
@api recordId;
@track accountData;
@track isEditing = false;
// ✅ Use @wire for reactive, auto-refreshing data
@wire(getAccountDetails, { recordId: '$recordId' })
wiredAccount({ error, data }) {
if (data) {
this.accountData = data;
} else if (error) {
console.error('Error loading account:', error);
}
}
// ❌ Don't use @wire for user-triggered actions
// @wire(updateAccount, { data: '$editedData' })
// wiredUpdate;
// ✅ Instead, use imperative for manual actions
handleEdit() {
this.isEditing = true;
}
handleSave() {
updateAccount({ accountData: this.accountData })
.then(() => {
// After save, @wire automatically refreshes the data
// due to recordId change or use refreshApex
this.isEditing = false;
})
.catch(error => {
console.error('Save failed:', error);
});
}
}
@wire – Auto-refreshes when dependencies change; great for load-on-init dataImperative – User controls when data fetches; better for search, filters, paginationQuestion: Parent component needs to pass data to child and get response back. How do you handle communication both ways?
Answer: Use @api properties and custom events:
// Parent Component
import { LightningElement, track } from 'lwc';
export default class ParentAccount extends LightningElement {
@track selectedAccount = null;
@track accounts = [
{ Id: '1', Name: 'Acme Corp' },
{ Id: '2', Name: 'Tech Inc' }
];
handleSelectAccount(event) {
const accountId = event.currentTarget.dataset.id;
this.selectedAccount = this.accounts.find(acc => acc.Id === accountId);
}
handleChildSave(event) {
const updatedAccount = event.detail;
const index = this.accounts.findIndex(acc => acc.Id === updatedAccount.Id);
if (index > -1) {
this.accounts[index] = updatedAccount;
}
}
handleChildCancel() {
this.selectedAccount = null;
}
}
<!-- Parent Template -->
<template>
<lightning-card title="Accounts">
<div class="slds-p-around_medium">
<template for:each={accounts} for:item="acc">
<lightning-button
key={acc.Id}
label={acc.Name}
onclick={handleSelectAccount}
data-id={acc.Id}
></lightning-button>
</template>
</div>
</lightning-card>
<template if:true={selectedAccount}>
<c-account-editor
account={selectedAccount}
onsave={handleChildSave}
oncancel={handleChildCancel}
></c-account-editor>
</template>
</template>
// Child Component
import { LightningElement, api } from 'lwc';
import updateAccount from '@salesforce/apex/AccountController.updateAccount';
export default class AccountEditor extends LightningElement {
@api account;
accountName;
connectedCallback() {
this.accountName = this.account.Name;
}
handleInputChange(event) {
this.accountName = event.target.value;
}
handleSave() {
updateAccount({
accountId: this.account.Id,
accountName: this.accountName
})
.then(() => {
const updatedAccount = { ...this.account, Name: this.accountName };
this.dispatchEvent(new CustomEvent('save', {
detail: updatedAccount
}));
});
}
handleCancel() {
this.dispatchEvent(new CustomEvent('cancel'));
}
}
@api – Parent passes data (one-way down)dispatchEvent(CustomEvent) – Child sends data back to parentonsave={handleChildSave}Question: A component is slow because of repeated server calls. How do you optimize performance?
Answer: Use caching and memoization:
import { LightningElement, track } from 'lwc';
import getRecords from '@salesforce/apex/RecordController.getRecords';
export default class OptimizedRecordList extends LightningElement {
@track records = [];
@track isLoading = false;
recordCache = new Map(); // Cache records by filter
handleFilterChange(event) {
const filter = event.target.value;
// Check cache first
if (this.recordCache.has(filter)) {
this.records = this.recordCache.get(filter);
return;
}
// Fetch only if not in cache
this.isLoading = true;
getRecords({ filter })
.then(data => {
this.records = data;
this.recordCache.set(filter, data); // Store in cache
})
.finally(() => {
this.isLoading = false;
});
}
// Add debounce for search to prevent multiple calls
handleSearch(event) {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
this.handleFilterChange(event);
}, 300);
}
}
@wire(cacheable=true) in Apex for built-in cachingQuestion: You need to show a spinner while loading and handle API errors gracefully. How would you design it?
Answer: Use loading state and error messages:
import { LightningElement, track } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
export default class AccountListWithSpinner extends LightningElement {
@track accounts = [];
@track isLoading = false;
@track error = '';
connectedCallback() {
this.loadAccounts();
}
loadAccounts() {
this.isLoading = true;
this.error = '';
getAccounts()
.then(data => {
this.accounts = data;
this.error = '';
})
.catch(error => {
this.error = this.getErrorMessage(error);
this.showErrorToast(this.error);
})
.finally(() => {
this.isLoading = false;
});
}
getErrorMessage(error) {
if (error.body && error.body.message) {
return error.body.message;
}
return 'An unexpected error occurred';
}
showErrorToast(message) {
this.dispatchEvent(new ShowToastEvent({
title: 'Error',
message,
variant: 'error'
}));
}
handleRetry() {
this.loadAccounts();
}
}
<!-- Template with Spinner and Error Handling -->
<template>
<lightning-card title="Accounts">
<!-- Spinner while loading -->
<template if:true={isLoading}>
<div class="slds-p-around_medium">
<lightning-spinner alternative-text="Loading..."></lightning-spinner>
</div>
</template>
<!-- Error message with retry button -->
<template if:true={error}>
<div class="slds-p-around_medium slds-box slds-theme_alert-texture slds-theme_error">
<p>{error}</p>
<lightning-button
label="Retry"
variant="brand"
onclick={handleRetry}
></lightning-button>
</div>
</template>
<!-- Data table when loaded -->
<template if:true={accounts}>
<template if:false={isLoading}>
<lightning-datatable
key-field="Id"
data={accounts}
columns={columns}
></lightning-datatable>
</template>
</template>
</lightning-card>
</template>
lightning-spinner during fetchfinally() to always reset loading stateQuestion: Data is not updating in UI after Apex call. What could be wrong and how do you fix reactivity issues?
Answer: LWC uses shallow equality checks. Create new references for objects and arrays:
import { LightningElement, track } from 'lwc';
import updateAccount from '@salesforce/apex/AccountController.updateAccount';
export default class ReactivityExample extends LightningElement {
@track account = { Id: '123', Name: 'Acme' };
@track accountList = [{ Id: '1', Name: 'Client A' }];
// ❌ WRONG: Direct mutation doesn't trigger reactivity
handleUpdateWrong() {
this.account.Name = 'Updated Name'; // No reactivity!
this.accountList[0].Name = 'New Name'; // No reactivity!
}
// ✅ CORRECT: Create new references
handleUpdateCorrect() {
// For objects: spread operator
this.account = { ...this.account, Name: 'Updated Name' };
// For arrays: create new array
this.accountList = [
{ ...this.accountList[0], Name: 'New Name' },
...this.accountList.slice(1)
];
}
// ✅ BETTER: After Apex call, refresh data
async handleSaveAndRefresh() {
try {
await updateAccount({ accountData: this.account });
// Refresh by creating new instance
this.account = { ...this.account };
} catch (error) {
console.error('Save failed:', error);
}
}
// ✅ FOR NESTED OBJECTS
handleUpdateNested() {
const updatedAccount = {
...this.account,
details: {
...this.account.details,
phone: '555-1234'
}
};
this.account = updatedAccount;
}
}
{ ...object, field: value }[...array] or array.slice()Question: You need to build a reusable component used across multiple pages. How do you design it?
Answer: Use @api properties to make components flexible:
// Reusable DataTable Component
import { LightningElement, api, track } from 'lwc';
export default class ReusableDataTable extends LightningElement {
@api title = 'Data';
@api columnConfig = []; // Accept column config from parent
@api recordData = []; // Accept data from parent
@api pageSize = 10;
@api allowEdit = false;
@api allowDelete = false;
@track currentPage = 1;
connectedCallback() {
// Validate inputs
if (!this.columnConfig || this.columnConfig.length === 0) {
console.warn('DataTable: columnConfig is required');
}
}
get paginatedData() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.recordData.slice(start, end);
}
handleEdit(event) {
const recordId = event.detail;
this.dispatchEvent(new CustomEvent('edit', { detail: recordId }));
}
handleDelete(event) {
const recordId = event.detail;
this.dispatchEvent(new CustomEvent('delete', { detail: recordId }));
}
}
<!-- Reusable DataTable Template -->
<template>
<lightning-card title={title}>
<lightning-datatable
key-field="Id"
data={paginatedData}
columns={columnConfig}
hide-checkbox-column
></lightning-datatable>
</lightning-card>
</template>
// Parent: Using Reusable Component
import { LightningElement, track } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
export default class AccountDashboard extends LightningElement {
@track accounts = [];
columnConfig = [
{ label: 'Name', fieldName: 'Name', type: 'text' },
{ label: 'Industry', fieldName: 'Industry', type: 'text' },
{ label: 'Phone', fieldName: 'Phone', type: 'phone' }
];
connectedCallback() {
getAccounts().then(data => {
this.accounts = data;
});
}
handleEdit(event) {
const recordId = event.detail;
console.log('Editing:', recordId);
}
}
<!-- Parent Template Using Reusable Component -->
<template>
<c-reusable-data-table
title="All Accounts"
record-data={accounts}
column-config={columnConfig}
page-size="20"
allow-edit="true"
onedit={handleEdit}
></c-reusable-data-table>
</template>
@api properties to accept configuration from parent@api properties@api properties with JSDoc commentsQuestion: A list renders incorrectly after update. How do you use key in template for rerendering?
Answer: Use key attribute in for:each loops:
import { LightningElement, track } from 'lwc';
export default class TodoList extends LightningElement {
@track todos = [
{ id: '1', title: 'Task 1', completed: false },
{ id: '2', title: 'Task 2', completed: false }
];
handleAddTodo() {
const newTodo = {
id: Date.now().toString(),
title: 'New Task',
completed: false
};
this.todos = [...this.todos, newTodo];
}
handleToggleTodo(event) {
const todoId = event.currentTarget.dataset.id;
const index = this.todos.findIndex(t => t.id === todoId);
if (index > -1) {
// Create new array to trigger reactivity
this.todos = [
...this.todos.slice(0, index),
{ ...this.todos[index], completed: !this.todos[index].completed },
...this.todos.slice(index + 1)
];
}
}
handleDeleteTodo(event) {
const todoId = event.currentTarget.dataset.id;
this.todos = this.todos.filter(t => t.id !== todoId);
}
}
<!-- Template with Proper Key -->
<template>
<lightning-card title="Todos">
<!-- ❌ DON'T: Use index as key -->
<!-- <template for:each={todos} for:item="todo" for:index="index">
<li key={index}>...</li>
</template> -->
<!-- ✅ DO: Use unique identifier -->
<ul>
<template for:each={todos} for:item="todo">
<li key={todo.id} data-id={todo.id}>
<span class={completedClass}>{todo.title}</span>
<lightning-button
variant="bare"
icon-name="utility:check"
onclick={handleToggleTodo}
data-id={todo.id}
></lightning-button>
<lightning-button
variant="bare"
icon-name="utility:delete"
onclick={handleDeleteTodo}
data-id={todo.id}
></lightning-button>
</li>
</template>
</ul>
</lightning-card>
</template>
key attribute in for:eachQuestion: You must restrict UI based on user permissions. How do you handle security in LWC?
Answer: Check permissions before rendering sensitive UI:
import { LightningElement, track, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import Id from '@salesforce/user/Id';
import getUserPermissions from '@salesforce/apex/UserController.getUserPermissions';
const FIELDS = ['Account.Id', 'Account.Name'];
export default class PermissionBasedView extends LightningElement {
userId = Id;
@track account;
@track userPermissions = {};
@wire(getRecord, { recordId: '$recordId', fields: FIELDS })
wiredRecord({ data, error }) {
if (data) {
this.account = data;
}
}
@wire(getUserPermissions)
wiredPermissions({ data, error }) {
if (data) {
this.userPermissions = {
canRead: data.canReadAccounts,
canEdit: data.canEditAccounts,
canDelete: data.canDeleteAccounts,
isAdmin: data.isAdmin
};
}
}
get canEditAccount() {
return this.userPermissions.canEdit;
}
get canDeleteAccount() {
return this.userPermissions.canDelete;
}
handleEdit() {
if (!this.canEditAccount) {
alert('You do not have permission to edit this record');
return;
}
// Proceed with edit
}
}
<!-- Template with Permission Checks -->
<template>
<lightning-card title="Account Details">
<template if:true={account}>
<p>{account.fields.Name.value}</p>
</template>
<!-- Only show edit button if user has permission -->
<template if:true={canEditAccount}>
<lightning-button
label="Edit"
variant="brand"
onclick={handleEdit}
></lightning-button>
</template>
<!-- Only show delete button if user is admin -->
<template if:true={userPermissions.isAdmin}>
<lightning-button
label="Delete"
variant="destructive"
onclick={handleDelete}
></lightning-button>
</template>
</lightning-card>
</template>
with sharinglightning-record-edit-form which auto-enforces FLSQuestion: Need to upload a file and process it in Apex. What approach will you take?
Answer: Use lightning-file-upload and handle the file in Apex:
import { LightningElement, track } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import uploadFile from '@salesforce/apex/FileController.uploadFile';
export default class FileUploadExample extends LightningElement {
@track uploadedFiles = [];
handleFileUpload(event) {
const files = event.detail.files;
if (files.length === 0) return;
const file = files[0];
const reader = new FileReader();
reader.onload = () => {
const fileContent = reader.result;
const base64 = this.getBase64(fileContent);
// Call Apex to process file
uploadFile({
fileName: file.name,
fileContent: base64,
fileType: file.type
})
.then(result => {
this.uploadedFiles = [
...this.uploadedFiles,
{ name: file.name, recordId: result }
];
this.showToast('Success', 'File uploaded', 'success');
})
.catch(error => {
this.showToast('Error', error.body.message, 'error');
});
};
reader.readAsDataURL(file);
}
getBase64(fileContent) {
return fileContent.split(',')[1]; // Remove data:image/png;base64, prefix
}
showToast(title, message, variant) {
this.dispatchEvent(new ShowToastEvent({
title,
message,
variant
}));
}
}
<!-- File Upload Template -->
<template>
<lightning-card title="Upload File">
<lightning-file-upload
label="Select File"
accept=".csv,.pdf,.xlsx"
record-id="account-record"
onuploadfinished={handleFileUpload}
></lightning-file-upload>
<template if:true={uploadedFiles}>
<p>Uploaded Files:</p>
<ul>
<template for:each={uploadedFiles} for:item="file">
<li key={file.recordId}>{file.name}</li>
</template>
</ul>
</template>
</lightning-card>
</template>
// Apex: Process File
public with sharing class FileController {
@AuraEnabled
public static String uploadFile(String fileName, String fileContent, String fileType) {
ContentVersion cv = new ContentVersion();
cv.Title = fileName;
cv.VersionData = EncodingUtil.base64Decode(fileContent);
cv.IsMajorVersion = true;
insert cv;
ContentDocumentLink cdl = new ContentDocumentLink();
cdl.ContentDocumentId = [SELECT ContentDocumentId FROM ContentVersion WHERE Id = :cv.Id][0].ContentDocumentId;
cdl.LinkedEntityId = UserInfo.getUserId(); // Or your record Id
cdl.ShareType = 'V';
insert cdl;
return cv.Id;
}
}
lightning-file-upload component for UIQuestion: Multiple components need same data. Do you call Apex multiple times or use LDS / shared state?
Answer: Use Lightning Data Service or a shared state pattern:
// Approach 1: Use Lightning Data Service (LDS)
import { LightningElement, api, wire } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
const FIELDS = ['Account.Id', 'Account.Name', 'Account.Phone'];
export default class AccountViewerLDS extends LightningElement {
@api recordId;
// LDS automatically caches data and shares across components
@wire(getRecord, { recordId: '$recordId', fields: FIELDS })
account;
}
// Approach 2: Use Shared Data Service (Redux-like pattern)
// dataService.js
let sharedData = {};
export function setSharedData(key, data) {
sharedData[key] = data;
}
export function getSharedData(key) {
return sharedData[key];
}
// Component 1: Loads data
import { setSharedData } from 'c/dataService';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
export default class AccountLoader extends LightningElement {
connectedCallback() {
getAccounts().then(data => {
setSharedData('accounts', data); // Share data
});
}
}
// Component 2: Uses shared data
import { getSharedData } from 'c/dataService';
export default class AccountConsumer extends LightningElement {
get accounts() {
return getSharedData('accounts');
}
}
// Approach 3: Parent provides data to children via @api
<template>
<c-account-list accounts={sharedAccounts}></c-account-list>
<c-account-summary accounts={sharedAccounts}></c-account-summary>
</template>
LDS (getRecord) – Best for single record, automatic cachingShared Service Module – Good for app-wide state (data only, not UI)Parent Component – Simple, works well for related componentsQuestion: A record should be private, but certain users get access dynamically via logic. How do you implement Apex sharing?
Answer: Create sharing records programmatically:
// Apex: Share Opportunity with specific user if condition is met
public class OpportunityShareService {
public static void shareWithApprover(List<Opportunity> opportunities) {
List<OpportunityShare> shares = new List<OpportunityShare>();
for (Opportunity opp : opportunities) {
// Only share if amount > $100k and approver exists
if (opp.Amount > 100000 && opp.ApproverIds__c != null) {
List<Id> approverIds = opp.ApproverIds__c.split(';');
for (Id approverId : approverIds) {
OpportunityShare share = new OpportunityShare();
share.OpportunityId = opp.Id;
share.UserOrGroupId = approverId;
share.OpportunityAccessLevel = 'Edit'; // Read or Edit
shares.add(share);
}
}
}
if (!shares.isEmpty()) {
insert shares;
}
}
// Remove access when condition no longer met
public static void removeApproverShare(List<Opportunity> opportunities) {
List<Id> oppIds = new List<Id>(new Map<Id, Opportunity>(opportunities).keySet());
List<OpportunityShare> sharesToDelete = [
SELECT Id
FROM OpportunityShare
WHERE OpportunityId IN :oppIds
AND RowCause = 'Manual'
];
if (!sharesToDelete.isEmpty()) {
delete sharesToDelete;
}
}
}
// Trigger
trigger OpportunitySharingTrigger on Opportunity (after insert, after update) {
if (Trigger.isInsert) {
OpportunityShareService.shareWithApprover(Trigger.new);
} else if (Trigger.isUpdate) {
OpportunityShareService.shareWithApprover(Trigger.new);
OpportunityShareService.removeApproverShare(Trigger.new);
}
}
Key Points:
AccessLevel can be 'Read' only or 'Edit'RowCause indicates why record is shared (Manual, Rule, etc.)with sharing keyword to enforce FLSQuestion: Difference between Profile vs Permission Set vs Sharing in real scenario?
Answer: Three different layers of access control:
| Layer | What It Controls | Scope | Example |
|---|---|---|---|
| Profile | Object access, field access, system permissions | User-wide | Sales Profile can't access Finance Object |
| Permission Set | Extends profile permissions (add, never remove) | User-assigned | Add "Manage Quotes" to Sales Rep temporarily |
| Sharing | Record-level access control | Per-record | Sales Rep A can see Opp X but not Opp Y |
Real Scenario:
Sales Profile (base permissions)
├─ Can access Account, Opportunity objects
├─ Cannot access Finance object
└─ Cannot see phone field
+ Permission Set (temporary)
├─ Add "Approve Quotes" ability
└─ Keep all other restrictions
+ Sharing (record control)
├─ Sally can see Account 123
├─ Sally cannot see Account 456
└─ Both accounts visible to Manager due to role hierarchy
Question: How do you handle field-level security vs object-level vs record-level access?
Answer: Three layers of restriction:
| Level | Controls | Set By | Example |
|---|---|---|---|
| Object-Level | Can user access object at all? | Profile / Permission Set | Sales user cannot access Finance object |
| Field-Level | Can user see/edit specific fields? | Profile / Permission Set | Sales user can see Amount but not Cost__c |
| Record-Level | Can user access specific records? | OWD / Sharing Rules / Apex Sharing | Sales user can see their own Oppty, not competitor's |
Access Check Order (top to bottom):
1. Can user access the Object? (OWD)
2. Can user access the Field? (FLS in Profile/PermSet)
3. Can user see this Record? (Sharing Rules / Apex Sharing)
4. Can user Edit this Field? (FLS for Edit + Record Access)
If ANY layer says NO, access is DENIED.
Example:
Sales user:
✅ Can access Opportunity object (OWD = Private)
✅ Can see Amount field (FLS allows read)
❌ Cannot see this specific Opp (not owner, not shared, manager)
❌ Result: User cannot see record
---
Manager:
✅ Can access Opportunity object
✅ Can see Amount field
✅ Can see Opp because below Sales Rep in hierarchy
✅ Can edit Amount (FLS allows edit)
✅ Result: Manager can view and edit
Question: A user can see a record but not edit it. What could be the reasons?
Answer: Check in this order:
Question: When would you use Public Groups vs Roles?
Answer: Different purposes and behaviors:
| Aspect | Roles | Public Groups |
|---|---|---|
| Hierarchy | Hierarchical (tree structure) | Flat (just membership) |
| Inheritance | Parent sees child records | No inheritance |
| Use Case | Org structure, manager visibility | Cross-functional teams, skill-based groups |
| Example | CEO → Manager → Sales Rep | "Finance Approvers", "East Coast Team", "Data Analysts" |
| Sharing Rules | Can use "Based on Role" | Can't (must use "Based on Role" for hierarchy) |
Scenario: Share records to "Finance Team" (includes people from Sales, Ops, Legal)
Solution: Use Public Group "Finance Team" with members from different roles.
Sharing Rule: IF Approver_Group__c = 'Finance', share with PublicGroup.FinanceTeam
Question: A community user needs limited access to related records. How do you design external sharing model?
Answer: Use Account-based sharing:
// Community User sees:
// ✅ Their own User record
// ✅ Their Account (customer's account)
// ✅ Contacts related to that Account (via relationship)
// ✅ Cases they created or are related to Account
// ❌ Sensitive opportunity details
// Setup:
Setup → Sharing Settings:
- Account OWD: Controlled by Parent
- Contact OWD: Controlled by Parent (Account)
- Case OWD: Public Read/Write or Controlled by Parent
- Opportunity OWD: Private (not visible to community)
// Apex: Ensure portal user only sees their Account data
public with sharing class ContractController {
@AuraEnabled(cacheable=true)
public static List<Contract__c> getMyContracts() {
// with sharing automatically filters to user's accessible records
return [
SELECT Id, Name, Amount__c, Status__c
FROM Contract__c
WHERE Account__c IN (SELECT AccountId FROM User WHERE Id = :UserInfo.getUserId())
];
}
}
Question: What happens when OWD is Public Read/Write but you still want restrictions?
Answer: Use Anti-Sharing Rules or custom logic:
// If OWD = Public Read/Write, everyone sees all records.
// To restrict: Use anti-sharing or custom validation.
// Option 1: Validation Rule (prevents edit, not view)
IF(
AND(
$Profile.Name != 'System Administrator',
Sensitivity__c = 'Confidential'
),
TRUE
)
// Blocks non-admins from editing Confidential records (but they can still see)
// Option 2: Custom Logic in Apex (deny read/write)
public with sharing class AccountController {
@AuraEnabled
public static Account getAccount(Id accountId) {
Account acc = [SELECT Id, Name, SensitiveData__c FROM Account WHERE Id = :accountId];
// Deny access if sensitive and not admin
if (acc.Sensitivity__c = 'Confidential' && !isAdmin()) {
throw new AuraHandledException('Access Denied');
}
return acc;
}
}
// Option 3: Remove OWD = Public Read/Write, use Sharing Rules instead
// ✅ Better approach: Set OWD to Private/Read Only, grant access via Sharing Rules
Question: How do you troubleshoot "user can't see record" issue step by step?
Answer: Use this checklist:
STEP 1: Check Org-Wide Defaults (OWD)
├─ Go to Setup → Sharing Settings → [Object]
├─ Read: "The account level is set to Private"
├─ Q: Can user see ANY records of this type?
│ └─ If NO → Likely OWD or Field-Level Security issue
STEP 2: Check Record Sharing
├─ Open the record → sharing icon (bottom right)
├─ Q: Is user listed with 'Read' or 'Edit' access?
│ └─ If NO → Add via sharing rule or manual share
│ └─ If YES → Drill down to field level
STEP 3: Check Profile/Permission Set
├─ Setup → Users → User Details → [User]
├─ Check Profile → Object Permissions
├─ Q: Does profile have READ access to object?
│ └─ If NO → Add to profile or permission set
│ └─ If YES → Check field access next
STEP 4: Check Field-Level Security
├─ Profile → Field Permissions → [Object]
├─ Q: Is the FIELD (or key fields) readable?
│ └─ If NO → Enable read access on field
│ └─ If YES → Check record ownership/hierarchy
STEP 5: Check Role Hierarchy
├─ Is user owner of record? YES → can see
├─ Is user above owner in hierarchy? YES → can see
├─ Is record shared? Check sharing rules/manual share
├─ Is sharing rule matching? YES → can see
STEP 6: Check Apex Sharing
├─ Query ShareWith__c or custom sharing logic
├─ Is record dynamically shared? Check trigger logs
STEP 7: Last Resort – Check System
├─ Clear browser cache (Ctrl+Shift+Del)
├─ Login as different user and test
├─ Check org debug logs for AuraEnabled exceptions
└─ Run: SOSL search to see if record appears
Question: Sales reps should see only their records, managers should see team data. How do you design access?
Answer: Combine OWD + Role Hierarchy:
// Org Structure:
VP Sales
├─ Regional Manager - West
│ ├─ Sales Rep 1
│ └─ Sales Rep 2
└─ Regional Manager - East
└─ Sales Rep 3
// Configuration:
1. OWD for Opportunity = PRIVATE
2. Sales Rep owns their own Opportunities
3. Role Hierarchy grants manager visibility
// Results:
Sales Rep 1:
- Sees their own Oppty (as owner)
- Cannot see Oppty owned by Sales Rep 2
- Cannot see Regional Manager's Oppty
Regional Manager - West:
- Sees all Oppty owned by Sales Rep 1 & 2 (below in hierarchy)
- Sees their own Oppty (as owner)
- Cannot see Regional Manager - East's Oppty
VP Sales:
- Sees all Oppty (everyone below in hierarchy)
// Adding Apex Sharing for exceptions:
If an Oppty needs review by Finance team:
public class OpportunityShareService {
public static void shareWithFinanceForReview(List<Opportunity> opportunities) {
List<OpportunityShare> shares = new List<OpportunityShare>();
for (Opportunity opp : opportunities) {
if (opp.FinanceReview__c == true) {
OpportunityShare share = new OpportunityShare();
share.OpportunityId = opp.Id;
share.UserOrGroupId = PublicGroupId.Finance; // Public Group ID
share.OpportunityAccessLevel = 'Read';
shares.add(share);
}
}
insert shares;
}
}
Question: Finance needs read-only access to all Opportunities across regions. What approach will you use?
Answer: Use Sharing Rule with Public Group:
// Step 1: Create Public Group for Finance
Setup → Users → Public Groups → New
Name: Finance Team
Add members: All Finance users, Finance manager
// Step 2: Create Sharing Rule
Setup → Sharing Settings → Opportunity → New
Rule Name: Finance View All Oppty
Type: Opportunity
Share With: Public Group (Finance Team)
Access Level: Read
Criteria:
- All Opportunities (no filters)
OR
- RecordType = "Enterprise"
OR
- StageName IN ('Proposal', 'Negotiation')
// Result:
- Every Finance user can READ all Oppty
- Finance users cannot EDIT Oppty
- Finance users cannot delete Oppty
- Access applies globally across regions
// Apex: Verify access
public with sharing class FinanceReporting {
@AuraEnabled(cacheable=true)
public static List<Opportunity> getAllOpportunitiesForReporting() {
// with sharing respects OWD and sharing rules
return [
SELECT Id, Name, Amount, StageName, OwnerId, Account.Name
FROM Opportunity
ORDER BY Amount DESC
];
}
}
Question: You need to give temporary access of a record to a user. How would you handle it?
Answer: Use scheduled Apex or workflow to auto-revoke access:
import { LightningElement, api, track } from 'lwc';
import grantTemporaryAccess from '@salesforce/apex/SharingService.grantTemporaryAccess';
export default class TemporaryAccessComponent extends LightningElement {
@api recordId;
@track accessDays = 7;
@track selectedUser;
async handleGrantAccess() {
try {
await grantTemporaryAccess({
recordId: this.recordId,
userId: this.selectedUser,
days: this.accessDays
});
alert('Access granted until ' + new Date(Date.now() + this.accessDays * 24 * 60 * 60 * 1000));
} catch (error) {
alert('Error: ' + error.body.message);
}
}
}
// Apex: Grant and auto-revoke access
public class SharingService {
@AuraEnabled
public static void grantTemporaryAccess(Id recordId, Id userId, Integer days) {
// Insert share record
OpportunityShare share = new OpportunityShare();
share.OpportunityId = recordId;
share.UserOrGroupId = userId;
share.OpportunityAccessLevel = 'Edit';
share.RowCause = 'Manual';
insert share;
// Create scheduled action to remove access
scheduleAccessRevoke(recordId, userId, days);
}
private static void scheduleAccessRevoke(Id recordId, Id userId, Integer days) {
RevokeAccessJob job = new RevokeAccessJob(recordId, userId);
String cronExpression = getCronExpression(days);
System.schedule('Revoke_' + recordId, cronExpression, job);
}
}
// Scheduled Job: Revoke access
public class RevokeAccessJob implements Schedulable {
private Id recordId;
private Id userId;
public RevokeAccessJob(Id recordId, Id userId) {
this.recordId = recordId;
this.userId = userId;
}
public void execute(SchedulableContext ctx) {
List<OpportunityShare> shares = [
SELECT Id
FROM OpportunityShare
WHERE OpportunityId = :recordId
AND UserOrGroupId = :userId
AND RowCause = 'Manual'
];
if (!shares.isEmpty()) {
delete shares;
}
}
}
Question: A record should be shared dynamically based on a field value. What solution will you use?
Answer: Use Apex trigger with field-based logic:
// Scenario: Share Opportunity with Account team when Status = "Reviewing"
trigger OpportunityFieldBasedSharing on Opportunity (after insert, after update) {
if (Trigger.isAfter) {
OpportunityFieldSharingService.shareBasedOnStatus(Trigger.new, Trigger.oldMap);
}
}
public class OpportunityFieldSharingService {
public static void shareBasedOnStatus(List<Opportunity> newOppty, Map<Id, Opportunity> oldMap) {
List<OpportunityShare> sharesToAdd = new List<OpportunityShare>();
List<Id> opptyToUnshare = new List<Id>();
for (Opportunity opp : newOppty) {
Opportunity oldOpp = oldMap.get(opp.Id);
String newStatus = opp.StageName;
String oldStatus = oldOpp.StageName;
// If changed to "Reviewing", share with Account team
if (newStatus == 'Reviewing' && oldStatus != 'Reviewing') {
// Get account team members
List<AccountTeamMember> teamMembers = [
SELECT UserId FROM AccountTeamMember
WHERE AccountId = :opp.AccountId
];
for (AccountTeamMember member : teamMembers) {
OpportunityShare share = new OpportunityShare();
share.OpportunityId = opp.Id;
share.UserOrGroupId = member.UserId;
share.OpportunityAccessLevel = 'Read';
sharesToAdd.add(share);
}
}
// If changed from "Reviewing" to other status, revoke access
if (oldStatus == 'Reviewing' && newStatus != 'Reviewing') {
opptyToUnshare.add(opp.Id);
}
}
// Insert new shares
if (!sharesToAdd.isEmpty()) {
insert sharesToAdd;
}
// Remove old shares
if (!opptyToUnshare.isEmpty()) {
List<OpportunityShare> sharesToDelete = [
SELECT Id FROM OpportunityShare
WHERE OpportunityId IN :opptyToUnshare
AND RowCause = 'Manual'
];
delete sharesToDelete;
}
}
}
Question: Users can see records but cannot edit certain fields. How do you control this?
Answer: Combine Field-Level Security + Validation Rules + Lightning Components:
// Approach 1: Field-Level Security (simplest)
Setup → Profiles → [Profile] → Field Permissions
- Amount field: Read enabled, Edit disabled
- Result: User sees Amount but input field is read-only
// Approach 2: Validation Rule (more control)
IF(
AND(
NOT($Profile.Name = 'System Administrator'),
CHANGED(Renewal_Date__c),
Renewal_Date__c > TODAY()
),
TRUE
)
Error Message: "Non-admins cannot edit future renewal dates"
// Approach 3: Lightning Component (most control)
import { LightningElement, api, track, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import getFieldEditability from '@salesforce/apex/FieldSecurityService.getFieldEditability';
export default class RestrictedFieldForm extends LightningElement {
@api recordId;
@track fieldPermissions = {};
@wire(getFieldEditability, { recordId: '$recordId' })
wiredPermissions({ data, error }) {
if (data) {
this.fieldPermissions = data;
}
}
get canEditAmount() {
return this.fieldPermissions.Amount?.editable;
}
handleSave() {
// Validate field edits before sending to Apex
if (!this.canEditAmount) {
alert('You cannot edit Amount field');
return;
}
// Proceed with save
}
}
// Apex: Check FLS before allowing edit
public with sharing class FieldSecurityService {
@AuraEnabled
public static Map<String, FieldPermission> getFieldEditability(Id recordId) {
Map<String, FieldPermission> fieldAccess = new Map<String, FieldPermission>();
Schema.DescribeSobjectResult describe = recordId.getSObjectType().getDescribe();
Map<String, Schema.SObjectField> fields = describe.fields.getMap();
for (String fieldName : new List<String>{ 'Amount', 'RenewalDate', 'Description' }) {
Schema.SObjectField field = fields.get(fieldName.toLowerCase());
Schema.DescribeFieldResult fieldDesc = field.getDescribe();
FieldPermission perm = new FieldPermission();
perm.readable = fieldDesc.isAccessible();
perm.editable = fieldDesc.isUpdateable();
fieldAccess.put(fieldName, perm);
}
return fieldAccess;
}
public class FieldPermission {
@AuraEnabled public Boolean readable { get; set; }
@AuraEnabled public Boolean editable { get; set; }
}
}
Question: External (community) users should only see their own related records. How do you design it?
Answer: Use Account-based sharing + Apex with sharing:
// Community user setup:
1. Portal User linked to Account (customer account)
2. OWD settings for Customer Portal:
- Account: Controlled by Parent (Account)
- Contact: Controlled by Parent (Account)
- Case: Public Read/Write (so they can see all cases)
- Opportunity: Private
3. Portal User sees:
✅ Their Account (the company they work for)
✅ Contacts on their Account
✅ Cases on their Account
❌ Other companies' data
// Apex: Enforce access in code
public with sharing class CommunityDataService {
@AuraEnabled(cacheable=true)
public static List<Contact> getAccountContacts() {
// with sharing automatically filters to user's account
User portalUser = [SELECT AccountId FROM User WHERE Id = :UserInfo.getUserId()];
return [
SELECT Id, Name, Email, Phone
FROM Contact
WHERE AccountId = :portalUser.AccountId
];
}
@AuraEnabled(cacheable=true)
public static List<Case> getMyAccountCases() {
User portalUser = [SELECT AccountId FROM User WHERE Id = :UserInfo.getUserId()];
return [
SELECT Id, CaseNumber, Subject, Status
FROM Case
WHERE AccountId = :portalUser.AccountId
];
}
}
// LWC: Display community data
import { LightningElement, track, wire } from 'lwc';
import getAccountContacts from '@salesforce/apex/CommunityDataService.getAccountContacts';
export default class CommunityView extends LightningElement {
@track contacts = [];
@wire(getAccountContacts)
wiredContacts({ data, error }) {
if (data) {
this.contacts = data;
}
}
}
Question: A user cannot see a record they should have access to. How do you troubleshoot step by step?
Answer: Use this systematic troubleshooting approach:
// Step 1: Verify user can see records at all (OWD baseline)
SELECT Id, Name FROM Opportunity LIMIT 10
// If query returns records → OWD allows some visibility
// If query returns 0 records → Check OWD (likely Private) or FLS
// Step 2: Check if user owns the record
SELECT OwnerId FROM Opportunity WHERE Id = :recordId
IF (OwnerId = UserInfo.getUserId()) {
CONCLUSION: User is owner, should have full access
CHECK: Profile edit permissions
}
// Step 3: Check role hierarchy
User u = [SELECT UserRoleId FROM User WHERE Id = :UserInfo.getUserId()];
UserRole r = [SELECT Id, ParentRoleId FROM UserRole WHERE Id = :u.UserRoleId];
IF (record.OwnerId's role is below current user's role hierarchy) {
CONCLUSION: User should see record via role hierarchy
CHECK: OWD is not blocking it (set to Private at minimum)
}
// Step 4: Check if record is shared explicitly
List<OpportunityShare> shares = [
SELECT Id, AccessLevel
FROM OpportunityShare
WHERE OpportunityId = :recordId
AND (UserOrGroupId = :userId OR UserOrGroupId IN (public groups user belongs to))
];
IF (shares.size() > 0) {
CONCLUSION: Record is explicitly shared
IF (AccessLevel = 'Read') → User can READ but not EDIT
}
// Step 5: Check Field-Level Security
Schema.DescribeFieldResult fieldDesc = Opportunity.Amount.getDescribe();
IF (!fieldDesc.isAccessible()) {
PROBLEM: User cannot read Amount field
FIX: Enable field in Profile/PermSet
} ELSE IF (!fieldDesc.isUpdateable()) {
PROBLEM: User can read but cannot edit Amount
FIX: User has Read access to record but not edit to this field
}
// Step 6: Check for API filters
// Some record types may be hidden by validation rules or SOSL
SELECT COUNT() FROM Opportunity WHERE Id = :recordId
IF (count = 0) {
PROBLEM: Record doesn't match user's query filters
CHECK: Are you using with sharing clause?
FIX: Ensure Apex uses with sharing for correct filtering
}
// Step 7: Verify record actually exists and isn't deleted
SELECT Id FROM Opportunity WHERE Id = :recordId ALL ROWS
IF (IsDeleted = true) {
CONCLUSION: Record is in recycle bin
FIX: Restore from recycle bin if needed
}
// Debug Query to get complete picture:
List<Opportunity> opps = [
SELECT
Id, Name, OwnerId, Owner.Name, Amount,
(SELECT Id, UserOrGroupId, OpportunityAccessLevel FROM OpportunityShares)
FROM Opportunity
WHERE Id = :recordId
];
Question: You need to restrict access even when OWD is Public Read/Write. What can you do?
Answer: OWD = Public Read/Write gives global access; use alternatives:
// PROBLEM: If OWD = Public Read/Write, EVERYONE can see ALL records
// SOLUTION OPTIONS:
// Option 1: Change OWD to Private or Read Only ✅ BEST
Setup → Sharing Settings → Opportunity
Change: Public Read/Write → Private
Then grant access via Sharing Rules/Apex Sharing
// Option 2: Use Validation Rules (prevents EDIT, not VIEW)
IF(
AND(
Confidentiality__c = 'Sensitive',
NOT($Profile.Name = 'System Administrator')
),
TRUE
)
Error: "Only admins can edit sensitive records"
// Note: Users can still SEE sensitive records, just can't edit
// Option 3: Use Apex with custom restrictions
public with sharing class OpportunitySecurity {
@AuraEnabled(cacheable=true)
public static List<Opportunity> getAccessibleOpportunities() {
// Apex with sharing respects OWD and Sharing Rules
List<Opportunity> opps = [
SELECT Id, Name, Amount
FROM Opportunity
];
// Add custom filter to deny based on field
List<Opportunity> filtered = new List<Opportunity>();
for (Opportunity opp : opps) {
if (opp.Confidentiality__c != 'TopSecret' || isCurrentUserAdmin()) {
filtered.add(opp);
}
}
return filtered;
}
}
// Option 4: Use record-level access control in LWC
// Even if record is visible via SOQL, hide in LWC based on field
import { LightningElement, track } from 'lwc';
export default class OpportunitiesList extends LightningElement {
@track visibleOpportunities = [];
handleDataLoaded(event) {
const allOpps = event.detail;
// Filter in UI if Confidentiality = TopSecret
this.visibleOpportunities = allOpps.filter(
opp => opp.Confidentiality__c !== 'TopSecret'
);
}
}
// RECOMMENDATION:
// Change OWD from Public Read/Write to Private
// Grant access explicitly via Sharing Rules or Apex
// This is the proper security model
Question: Multiple teams need access to the same set of records. Do you use Roles or Public Groups? Why?
Answer: Depends on team structure:
| Scenario | Use Roles | Use Public Groups |
|---|---|---|
| Sales team hierarchy (VP → Manager → Rep) | ✅ YES – hierarchy matters | ❌ No – not hierarchical |
| Cross-functional teams (Sales + Finance + Legal) | ❌ No – not structural | ✅ YES – skill-based, not org hierarchy |
| Finance needs all Oppty (across all regions) | ❌ No – they're not in sales org | ✅ YES – create "Finance Approvers" group |
| Multiple account teams for same account | ❌ No – not structural | ✅ YES – "West Region Team", "East Region Team" |
Real Example:
Scenario: Three teams need access to "Enterprise Opportunities"
- Sales team (hierarchical)
- Finance review team (cross-functional)
- Legal review team (ad-hoc)
Solution:
1. Create Sharing Rule for "Sales" based on Roles
Share with role "Sales" + subordinates
2. Create Public Group "Finance Approvers"
Add: Finance VP, Finance Manager, Auditor role
3. Create Public Group "Legal Team"
Add: Legal counsel, in-house attorney, paralegal
4. Create two Sharing Rules:
Rule 1: IF Requires_Finance_Review__c = true
Share with Public Group "Finance Approvers"
Rule 2: IF Requires_Legal_Review__c = true
Share with Public Group "Legal Team"
Result:
- Sales sees records in their role
- Finance sees only records needing finance review
- Legal sees only records needing legal review
Question: You need to share records from a custom object based on complex logic. Would you use Sharing Rules or Apex Sharing? Why?
Answer: Choose based on complexity:
| Scenario | Sharing Rules | Apex Sharing |
|---|---|---|
| Share if field = "X" | ✅ Simple and fast | ❌ Overkill |
| Share if field = "X" AND role = "Y" | ✅ Good (use role-based rule) | ❌ Sharing rules handle this |
| Share based on lookup field value (Account region) | ❌ Can't do cross-object logic | ✅ YES – query parent record, evaluate |
| Share if related Contact matches criteria | ❌ No cross-object logic | ✅ YES – query related records |
| Complex: Share to users on same team as record owner | ❌ Can't query team membership | ✅ YES – use Apex for complex joins |
Example: Complex Sharing Rule
Requirement: Share Project record with all users in same Department as owner
// Sharing Rules can't do this (no cross-object logic)
// Solution: Use Apex
trigger ProjectSharingTrigger on Project__c (after insert, after update) {
ProjectSharingService.shareWithDepartment(Trigger.new);
}
public class ProjectSharingService {
public static void shareWithDepartment(List<Project__c> projects) {
// Get owner departments
Set<Id> ownerIds = new Set<Id>();
for (Project__c proj : projects) {
ownerIds.add(proj.OwnerId);
}
// Query owners and their department
Map<Id, String> ownerDepartments = new Map<Id, String>();
for (User u : [SELECT Id, Department FROM User WHERE Id IN :ownerIds]) {
ownerDepartments.put(u.Id, u.Department);
}
// Find all users in same departments
Set<String> departments = new Set<String>(ownerDepartments.values());
List<User> departmentUsers = [
SELECT Id FROM User
WHERE Department IN :departments
];
// Create shares
List<Project__Share> shares = new List<Project__Share>();
for (Project__c proj : projects) {
String ownerDept = ownerDepartments.get(proj.OwnerId);
for (User u : departmentUsers) {
if (u.Department == ownerDept) {
Project__Share share = new Project__Share();
share.ParentId = proj.Id;
share.UserOrGroupId = u.Id;
share.AccessLevel = 'Edit';
share.RowCause = 'Manual';
shares.add(share);
}
}
}
insert shares;
}
}
// DECISION RULE:
// If: Simple field-based rule → Use Sharing Rules ✅
// If: Complex, cross-object, queryable logic → Use Apex ✅
// If: Mix of both → Sharing Rules + Apex (don't duplicate logic)