This page is for admins and developers facing a Salesforce round, from a first job to a senior platform role. Most Salesforce interviews start with the data model and relationships, move to profiles, permission sets, roles and sharing, then test flows, Apex triggers, governor limits and SOQL. Developer rounds add async Apex, Lightning Web Components, testing and deployment, and senior rounds end with a requirement to design end to end, a production story and a judgement call. Each question shows what the interviewer is really checking, the shape of a strong answer and a short answer you can say out loud. Try the code in a free Developer Edition org, then change the stories to your own.
Search all questions by round, difficulty and level, or save the ones you want to practise.
Master-detail: parent required, child has no owner of its own and inherits sharing, deleting the parent deletes children, roll-up summaries on the parent.
Lookup: optional by default, child keeps its own owner and sharing, parent delete usually just clears the field or can be blocked.
Choice: master-detail when the child means nothing alone; lookup when it has its own life and owner.
"Both link a child record to a parent, but master-detail is a much tighter bond. With master-detail the parent field is always required, the child has no owner of its own, so it inherits sharing and security from the parent, and deleting the parent deletes the children. I also get roll-up summary fields on the parent, like a count of line items or a total amount. A lookup is looser. The field can be optional, each child keeps its own owner and sharing, and when the parent is deleted the lookup is usually just cleared, or I can block the delete. So I pick master-detail when the child makes no sense on its own, like invoice lines under an invoice. I pick a lookup when the child has its own life and owner, like a support case that points at a product."
Saying the only difference is that one field is required, and missing ownership, inherited sharing and cascade delete.
Junction object: a custom object with two master-detail fields, one to each side.
Primary parent: the first master-detail created drives the page look and the junction record's owner.
Access and roll-ups: users need access to both parents by default; roll-ups work on both sides.
"Salesforce has no direct many-to-many field, so I'd build a junction object. Here that's Job Application, with one master-detail to Candidate and another to Job Opening. Each application record is one pairing, and it's also the natural home for data that belongs to the pairing, like the date applied or the interview stage. The first master-detail I create becomes the primary one, which drives how the record page looks and where the junction record takes its owner from. By default a user needs at least read access to both the candidate and the job opening to see an application, which is usually what the business wants. I get roll-up summaries on both sides, so each job can show its number of applicants, and I'd add the related list to both parents so recruiters can work from either end."
Suggesting a multi-select picklist or a text field of Ids to link the records.
SOQL: one object plus its relationships, exact filters, returns a list of records.
SOSL: text search across many objects through the search index, returns a list of lists.
Catches: SOSL may miss very fresh records and returns nothing in tests unless results are fixed.
"SOQL is for when I know which object I'm querying and exactly what I'm filtering on, like all open opportunities for one account. It returns a list of records of one object, and I can reach into parent and child records through relationships. SOSL is a text search over the search index. I use it when I know a word but not where it lives, like a user typing Acme into a custom search box, and I want matching accounts, contacts and leads in one call. It returns a list of lists, one per object, and it's much better than SOQL with leading wildcards for searching text across many fields. The trade-offs: SOSL results come from the index, so a record saved a moment ago might not show yet, and in tests SOSL returns nothing unless I set fixed search results. For exact filters and business logic, SOQL is the tool."
// SOQL: one object, exact filters
List<Opportunity> opps = [SELECT Id, Name FROM Opportunity
WHERE AccountId = :accId AND IsClosed = false];
// SOSL: one search term, several objects
List<List<SObject>> hits = [FIND 'Acme*' IN NAME FIELDS
RETURNING Account(Id, Name), Contact(Id, Name), Lead(Id, Name)];
Saying SOSL is just SOQL for several objects, or using it for exact business filters.
Child to parent: dot notation on the relationship name, like Account.Owner.Name.
Parent to child: a subquery in the SELECT list using the plural child relationship name.
Custom: relationship names end in __r; child rows count toward the row limit.
"SOQL doesn't do joins the way SQL does; it follows relationships. Going up from child to parent, I use dot notation on the relationship name, so from Contact I can read Account.Name and even Account.Owner.Name, a few levels up. Going down from parent to children, I write a subquery inside the SELECT list using the child relationship name, which is usually the plural, so Opportunities on Account. For custom relationships the name ends in __r instead of __c, like Invoices__r. In the first query I get accounts in one industry with only their open opportunities nested inside each account, and in Apex I loop over acc.Opportunities. The second reads the owner's name straight through the account. Child rows count toward the query row limit, so on big accounts I'd keep the subquery filtered or move the work to a batch."
SELECT Id, Name,
(SELECT Id, Name, StageName, Amount
FROM Opportunities
WHERE IsClosed = false)
FROM Account
WHERE Industry = 'Technology'
SELECT Id, LastName, Account.Name, Account.Owner.Name
FROM Contact
WHERE AccountId != null
Querying accounts, then querying opportunities for each account inside a loop.
Profile: exactly one per user; baseline object, field, app and system permissions, plus login hours and IP ranges.
Permission set: additive access on top, many per user, can be grouped; the modern way to grant most access.
Role: no object permissions at all; places the user in the hierarchy that decides record visibility.
"Profiles and permission sets control what a user can do: which objects they can create, read, edit or delete, which fields they see, which apps they get, plus system permissions. Every user has exactly one profile, and permission sets add access on top. A user can have many, and I can bundle them into permission set groups. The approach I follow is a lean profile with minimal access and everything else granted through permission sets, because it's far easier to maintain as people change jobs. A role is different. It doesn't grant object access at all. It places the user in the role hierarchy, which decides which records they can see, because people above you generally see what you own. So to let someone see more records, I'd look at their role and the sharing setup, not the profile. To let them edit a new field, I'd use a permission set."
Saying a role controls which objects or fields a user can edit.
Object access first: without object permission from a profile or permission set, nothing else matters.
Baseline: org-wide defaults set the most restrictive level for records you don't own.
Opening up: role hierarchy, sharing rules by owner or criteria, teams, manual and Apex managed sharing.
Narrowing: sharing rules never remove access; tighten the default or use restriction rules where supported.
"I think of it as layers. First the user needs object permission from their profile or a permission set, or nothing else matters. Then org-wide defaults set the baseline for records they don't own: private, public read only, public read/write, or controlled by parent for detail records. I set the default to the most restrictive level anyone needs, then open access up. The role hierarchy opens it to managers above the owner. Sharing rules open it to groups or roles, either by who owns the record or by criteria on the record, like all accounts in one region. After that there's manual sharing, account and opportunity teams, and Apex managed sharing for complex cases. So a sharing rule can only grant access, never remove it. To hide records from someone, I tighten the default and share back, or use restriction rules on the objects that support them."
Setting org-wide defaults to public read/write and trying to hide records with sharing rules.
System mode: Apex ignores object and field permissions by default.
Sharing keywords: with sharing respects record access, without sharing ignores it, inherited sharing follows the caller.
Field security: WITH USER_MODE, AccessLevel.USER_MODE or Security.stripInaccessible, since sharing keywords don't check it.
"By default Apex runs in system mode, so it ignores the running user's object and field permissions, and depending on the sharing keyword it may ignore record sharing too. With sharing makes the class respect the user's record access, so queries only return records they can see. Without sharing ignores it, which I use deliberately, for example a service that must count every case in the org. Inherited sharing runs in whatever mode the caller is in, and falls back to with sharing when the class is the entry point, which makes it a safe default for utility classes. The part people miss is that with sharing is only about records. It doesn't check object or field permissions. For that I run queries WITH USER_MODE, pass AccessLevel.USER_MODE to Database methods, or use Security.stripInaccessible. For anything a Lightning component calls, I declare sharing explicitly and enforce user mode."
public with sharing class CaseService {
@AuraEnabled(cacheable=true)
public static List<Case> openCases(Id accountId) {
return [SELECT Id, Subject, Status FROM Case
WHERE AccountId = :accountId AND IsClosed = false
WITH USER_MODE];
}
}
Believing with sharing also hides fields the user has no permission to read.
Say no, kindly: View All Data opens every record and rarely gets removed.
Diagnose: object permission, then the Sharing Hierarchy button on the record, owner and role.
Fix narrowly: sharing rule, account team or role fix; a manual share if it's urgent today.
"I wouldn't grant View All Data. It opens every record in the org, including things she shouldn't see, and it's the kind of permission that never gets taken back. But I also wouldn't leave her stuck, so I'd diagnose it quickly. First, does she have read access on Account at all through her profile or permission sets? Then I'd open the record and use the Sharing Hierarchy button to see who has access and why. Usually the account is owned by someone outside her branch of the role hierarchy and no sharing rule covers her team. The right fix matches the business rule: a criteria-based sharing rule for her team, an account team membership, or correcting her role. If it's urgent, a manual share on that one account solves today while the proper rule is agreed. Then I'd tell the manager what we changed and why."
Granting View All Data or Modify All Data to make the ticket go away.
Logic: the formula describes the bad case; true blocks the save and shows the message.
Scope: leave closed deals alone; put the error on the field with a message that says how to fix it.
Bypass: a custom permission in the formula, granted by permission set to the load user only.
"A validation rule formula describes the bad case: if it evaluates to true, the save is blocked and the user sees my message. So here I'd write AND, NOT IsClosed, and CloseDate less than TODAY. Closed deals are left alone, since their close date is naturally in the past. I'd show the error on the Close Date field so it appears right where the user fixes it, with a message that says what to do, like move the close date to today or later, or close the deal. Validation rules run on every save, whether it comes from the UI, the API, Apex or a flow, so a big data load would trip it too. For that I add a custom permission, say Bypass Validation, into the formula with NOT, and give it through a permission set to the integration user only for the load."
AND(
NOT(IsClosed),
CloseDate < TODAY(),
NOT($Permission.Bypass_Validation)
)
Writing the formula for the good case, so every valid record is blocked, or deactivating the rule for every data load.
Flow first: field updates, related records, notifications and simple decisions that admins can maintain.
Apex when: heavy logic, large collections, complex error handling, callouts with retries, big data volumes.
Context: follow the object's existing pattern; mixed automation on one object is hard to debug.
"My default is to start with a flow, because admins can read and maintain it and it's quick to change. A record-triggered flow handles field updates, creating a related record, sending notifications and simple decisions really well. I move to Apex when the logic gets heavy: lots of nested conditions, working across big collections with maps, complex error handling, callouts that need retries, or very large data volumes where I need tight control over queries and CPU time. Apex also wins when the logic needs proper unit tests and has to be reused from several places. The other thing I weigh is what already exists. If an object has a mature trigger framework, I'll usually add to it rather than scatter logic across a trigger and three flows, because mixed automation on one object is where most debugging pain comes from. For anything new, I use Flow, not workflow rules or Process Builder."
Saying code is always better, or that flows have no limits, since flows share the same governor limits as Apex.
Before-save: changes the triggering record with no extra DML; fast, but can only update that record.
After-save: has the Id and system fields; creates or updates related records, sends emails.
Triggers: before triggers set fields on Trigger.new; in after triggers Trigger.new is read-only.
"A before-save flow, which Flow Builder calls fast field updates, runs before the record is written, so any change I make to the triggering record just goes in with the save. There's no extra DML, which makes it very fast, but it can only update that same record. An after-save flow runs once the record is saved, so it has the record Id and system fields, and it can create or update related records or send emails. It costs more, because each change to another record is more DML. Triggers follow the same idea. In a before trigger I set fields directly on Trigger.new with no DML statement. In an after trigger the record has an Id, which I need to create child records, but Trigger.new is read-only, so changing the same record means a separate update, which also fires the trigger again. So same-record fields go before, related records go after."
Doing same-record field updates in an after trigger with an extra update call.
Before the save: system validation, before-save flows, before triggers, then custom validation and duplicate rules.
After the save: after triggers, then assignment, auto-response and workflow rules, then after-save flows.
Parent and commit: roll-ups update the parent, sharing is recalculated, then commit.
Post-commit: emails and async jobs only start after the commit.
"When a user saves, Salesforce loads the record and runs system validation, like required fields and field formats. Then before-save flows run, then before triggers. After that it runs system validation again along with my custom validation rules, then duplicate rules. Then the record is saved to the database, but not committed. After triggers run next, then assignment, auto-response and any old workflow rules, and then after-save flows. If there are roll-up summary fields, the parent record is updated, which can fire the parent's own automation. Criteria-based sharing is evaluated, and only then does everything commit. Post-commit work comes last: emails are sent and async jobs like queueables start. So custom validation rules see values changed by before triggers, and a failure anywhere before commit rolls the whole thing back. Also, with several triggers on one object, their order isn't guaranteed, which is why I keep one trigger per object."
Putting custom validation rules before the before triggers, or thinking emails go out before the transaction commits.
Ask first: what the flow does decides the advice.
Same-record fields: a before-save flow is cheap and runs before the before triggers.
Related or dependent logic: after-save flows run after the after triggers and can re-fire them; consider the Apex handler.
Guardrails: document it, share the bypass switch, test with a bulk load.
"I'd start by asking what the flow needs to do, because that changes the advice. If it's a simple field update on the same record, a before-save flow is cheap and runs before the before triggers, so the Apex will see the new value. If it updates related records or depends on values the trigger sets, I'd be more careful. After-save flows run after the after triggers, and a flow that updates the opportunity again re-fires the whole trigger framework, which costs CPU time and can cause loops. In that case I'd suggest adding the logic to the Apex handler, or exposing it as an invocable action the team owns. Whatever we pick, I'd make sure it's on the object's automation map, respects the same bypass switch, and is tested in a sandbox with a bulk load, not just one record in the UI. I'd frame it as protecting the admin's change, not blocking it."
Either banning flows on the object outright or letting the flow go in with no thought about order and recursion.
Why: shared servers; each transaction gets hard limits so one tenant can't hurt the rest.
Common ones: 100 SOQL queries and 150 DML statements per sync transaction, 50,000 query rows, 10,000 DML rows, CPU time.
Cause and check: queries or DML inside loops; watch with the Limits class and debug logs.
"Salesforce is multi-tenant: many customers share the same servers, so each transaction gets hard limits to stop one bad piece of code from hurting everyone else. If you cross one, the platform throws an exception you can't catch and the whole transaction rolls back. The ones I watch most are 100 SOQL queries per synchronous transaction, 150 DML statements, 50,000 rows returned by queries, 10,000 records changed by DML, and CPU time, which is 10 seconds for synchronous code. Async Apex gets higher limits for some of these, like 200 queries and 60 seconds of CPU. In practice almost every limit error comes from the same mistake: a query or DML statement inside a loop, which works for one record in the UI and fails the moment someone loads 200. I check usage with the Limits class and the limit summary in the debug log, and I design for bulk from the start."
Treating limits as something to work around with try-catch, or not knowing the query and DML limits at all.
Collect: loop Trigger.new once and gather the account Ids that matter.
Query once: one SOQL query into a map keyed by Id.
Apply: set fields on Trigger.new in a before trigger, so no DML is needed.
"Bulkifying means the trigger behaves the same for one record or two hundred, with a fixed number of queries. First I loop over Trigger.new once and collect the account Ids I care about, skipping contacts with no account or that already have a country. Then I run a single query for all those accounts and put the results in a map keyed by Id. Then I loop again and copy the billing country onto each contact from the map. Because it's a before insert trigger, I'm changing the records that are about to be saved, so there's no update statement at all. The result is one query no matter how many contacts come in. In a real org I'd move this into a handler class and test it by inserting 200 contacts, but the pattern stays the same: collect, query once, map, apply."
trigger ContactTrigger on Contact (before insert) {
Set<Id> accountIds = new Set<Id>();
for (Contact c : Trigger.new) {
if (c.AccountId != null && c.MailingCountry == null) {
accountIds.add(c.AccountId);
}
}
if (!accountIds.isEmpty()) {
Map<Id, Account> accounts = new Map<Id, Account>(
[SELECT Id, BillingCountry FROM Account WHERE Id IN :accountIds]
);
for (Contact c : Trigger.new) {
Account a = accounts.get(c.AccountId);
if (a != null && c.MailingCountry == null) {
c.MailingCountry = a.BillingCountry;
}
}
}
}
Querying the account inside the loop, or calling update on Trigger.new in a before trigger.
One trigger per object: a logic-free trigger that calls a handler class per context.
Bypass: a custom setting or custom permission so data loads can skip automation safely.
Recursion: track processed record Ids in a static set and act only when relevant fields changed.
"I keep one trigger per object, and the trigger itself holds no logic. It hands off to a handler class, with a method for each context like before insert or after update. That gives me a predictable order, because Salesforce doesn't guarantee the order between two triggers on one object, and it keeps the logic testable and reusable. I also add a bypass switch, usually a custom setting or custom permission, so data loads can skip automation safely. For recursion, the classic case is an after update trigger that updates the same records, which fires the trigger again. The naive fix is a static Boolean flag, but that can skip records, because a large DML is processed in chunks of 200 within one transaction and the flag blocks the later chunks. So I track a static set of Ids already processed, and better still, only act when the fields I care about actually changed, by comparing with Trigger.oldMap."
Putting all logic in several triggers on the same object, or relying on a static Boolean that skips later chunks.
Cause: DML on setup objects like Group, GroupMember or PermissionSetAssignment and on data objects in one transaction.
Fix in code: move one side into a queueable or future method, passing Ids.
Fix in tests: wrap the setup DML in System.runAs.
"Salesforce separates setup objects, like User, Group, GroupMember, UserRole and PermissionSetAssignment, from regular data objects like Account or Case. Changing a setup object can change who sees what, so the platform won't let you do DML on both kinds in the same transaction. A common example is a trigger on Opportunity that updates the record and also adds the owner to a public group, or onboarding code that assigns a permission set and then updates the account. The fix is to split the work into separate transactions. I keep the data changes where they are and move the setup change into a queueable or a future method, passing Ids rather than records. In test classes the same error shows up when a test creates setup records and data together, and there I wrap the setup work in a System.runAs block, which gives it its own context."
Catching the exception and ignoring it, or not knowing which objects count as setup objects.
Future: simplest; primitive parameters only, no chaining, no job Id.
Queueable: complex types, a job Id to monitor, can chain the next job.
Batch: very large volumes in chunks, each chunk a fresh transaction.
Schedulable: runs on a cron schedule, often to start a batch.
"They all run later, in their own transaction with higher limits, but they fit different jobs. A future method is the simplest: a static void method marked @future, but it only accepts primitive parameters like Ids, I can't chain it, and I get no job Id to track. Queueable is what I reach for by default now. It takes complex types including sObjects, returns a job Id I can monitor, and a job can enqueue the next one, so I can chain steps. Batch Apex is for volume: it splits a huge set of records into chunks, and each chunk runs as its own transaction with fresh limits. Schedulable is about time: it runs a class on a cron schedule, usually just to kick off a batch every night. So small follow-up work after a save is queueable, huge data is batch, and a timetable is schedulable."
Using a future method per record inside a loop, or choosing batch for a single small follow-up task.
Start: a query locator defines the records, up to fifty million.
Execute: once per chunk, 200 by default, each a separate transaction with its own limits.
Finish and state: finish runs once; Database.Stateful keeps instance variables across chunks.
"Batch Apex has three methods. Start defines the records, usually with a query locator, which can cover up to fifty million rows. Execute runs once per chunk, 200 records by default, and each chunk is a separate transaction with its own limits, so one failing chunk doesn't roll back the others. Finish runs once at the end, which is where I'd send a summary or chain the next job. Here I'm closing cases that haven't changed in ninety days. I use Database.update with allOrNone set to false so one record failing a validation rule doesn't sink its whole chunk, and I implement Database.Stateful so the counter survives between chunks, because without it instance variables reset for every chunk. I'd launch it with Database.executeBatch and schedule it nightly with a small Schedulable class."
public class CloseStaleCasesBatch implements Database.Batchable<SObject>, Database.Stateful {
public Integer closedCount = 0;
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([
SELECT Id, Status FROM Case
WHERE IsClosed = false AND LastModifiedDate < LAST_N_DAYS:90
]);
}
public void execute(Database.BatchableContext bc, List<SObject> scope) {
List<Case> cases = (List<Case>) scope;
for (Case c : cases) {
c.Status = 'Closed';
}
for (Database.SaveResult r : Database.update(cases, false)) {
if (r.isSuccess()) closedCount++;
}
}
public void finish(Database.BatchableContext bc) {
System.debug('Closed ' + closedCount + ' cases');
}
}
// Database.executeBatch(new CloseStaleCasesBatch(), 200);
Expecting instance variables to keep their values between chunks without Database.Stateful.
Why: no synchronous callouts from triggers, and none after uncommitted DML in the same transaction.
How: collect Ids, enqueue one Queueable with Database.AllowsCallouts, or a future method with callout=true.
Robustness: Named Credential for the endpoint and auth, a status field, retries for failures.
"Salesforce doesn't allow a synchronous callout from a trigger, because the trigger runs inside the database transaction and holding it open while waiting on another system would lock records. More generally, you can't make a callout after uncommitted DML in the same transaction. So I move the callout out of the transaction. In the trigger I collect the Ids of the new accounts and enqueue one Queueable that implements Database.AllowsCallouts, or call a future method marked callout equals true. It's one job for the whole set of records, not one per record, because there's a limit on how many async jobs a transaction can start. The job queries the accounts fresh, sends them in one request if the API allows, uses a Named Credential so no endpoint or secret sits in code, and writes the result back to a status field, with a retry for failures."
Enqueuing one future call per record, or hard-coding the endpoint and password in Apex.
Standards: custom elements, shadow DOM and ES modules; mostly plain modern JavaScript.
Bundle: HTML template, a JavaScript class extending LightningElement, a meta XML file, optional CSS.
Decorators: @api public, @wire reactive data, @track only for deep mutation of objects or arrays.
"A Lightning Web Component is built on web standards: custom elements, shadow DOM and ES modules, so it's mostly plain modern JavaScript. The bundle is a folder with an HTML template, a JavaScript class that extends LightningElement, a meta XML file that says where it can be used, like record pages or app pages, and optionally a CSS file. For decorators, @api makes a property or method public, so a parent component or the Lightning App Builder can set it. recordId on a record page is the classic example. @wire connects a property or function to a data source, like an Apex method or a Lightning Data Service adapter, and re-runs when its reactive parameters change. @track is mostly not needed any more, since all fields are reactive. I only use it when I change a property inside an object or array and need the template to re-render."
Putting @track on every field, or not knowing what the meta XML file is for.
Wire: automatic and reactive; needs cacheable=true, so no DML; data is read-only and cached.
Imperative: on user action or when the method changes data; returns a promise.
Keep in sync: refreshApex on the wired result after a change.
"With @wire, the framework calls the method for me when the component loads and again whenever a reactive parameter changes, like recordId with a dollar prefix. The Apex method has to be marked cacheable=true, which also means it can't do DML, and the data I get back is read-only and may come from cache, so I call refreshApex when I know it's stale. That's ideal for simply showing data. I call Apex imperatively when I need control over timing: on a button click, after validating input, or whenever the method changes data, since that can't be cacheable. An imperative call returns a promise, so I await it and handle errors in a try-catch. Here the open cases are wired from the record Id, and closing a case is an imperative call from the button handler, followed by refreshApex so the list updates."
import { LightningElement, api, wire } from 'lwc';
import { refreshApex } from '@salesforce/apex';
import getOpenCases from '@salesforce/apex/CaseController.getOpenCases';
import closeCase from '@salesforce/apex/CaseController.closeCase';
export default class AccountCases extends LightningElement {
@api recordId;
error;
@wire(getOpenCases, { accountId: '$recordId' })
cases;
async handleClose(event) {
try {
await closeCase({ caseId: event.target.dataset.id });
await refreshApex(this.cases);
} catch (e) {
this.error = e.body ? e.body.message : e.message;
}
}
}
Trying to do DML from a cacheable method, or editing wired data in place.
Parent to child: public @api properties or methods.
Child to parent: dispatch a CustomEvent; the parent listens with an on handler in markup.
Unrelated: Lightning Message Service over a message channel, which also reaches Aura and Visualforce.
"It depends on how the components are related. Parent to child is the simplest: the child exposes a property or a method with @api, and the parent sets the property in its template or calls the method on the child element. Child to parent goes the other way with events. The child creates a CustomEvent, say select, with the record Id in the detail, and dispatches it. The parent listens with onselect on the child's tag. Event names are lowercase with no on prefix, and I keep them from bubbling unless I really need that. For components that aren't in the same tree, like two separate components dropped on one Lightning page, I use Lightning Message Service. I define a message channel, one component publishes and the other subscribes, and it works across Aura and Visualforce too. I'd avoid the old pubsub module in new work."
Having a child change its parent's data directly, or using a message channel between a parent and its own child.
Change sets: point and click between connected orgs; no deletions, no history, easy to miss dependencies.
Salesforce DX: source in Git, Salesforce CLI, scratch orgs or sandboxes, CI pipelines, destructive changes.
Production: validate first, then quick deploy in the release window.
"Change sets are the point-and-click way: I build an outbound change set in a sandbox, add components by hand, upload it to a connected org and deploy it there. They're fine for a small admin change, but they only move between orgs linked to the same production, they can't delete components, there's no version history, and it's easy to forget a dependency. With Salesforce DX the source of truth is a Git repository. I work in a scratch org or sandbox, pull the metadata into source format with the Salesforce CLI, commit it, open a pull request, and a pipeline validates and deploys it. That gives me code review, a record of every change, deletions through a destructive changes manifest, and deployments I can repeat. DevOps Center puts a UI on the same Git-based flow for admins. For production I validate first, then quick deploy in the release window."
Making changes directly in production because the deployment process feels slow.
The rule: at least 75 percent org-wide coverage, all tests passing, and some coverage for every trigger.
Good data: own test data via @testSetup or a factory, never real org data.
Real checks: bulk and negative cases, System.runAs, Test.startTest and stopTest, mocks, assertions.
"To deploy Apex to production, the org needs at least 75 percent of its Apex lines covered by tests overall, the tests have to pass, and every trigger needs some coverage. But I treat that number as a floor, not the goal. A good test proves behaviour. I create my own data, usually in a @testSetup method or a test data factory, rather than relying on org data. I test in bulk, with 200 records, to catch queries inside loops. I test the negative path, like a save that should fail, and I run as a restricted user with System.runAs when sharing matters. I wrap the action in Test.startTest and Test.stopTest, which gives fresh limits and makes async jobs finish at stopTest, so I can assert on their results. Callouts get a mock through Test.setMock. And every test ends with real assertions about the outcome."
Writing tests with no assertions just to reach the coverage number.
Don't force it: no commenting out assertions, no skipping tests to get green.
Find the cause: usually a change made directly in production that breaks old test data.
Decide and prevent: go only if fixed and re-validated in the window; validate days ahead next time.
"First, I don't try to force it through, and I don't comment out assertions to get green. I'd read the failures. Very often the cause is something changed directly in production, like a new validation rule or required field, that breaks the test data those old tests create. Or a test depends on real org data that has changed. I'd check whether the fix is small and safe, like updating a test data factory, and whether I can validate it quickly. Then I'd make the call with the release owner: if we can fix and re-validate within the window, we go; if not, we postpone rather than ship something half tested. Either way I'd raise the root cause: we should validate the deployment days before release night, run all tests on a schedule so failures show up early, and stop unreviewed changes going straight into production."
Deleting or disabling the failing tests to get the release out.
Clarify: what onboarding needs, who sales ops are, how region is stored.
Build: a custom object with a lookup, an after-save flow on the stage change, a validation rule with a custom permission.
Access: private opportunities plus criteria-based sharing rules per region.
Prove: test in a sandbox as each type of user, then deploy together.
"First I'd confirm the details: what an onboarding record needs, who counts as sales ops, and how region is stored on the deal. For the data model, I'd create an Onboarding object with a lookup to Opportunity, not master-detail, because delivery needs to own those records themselves. For creation, an after-save record-triggered flow on Opportunity that runs only when the stage changes to Closed Won, creates the onboarding record and assigns it to the delivery queue. For the lock, a validation rule that fires when the previous stage was Closed Won and the amount changes, unless the user has a custom permission, which I'd give sales ops through a permission set. For visibility, with opportunities private by default, I'd add criteria-based sharing rules, one per region, sharing Closed Won deals read-only with that region's delivery group. Then I'd test it in a sandbox as each type of user and deploy it as one release."
AND(
ISPICKVAL(PRIORVALUE(StageName), "Closed Won"),
ISCHANGED(Amount),
NOT($Permission.Edit_Won_Amount)
)
Jumping straight to an Apex trigger for all three parts, or giving delivery managers View All on opportunities.
Situation: what failed, for whom, and why it was urgent.
Diagnosis: the debug log, the limit summary, the loop that caused it.
Fix and lesson: the bulk-safe rewrite, a 200-record test, and the process change.
"At my last company, the sales ops team ran a monthly import of about five thousand accounts through a data loader, and one month it started failing with too many SOQL queries. Nothing had changed in the import itself, so I pulled a debug log for the integration user and read the limit summary. A developer had added a helper that looked up the territory for each account, and it ran a query inside a loop. In the UI, one record at a time, it was fine. At 200 records per chunk it went past a hundred queries. I refactored it to collect all the territory keys first, query once into a map, and then assign. I added a test that inserts 200 accounts and checks the result, and the next import ran clean. After that we added a bulk test to our code review checklist."
A story where the fix was raising a limit, splitting the file smaller, or turning the automation off for good.
Request: what was asked for and what it would have cost to build.
Real need: the questions that uncovered what the business actually needed.
Outcome: the standard solution, how you showed it, and what the saved effort went to.
"A sales director once asked for a custom Lightning page to approve large discounts, with its own screen, email alerts and a history log, and the estimate was a few weeks of developer time. When I sat with him and walked through what he actually needed, it came down to this: deals above a certain discount go to the regional manager, then finance, with a record of who approved and when. That's exactly what a standard approval process does, with a flow for the notifications. I built a working version in a sandbox in two days and showed it to him using his own deals. He wanted one extra field on the approval screen, which was a simple layout change. It went live the next week, admins could maintain it without developers, and the developer time went to an integration that genuinely needed code."
A story where you simply refused the request without showing an alternative.
The mess: what ran on the object and the symptoms users felt.
Map first: an inventory of every automation and its real order from logs.
Migrate safely: a target pattern, small releases, tests pinning old behaviour, a bypass switch.
"At my last job I inherited an org where the Opportunity object had two triggers, four Process Builders, a couple of old workflow rules and three flows, built by different people over years. Users were hitting CPU time limit errors on big deals, and some fields were overwritten by one automation after another. I didn't start by rewriting. First I mapped every automation on the object, what it touched and when, and checked debug logs to see the real order. Then I agreed a target with the team: one trigger with a handler class for the heavy logic, and before-save flows for simple field updates. I migrated in small releases, one process at a time, each with tests that pinned the old behaviour, and I added a bypass switch for data loads. Over about two months the CPU errors stopped, the overwrites went away, and changes took hours instead of days."
A big-bang rewrite of everything in one release, with no tests of the old behaviour.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.