Post 1: Production Debugging, Trigger Logic, Batch Failures & Integration Errors
35Scenario Questions
6+ YearsExperience Level
Full Code+ Debugging Workflows
100% FreeNo Signup Required
What's inside: Real production Salesforce developer scenarios for 6+ year engineers. Not "what is a trigger" — but "your batch failed at record 300,000, how do you make it resumable?" and "reports on 8M row objects are timing out, walk me through your fix."
Each of the 35 questions follows our signature 7-step interview format: direct answer, root cause, code example (problem + solution), debugging workflow, Salesforce config, real-world XYZ Company example, key interviewer points, and a one-liner you can say directly in the room.
Reports on 8M Row Object Timing Out — Walk Me Through Your Fix
Advanced
Direct Answer
Reports timeout on large objects because queries fetch all rows instead of filtering upfront. Add required date filter, archive old data, and index lookup fields. Reports scale linearly with dataset size — unfiltered queries on 8M rows hit the 5-minute timeout.
Why?
Salesforce report queries scan every row unless you apply a filter upfront. On 8M rows, that's an 8M-row SOQL execution scanning the entire dataset. Without a date filter or database index, Salesforce's query optimizer does a full table scan. Query time scales with dataset size: 1M rows = 1 second, 8M rows = 8 seconds. Add the 5-minute (300-second) report timeout, and you need aggressive filtering and indexing to stay under it.
Root cause chain: Unfiltered query → full table scan → scans 8M rows → 5+ minute execution → timeout.
Code / SOQL Example
// ❌ WRONG — Full table scan on 8M rows
SELECT Id, Name, Amount, CreatedDate
FROM Order__c
WHERE RecordTypeId = '012xx000000001'
LIMIT 100000;
// ✓ CORRECT — Filtered by indexed date range
SELECT Id, Name, Amount, CreatedDate
FROM Order__c
WHERE RecordTypeId = '012xx000000001'
AND CreatedDate = THIS_QUARTER
LIMIT 100000;
// Salesforce Index (Setup > Object Manager > Order__c > Indexes)
// Index Name: Order_DateRecordType_Idx
// Indexed Fields: CreatedDate (ascending), RecordTypeId (ascending)
// This allows optimizer to seek to records matching CreatedDate AND RecordTypeId
// Instead of scanning all 8M rows, it seeks to ~200k matching rows
Debugging Workflow
Identify report timing out: User says "Q1 Revenue Report takes forever." Go to Setup > Reports. Run the report manually. Note how long it takes (likely 4:59 before timeout).
Check report filters: Edit report > Look at "Filters" section. Is there a required date filter? If report has "All Time" or no date filter, it's querying all 8M rows.
Run SOQL in Dev Console: Copy the exact query the report runs. Use browser DevTools (Network tab, XHR) to capture the SOQL. Run it in Dev Console to measure actual execution time without report rendering overhead.
Add required date filter to report: Edit report, add "CreatedDate = THIS_QUARTER" as a REQUIRED filter. Resave report. Run it again — execution time should drop from 4:59 to <30 seconds.
Create custom index: Go to Setup > Object Manager > Order__c > Indexes. Create new index: Name = "Order_DateRecordType", Fields = [CreatedDate, RecordTypeId]. (Max 10 custom indexes per object.)
Test report again: Re-run report. Execution time should drop further (index helps even with filtered queries). Monitor report run time over next week — should consistently complete in <10 seconds.
Archive old data (optional): If report queries 2 years of data and mostly recent data matters, archive orders >2 years old to a separate object or external storage. This reduces active dataset from 8M to 2M rows, dramatically improving performance.
What Salesforce Config Controls It
Report Filters (Setup > Reports > Report Name): Required vs. optional filters. Required filters are applied to every report run and reduce initial dataset before rendering.
Custom Indexes (Setup > Object Manager > Indexes): Up to 10 custom indexes per object. Include the fields your reports filter by most often. Salesforce optimizer uses indexes to seek instead of scan.
Report Timeout: 5-minute timeout for synchronous report runs. Scheduled reports have up to 30 minutes but still hit performance issues at 8M rows.
Database Statistics: Salesforce's query optimizer uses row count and field cardinality to choose indexes. If you create an index and the optimizer doesn't use it, go to Indexes > "Update Statistics" to refresh.
Data Archival: No built-in archive feature; use platform events or external systems to move old data out of Salesforce. Reduces active dataset, improves all query performance.
Real World Example
XYZ Company has an Order__c object with 8.2M orders (spanning 5 years). Their "Q1 Revenue Report" was timing out every time. Root cause: No date filter on the report, so it queried all 8.2M orders. Query executed in 4+ minutes, timed out at 5 minutes.
Fix applied:
1. Added required "Created Date = THIS_QUARTER" filter to report
2. Created custom index on (CreatedDate, RecordTypeId)
3. Result: Report runs in 8 seconds instead of timing out. Batch jobs using the same query also became 10x faster.
Key Points for Interviewer
Salesforce does NOT automatically optimize queries — unfiltered queries scan the entire dataset.
Date filters are the most effective fix because most queries need "recent" data (this month, quarter, year).
Indexes are not "set and forget" — verify the optimizer is using them via SOQL analyzer or by checking query plan.
At 8M+ rows, even indexed queries timeout if scope is too broad — consider archiving to reduce active dataset size.
"Unfiltered queries scan all 8M rows, hitting the 5-minute timeout. Add a required date filter to the report, create an index on filtered fields, and archive rows older than 2 years to reduce dataset size."
2
Batch Job Failed at Record 300,000 — How Do You Make It Resumable?
Advanced
Direct Answer
Store last successfully processed record ID in a Custom Metadata or Custom Setting. On batch restart, query records where ID > checkpoint to resume instead of reprocessing the first 300k records. Without a checkpoint, restarting the batch will duplicate/reprocess data.
Why?
Batch apex processes records in 200-record chunks (default batch size). If the 1500th batch (300k records) fails, restarting the batch from the beginning reprocesses all 300k records, wasting time and causing duplicates. Standard Salesforce batches don't have built-in resumability. Solution: Store a checkpoint (last processed record ID) in Custom Metadata and resume from that ID on next run.
Code / Example
// ❌ WRONG — Reprocesses all 300k records on restart
global Iterable start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, Name, Amount FROM Order__c ORDER BY Id'
);
}
// ✓ CORRECT — Resumes from checkpoint
global Iterable start(Database.BatchableContext bc) {
Checkpoint__mdt checkpoint = [
SELECT LastProcessedId__c FROM Checkpoint__mdt
WHERE DeveloperName = 'OrderSync' LIMIT 1
];
String lastId = checkpoint?.LastProcessedId__c ?? '000000000000000';
return Database.getQueryLocator(
'SELECT Id, Name, Amount FROM Order__c WHERE Id > :lastId ORDER BY Id'
);
}
global void finish(Database.BatchableContext bc) {
// Update checkpoint with last processed ID from Database.Stateful
update new Checkpoint__mdt(
DeveloperName = 'OrderSync',
LastProcessedId__c = this.lastProcessedId
);
}
Debugging Workflow
Observe batch failure: Run batch. At record 300k, exception throws (governor limit, API timeout, etc.). Batch status = "Failed".
Check Batch Job History: Setup > Batch Jobs. Note the JobId and failure reason (e.g., "SOQL limit exceeded").
Check checkpoint storage: Query Custom Metadata table (Checkpoint__mdt). See if LastProcessedId__c is populated. If empty, batch never updated checkpoint (failed before finish() ran).
Manually update checkpoint: Query Order__c for the 1500th batch's last record ID (around record 300k). Manually update Checkpoint__mdt.LastProcessedId__c = that ID.
Re-run batch: Batch start() queries records WHERE Id > checkpoint. Batch resumes at record 300,001, skipping first 300k.
Monitor resume: Check batch logs. Batch should complete without re-processing already-synced records. Verify finish() updates checkpoint with final ID.
What Salesforce Config Controls It
Custom Metadata (Checkpoint__mdt): Non-queryable metadata that persists across batch runs. Store LastProcessedId__c here (config, not data).
Custom Settings: Alternative to metadata (queryable in SOQL, but slower). Use if you need to query checkpoint frequently.
Database.Stateful: Retains state within ONE batch execution, not across batch restarts. Can't use for persistence across runs.
Batch Scope: Default 200 records per batch execute(). Smaller scope = more frequent finish() calls = more checkpoint updates.
Real World Example
XYZ Company: OrderSyncBatch processes 500k orders → ERP. Batches 1-1499 succeed (300k records). Batch 1500 fails (API timeout on external ERP). Without checkpoint, restart reprocesses all 300k orders, causing ERP duplicates and wasting 2 hours.
Fix: Added Checkpoint__mdt table with LastProcessedId__c field. On batch failure at record 300k, checkpoint is at 300,000. On restart, batch queries WHERE Id > 300000, resuming from record 300,001. No duplicates, no wasted processing.
Key Points for Interviewer
Salesforce batches do NOT have built-in resumability — you must implement it.
Storing checkpoint in Custom Metadata is better than Custom Settings (metadata = config, not data storage).
Salesforce generates record IDs sequentially by timestamp, so ID > X works as a safe resume marker.
Database.Stateful retains state within ONE batch execution, not across batch restarts — use metadata for persistence.
"Store the last successfully processed record ID in Custom Metadata. On batch restart, query records where ID > checkpoint to skip already-processed records and avoid duplicates."
3
Scheduled Flow Runs Every 5 Minutes but Keeps Timing Out — Diagnosis?
Advanced
Direct Answer
Flow has 10-minute timeout. If scheduled every 5 minutes and takes 8 minutes, flows overlap: 2-3 instances running in parallel, each executing unoptimized SOQL, hitting resource limits. Solution: Add locking flag to prevent overlaps, optimize SOQL with filters, or increase interval to 15+ minutes.
Why?
Scheduled flows run independently. If Flow A (interval: 5 min, runtime: 8 min) is triggered 3 times before instance 1 finishes, all 3 instances run in parallel. Each executes the same SOQL, saturating database connections and hitting governor limits (SOQL row count, database connections, etc.). Result: parallel instances competing for resources, all timeout.
Debugging Workflow
Check flow execution history: Setup > Flows > Flow Name > Run History. Note if multiple runs are "In Progress" simultaneously.
Add locking flag in Custom Setting: Create Flow__c Custom Setting with field IsFlowRunning__c (Boolean).
In flow: check if IsFlowRunning__c = true. If yes, exit (another run is in progress). If no, set to true, run logic, set to false at end.
Optimize SOQL: Add filters (WHERE Status = 'Draft' AND LastModifiedDate > LAST_N_DAYS:1) to reduce row count from 500k to 10k.
Re-run and monitor: Check flow history. Should see no overlapping runs, faster completion time.
Key Points
Scheduled flows run independently — if interval is shorter than runtime, they overlap.
Locking is the first defense: prevent overlapping before optimizing queries.
Query filters (date range, status) often outperform rewriting logic.
"Scheduled flow has 10-minute timeout. If scheduled every 5 minutes but takes 8 minutes, flows overlap and timeout. Add a locking flag to prevent overlaps and optimize SOQL with filters."
4
Memory Leak in Batch Job — Heap Size Increases Each Batch. Diagnosis?
Advanced
Direct Answer
Holding references to records in a static variable that never clears between batches. Static collections accumulate across batch boundaries. Solution: Use local variables scoped to execute() method (garbage collected after each batch) or clear static collections in finish().
Why?
Static variables persist for the entire transaction (or process context). If you add Order records to a static List inside execute(), that List grows with each batch. After 1000 batches (200 records each = 200k records), static List holds 200k sObjects in memory. Heap limit = 12 MB. At ~11.9 MB heap usage, next batch execute() tries to allocate more memory, hits limit, OOM exception thrown.
Code Example
// ❌ WRONG — Static list accumulates forever
global class OrderBatch implements Database.Batchable {
static List allOrders = new List();
global void execute(Database.BatchableContext bc, List scope) {
allOrders.addAll(scope); // Never cleared, grows 200 records per batch
}
}
// After 1000 batches: 200k records in memory, heap explodes
// ✓ CORRECT — Local variables, cleared each batch
global void execute(Database.BatchableContext bc, List scope) {
Long heapBefore = Limits.getHeapSize();
List ordersToProcess = (List)scope;
update ordersToProcess;
Long heapAfter = Limits.getHeapSize();
System.debug('Heap used: ' + (heapAfter - heapBefore) + ' bytes');
// ordersToProcess is garbage collected when method ends
}
Key Points
Static variables persist for entire transaction (all batches). Avoid static Lists/Maps for bulk data.
Use local variables scoped to execute() — garbage collected automatically after method ends.
Limits.getHeapSize() is your diagnostic tool. Print it to track memory across batches.
12 MB heap limit per batch. Static collections can exceed this on large datasets.
"Static variables accumulate across batches. Use local variables scoped to execute() and print heap size to verify memory is being freed after each batch."
5
Database.getQueryLocator() vs. Iterable<sObject> in Batch Start()?
Medium
Direct Answer
Use getQueryLocator() for standard SOQL queries — Salesforce handles pagination automatically, no heap impact, supports up to 50M records. Use Iterable<sObject> for custom logic (filtered results, API data) — you control the collection, but limited to ~50k records before hitting heap limit.
Why?
getQueryLocator() returns a cursor (pointer to database results). As execute() runs, it fetches records in 200-record chunks from database, never holding all 50M records in memory. Iterable returns a List, which means all records are loaded into memory at once. For 50M records, loading all into memory exceeds heap (12 MB). For 50k records, Iterable works but is risky.
Code Example
// ✓ CORRECT — getQueryLocator for 50M+ rows
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, Amount FROM Order__c'
);
}
// ✓ CORRECT — Iterable for custom filtering, API data
global Iterable start(Database.BatchableContext bc) {
List dbOrders = [SELECT Id FROM Order__c LIMIT 50000];
List filtered = new List();
for (Order__c o : dbOrders) {
if (o.Amount > 1000) filtered.add(o);
}
return filtered; // ✓ Heap-safe, ~50k records
}
// ❌ WRONG — Iterable loading all 50M rows into memory
global Iterable start(Database.BatchableContext bc) {
return [SELECT Id FROM LargeObject__c]; // Heap explodes
}
Key Points
getQueryLocator() = streaming cursor, 50M+ rows, no heap pressure.
Iterable = load-all-at-once, max ~50k records before heap limit (12 MB).
If processing >10k records from DB, always use getQueryLocator().
"Use getQueryLocator() for DB queries on large datasets — it streams results without loading into memory. Use Iterable for custom filtering or API data where you control the collection size."
6
SOQL Query Takes 2 Seconds, Report Times Out — Why?
Medium
Direct Answer
Query execution (2s) ≠ Report render time (4-6s additional). Reports render summaries, grouping, sorting, pagination on top of query results. Large result sets cause rendering timeout. Solution: Add date filter to narrow results, disable unnecessary grouping, or use Visualforce/LWC dashboard instead of report.
Why?
Report workflow: (1) Execute SOQL query (2s), (2) Fetch all result rows (~50k rows), (3) Apply grouping/summaries (1-2s per grouping level), (4) Sort results (1-2s for 50k rows), (5) Format/paginate for display (1-2s). Total = 2s + 2s + 2s + 1s = ~7s. If report has 2+ grouping levels, add another 2-3s per level. Total exceeds 5-minute timeout.
Key Points
Query time and render time are separate. 2s query + 4s render = 6s total.
Grouping/summaries on 100k rows add 2-3 seconds each.
Filtering (date, status) reduces result set size faster than optimizing render logic.
"Reports render query results (grouping, summaries, sorting), adding 4-6 seconds. Filter results to reduce dataset, disable unnecessary grouping, or use Visualforce for complex analytics."
7
Visualforce Page: 3 Seconds in Sandbox, 45 Seconds in Production
Advanced
Direct Answer
Sandbox: 1k records. Production: 500k records. Unfiltered VF page SOQL loads all records. Query time scales with dataset size. Solution: Add LIMIT and required filter (date, status) to SOQL, or use StandardSetController for pagination.
Why?
Sandbox is a point-in-time copy of production data (usually). If production has 500k orders, sandbox might have 10k (truncated for sandbox size limits). Query on 10k records = 0.5s. Query on 500k records = 5-10s. Add VF rendering overhead (binding, repetition) = 40+ seconds.
Debugging Workflow
Check data size: sandbox has 1k orders, prod has 500k (confirmed via COUNT query).
Run VF page in both: sandbox = 3s, prod = 45s (15x slower).
Add LIMIT to SOQL: SELECT ... LIMIT 500 (cap at 500 records).
Add filter: WHERE CreatedDate = THIS_MONTH (reduces 500k to ~40k).
Use StandardSetController for pagination: loads 100 records per page, only renders visible page.
Re-test: prod VF now loads in 3-5s (matches sandbox).
Key Points
Always test against production-scale data in sandbox.
Query performance scales linearly with data: 500x data = 500x slower query time.
Unfiltered queries work on 10k but fail on 1M records.
"Sandbox has 1k records, prod has 500k. Unfiltered queries scale with data volume. Add LIMIT and required date filter to reduce query scope, or use pagination."
8
API Callout Hangs 90 Seconds in Loop — Fix?
Advanced
Direct Answer
Sync callout timeout: 120s. Loop with 4 callouts × 30s = 120s timeout. Move to Queueable (3600s timeout per execution) or parallelize with future() (5 parallel callouts max).
Why?
Sync callout is blocking (waits for response). If callout A takes 30s, callout B starts after A finishes (30s later). 4 callouts × 30s = 120s. Callout C would start at 120s mark, but timeout is 120s, so C gets cancelled immediately or times out mid-execution.
Key Points
Sync callout: 120s timeout. Sync loop of 4 callouts will timeout.
Queueable: 3600s timeout, best for sequential callouts (chainable).
Future: supports 5 parallel callouts per invocation, no chaining.
"Sync callouts timeout at 120s. If looping 4+ times, use Queueable with chaining (3600s timeout) or future for parallelization (5 parallel max)."
9
LWC Grid Rendering 100k Rows Freezes UI
Medium
Direct Answer
Rendering 100k DOM elements blocks browser's main thread for 10-30 seconds. Solution: Pagination (fetch 50-100 rows per page) or virtual scrolling (render only visible rows).
Why?
Browser rendering engine is single-threaded. Creating 100k DOM elements (one per row) requires 100k DOM insertions, style recalculations, layout passes. At ~1ms per DOM element, 100k elements = 100s of CPU time (but browser CPU is shared, so actual time = 10-30s depending on system). During this time, UI is frozen (no clicks, no scrolls, looks like hang).
Key Points
100k DOM elements = 10-30s rendering time, UI frozen.
Pagination: Standard fix, 50-100 rows per page keeps UI responsive.
Virtual scrolling: Advanced, render only visible rows dynamically.
"Rendering 100k DOM elements freezes the browser. Use pagination (50-100 rows/page) or virtual scrolling to render only visible rows."
10
Custom Index Exists but SOQL Still Slow — Why Not Used?
Advanced
Direct Answer
Index field order matters. If index is (CreatedDate, Status), query must filter CreatedDate first. Also, optimizer may choose a different index if it calculates lower cost. Solution: Use SOQL Analyzer to verify index is used, ensure WHERE clause order matches index field order, update statistics if needed.
Why?
Salesforce's query optimizer chooses indexes based on estimated query cost. Index field order matters: index (A, B) is optimized for WHERE A=x AND B=y. If query is WHERE B=y AND A=x, optimizer might not use index (because filtering on B first doesn't match index order). Also, if optimizer calculates that a different index (or full table scan) is cheaper, it uses that instead.
Key Points
Index field order matters: (A, B) index helps WHERE A=x AND B=y, not WHERE B=y AND A=x.
Optimizer chooses indexes based on estimated query cost, not existence.
SOQL Analyzer shows which index is used; "Update Statistics" forces recalculation.
"Index field order matters. If index is (CreatedDate, Status), WHERE clause must filter CreatedDate first. Use SOQL Analyzer to verify index is used, then Update Statistics if needed."
⚡
Trigger Logic & Data Integrity
10 Questions
11
Static Boolean Recursion Guard Fails — Why?
Advanced
Direct Answer
Static Boolean persists across ALL transactions. If flag is set to true in transaction 1 (and not reset), transaction 2 sees flag=true and skips logic. Solution: Use Set<Id> to track specific records instead, or reset flag in finish() block of batch if used with batches.
Why?
Static variables are process-scoped, not transaction-scoped. If a trigger sets `static Boolean preventRecursion = true`, that flag persists beyond the current transaction. Next transaction sees flag=true and skips logic (even if it shouldn't). Set<Id> is safer because it tracks which specific records were processed, allowing multiple different records to be processed while still preventing re-processing of the same record.
Code Example
// ❌ WRONG — Static Boolean persists beyond transaction
trigger OrderTrigger on Order__c {
static Boolean preventRecursion = false;
if (!preventRecursion) {
preventRecursion = true; // Set to true
// Do logic
// At end of transaction: flag is still true
// Next transaction: flag=true, logic skipped incorrectly
}
}
// ✓ CORRECT — Static Set tracks processed records
trigger OrderTrigger on Order__c {
static Set processedIds = new Set();
for (Order__c o : Trigger.new) {
if (!processedIds.contains(o.Id)) {
processedIds.add(o.Id);
// Process this order
}
}
// processedIds is cleared after transaction ends
}
Debugging Workflow
Observe: Trigger logic runs on first insert, doesn't run on second insert (should run both times).
Check trigger code for static Boolean. If found, that's the culprit.
Test: Insert order 1 (logic runs). Check log — flag=true. Insert order 2 (logic skipped). Verify flag still=true.
Fix: Replace Boolean with Set<Id> of processedIds. Each record ID is added to Set after processing.
Retest: Insert order 1 (added to Set). Insert order 2 (not in Set, logic runs). Set is cleared after transaction ends.
Key Points
Static Boolean = process-scoped, persists across transactions (dangerous).
Static Set<Id> = safer, tracks specific records, cleared after transaction.
For bulk operations (100 inserts), Set<Id> prevents re-processing same record within transaction.
Always reset static variables if using in batch (reset in finish() or use non-static pattern).
Real World Example
XYZ Company: OrderTrigger used static Boolean preventRecursion. Order update triggered Account update, which updated Order (two-way loop). First order insert: flag=false, logic ran. After first transaction, flag=true. Second order inserted 5 seconds later (different transaction): flag still=true (persisted from first transaction), logic skipped. Order didn't sync to ERP. Took 2 days to discover flag was persisting across transactions.
Fix: Switched to Set<Id> to track processed Order IDs within single transaction. Set cleared after transaction ended. All subsequent orders processed correctly.
"Static Boolean recursion guards persist across transactions (dangerous). Use Set<Id> to track processed records within a single transaction instead. Set clears after transaction ends, Boolean persists."
12
Trigger Modifies Record Inside Loop — Order of Execution Problem?
Medium
Direct Answer
Modifying Trigger.new inside before-trigger is allowed and recommended (before DML saves). Modifying Trigger.new inside after-trigger is NOT recommended (changes after-save won't persist unless you DML update). Correct pattern: modify data in before-trigger, query in after-trigger, update children with separate DML.
Key Points
Before-trigger: modify Trigger.new before save (changes persist).
After-trigger: don't modify Trigger.new (changes don't persist). Query and update separately if needed.
Order of execution: Before Triggers → Validate → Save → After Triggers → Child Records → After-Child Triggers.
"Modify Trigger.new in before-trigger (changes persist). Don't modify in after-trigger (changes don't persist). Query and update separately if needed in after-trigger."
13
Trigger Updates Parent Record Inside After-Trigger — Does Parent Trigger Fire Again?
Medium
Direct Answer
No. Salesforce prevents after-trigger from re-firing on its own updates (built-in recursion prevention). If Child trigger updates Parent, Parent after-trigger does NOT fire again. However, if Parent trigger updates Child, Child after-trigger DOES fire. This asymmetry can cause unexpected cascades.
Why?
Salesforce's recursion prevention: after-trigger updates within the same execution context don't re-fire the same after-trigger. This prevents infinite loops where A→B→A→B. However, child triggers can still fire, creating one-way cascades. You need additional guards (Set<Id>) to prevent loops.
Key Points
After-trigger updates don't re-fire same after-trigger (built-in prevention).
But child/parent triggers can still fire, creating cascades (one-way loops).
Document trigger execution flow; it's easy to miss cascade points.
"After-trigger updates don't re-fire the same trigger. But child/parent updates can cascade. Use Set<Id> guards on both triggers to prevent two-way loops."
14
Trigger Queries Data that Just Changed — Dirty Read Risk?
Medium
Direct Answer
No. SOQL inside trigger sees committed data at trigger start time (consistent snapshot). If Order is updated mid-trigger, SOQL sees pre-update state, not dirty/uncommitted state. This prevents dirty reads but can cause logic errors if you expect SOQL to see in-flight changes.
Key Points
SOQL in trigger sees data at transaction start, not in-flight changes.
Prevents dirty reads, but query may miss recent updates within same transaction.
Use Trigger.new/old for in-flight data, SOQL for stable data outside transaction.
"SOQL in trigger sees data at transaction start (consistent snapshot), not in-flight changes. Prevents dirty reads but query may miss recent updates. Use Trigger.new/old for in-flight data."
15
Bulk Insert 1000 Orders — Trigger Processes Them Individually?
Medium
Direct Answer
No. Salesforce invokes trigger ONCE per DML statement (not per record). Bulk insert 1000 orders = trigger fires 1 time with Trigger.new = List<Order__c> (1000 records). Trigger must handle bulk. Never loop inside loop (nested loop = O(n²) performance).
Key Points
Trigger fires once per DML, receives list of records (up to 200 in batch).
Bulkify: process list in one query/update, not per record.
Nested loops (loop in trigger, inside loop in handler) cause O(n²) performance.
"Trigger fires once with Trigger.new = list of all records inserted. Process in bulk (one query, one update). Avoid loops inside loops (O(n²) performance hit)."
16
Field Update Flow Fires Trigger — How Do You Distinguish?
Medium
Direct Answer
Trigger.isUpdate alone doesn't distinguish flow vs. API update. Use workflow field to mark flow updates: Flow updates Status__c = 'Processed' + marks FlowRun__c = true. Trigger checks if FlowRun__c = true; if yes, skip logic (avoid feedback loop). Or add feature flag to disable trigger for flow context.
Key Points
Trigger doesn't know if update came from flow vs. API vs. UI.
Use a flag field (e.g., FlowRun__c) to mark flow updates.
Trigger checks flag; if true, skip logic (prevent feedback loop).
"Trigger can't distinguish flow from API update. Add a flag field (FlowRun__c = true) to mark flow updates. Trigger checks flag and skips logic if set to prevent feedback loops."
17
Validation Rule Fails, But Trigger Still Fires — Why?
Medium
Direct Answer
Validation rules fire BEFORE triggers. If validation fails, record save is blocked and trigger never fires. If trigger fires, validation passed. However, if validation error is suppressed (API call with allow_errors=true), trigger may still fire — but record didn't save, so trigger receives empty Trigger.new.
Key Points
Validation rules fire before triggers (order: Validation → Before-Trigger → Save).
Validation failure blocks save and trigger doesn't fire.
Validation can be suppressed via API; trigger may fire with empty/partial data.
"Validation rules fire before triggers. Validation failure blocks save and trigger doesn't fire. Never assume trigger logic ran if validation was strict."
18
Before-Trigger Modifies Trigger.new, But Change Never Persists?
Medium
Direct Answer
Before-trigger modifications to Trigger.new DO persist. If not persisting, likely cause: Field has a default value in custom field definition that overrides trigger change, or field has formula that recalculates. Solution: Check field setup (Setup > Customize > Fields). Disable formula/default if conflicting with trigger logic.
Key Points
Before-trigger changes persist if field has no default or formula.
Default values and formulas recalculate after trigger, overriding trigger changes.
Check field setup: if Default Value or Formula is set, it takes precedence over trigger.
"Before-trigger modifications persist. If not persisting, check field setup for Default Value or Formula — these recalculate after trigger and override trigger changes."
19
Trigger on A, Updates B, Updates A — Cascade Causes Data Corruption?
Advanced
Direct Answer
Two-way cascade: A-trigger updates B, B-trigger updates A (back to A). Creates infinite loop if not guarded. Solution: Implement recursion guard on both triggers using Set<Id> to track processed record IDs. Prevent reprocessing same ID within transaction.
Code Example
// ❌ WRONG — A updates B, B updates A (infinite loop)
trigger ATrigger on A__c {
if (Trigger.isAfter && Trigger.isUpdate) {
List bList = [SELECT Id FROM B__c WHERE AId = :new.Id];
for (B__c b : bList) b.Status = 'Updated';
update bList; // Fires B-trigger
}
}
trigger BTrigger on B__c {
if (Trigger.isAfter && Trigger.isUpdate) {
List aList = [SELECT Id FROM A__c WHERE Id IN (SELECT AId FROM B__c WHERE Id = :new.Id)];
for (A__c a : aList) a.Status = 'Updated';
update aList; // Fires A-trigger (infinite loop)
}
}
// ✓ CORRECT — Both triggers track processed IDs
trigger ATrigger on A__c {
static Set processedAIds = new Set();
if (Trigger.isAfter && Trigger.isUpdate && !processedAIds.contains(Trigger.new[0].Id)) {
processedAIds.add(Trigger.new[0].Id);
// Update B records
}
}
Guard BOTH triggers with Set<Id>; prevent same record from reprocessing.
Document trigger dependencies (A depends on B, B depends on A) in code comments.
"Two-way cascades (A→B→A) create infinite loops. Guard both triggers with Set<Id> to prevent same record from reprocessing. Document dependencies in comments."
20
Test Class Doesn't Fire Trigger — Missing Permissions?
Medium
Direct Answer
Test runs as SYSTEM user (bypasses permissions). Trigger should fire regardless of permission. If trigger doesn't fire in test, likely cause: Trigger is inactive (Setup > Triggers > Inactive), or test uses @isTest(SeeAllData=true) which runs as SYSTEM but may skip triggers for historical data. Solution: Verify trigger is active, test with fresh data (not SeeAllData=true).
Key Points
@isTest runs as SYSTEM (bypasses FLS, permissions). Trigger should fire.
If trigger doesn't fire: check if trigger is active (Setup > Triggers).
@isTest(SeeAllData=true) may skip some triggers on historical data.
"Tests run as SYSTEM, bypassing permissions. If trigger doesn't fire in test, check if trigger is active (Setup > Triggers). Avoid SeeAllData=true; use fresh test data instead."
⏳
Batch & Async Processing
8 Questions
21
Scheduled Batch Job Queues 2 Copies — Why?
Advanced
Direct Answer
Batch execution time exceeds schedule interval. If batch runs every 1 hour but takes 70 minutes, second schedule triggers while first batch is still running. Salesforce queues second batch. Solution: Increase schedule interval (every 2 hours) or optimize batch to complete within schedule interval.
Key Points
Scheduled job fires independently of batch status (doesn't check if previous batch is running).
If batch runtime > schedule interval, batches queue up (overlap).
Solution: increase schedule interval OR optimize batch runtime.
Monitor Batch Jobs history; if you see 2 jobs starting close together, confirm overlap.
"Scheduled jobs fire independently of batch status. If batch runtime exceeds schedule interval, batches queue. Increase schedule interval to allow completion, or optimize batch runtime."
22
Queueable Job Fails Silently — No Error Logged?
Medium
Direct Answer
Queueable exceptions don't auto-log. Solution: Wrap execute() in try-catch, log errors to custom Error__c object. Queueable never retries on failure (unlike Batch.finish() which can re-enqueue). Without logging, you won't know it failed.
Key Points
Queueable exceptions don't auto-log. You must wrap in try-catch and log manually.
Queueable never retries (unlike Batch). Log and monitor manually.
Use custom Error__c object to track all async failures centrally.
"Queueable exceptions don't auto-log. Wrap execute() in try-catch and log to custom Error__c object. Without logging, you won't know it failed."
23
Future() Method Limit: 50k per 24 Hours — How to Monitor?
Medium
Direct Answer
Use AsyncApexJob to count recent enqueues. Query: SELECT COUNT() FROM AsyncApexJob WHERE JobType='Future' AND CreatedDate > LAST_N_HOURS:24. If count approaches 50k, defer future() calls (queue in custom table for retry later) or move to batch (batch has 50k per 24hr limit but is less impactful).
Key Points
Future limit: 50k per 24 hours (org-wide, all users).
Query AsyncApexJob to monitor usage (count JobType='Future' in last 24hr).
If approaching limit, queue calls in custom table for retry or move to batch.
"Future limit: 50k per 24 hours. Monitor via AsyncApexJob query (JobType='Future', CreatedDate > LAST_N_HOURS:24). If approaching limit, defer calls or move to batch."
24
Batch Chaining in finish() — How Many Times Can You Chain?
Medium
Direct Answer
No hard limit on batch chaining. In theory, you can chain indefinitely. In practice, limit yourself to 5-10 chains to avoid cascading delays (each batch takes 1-2 min to start). For large datasets, use single batch with checkpoint resume instead of chaining.
Key Points
Batch chaining = batch in finish() enqueues next batch.
No hard limit, but practical limit ~5-10 (delays compound).
For massive jobs, use single batch with checkpoint resume (faster than 20 chained batches).
"Batch chaining has no hard limit. Practical limit ~5-10 (delays compound). For 100M+ records, use single batch with checkpoint resume instead."
25
Trigger Enqueues Queueable, Then Dequeues If Error — How?
Advanced
Direct Answer
Queueable fires AFTER trigger finishes (asynchronously). Trigger can't catch Queueable failures (they're async). Solution: Store Queueable state (pending) in custom object on enqueue. After Queueable runs, update state (success/failed). Trigger/batch can check failed state and retry or alert.
Key Points
Queueable is async; trigger can't catch its exceptions directly.
Store Queueable state in custom object (pending → success/failed).
Scheduled batch can check failed jobs and retry or alert.
"Trigger can't catch Queueable failures (async). Store Queueable state in custom object. Mark as Pending on enqueue, update to Success/Failed after execution."
26
ScheduledAction Apex: Retries on Failure?
Medium
Direct Answer
Scheduled Apex doesn't retry on failure. If job fails, it's gone (one-time execution). Solution: Log errors to custom Error__c object. Separate scheduled job monitors Error__c and retries failed jobs.
Key Points
Scheduled Apex = one-time execution per schedule (no retries on failure).
Log all errors to custom object for manual or automated retry.
Separate job monitors errors and retries.
"Scheduled Apex doesn't retry on failure. Log errors to custom object. Run separate scheduled job to monitor errors and retry failed jobs."
27
Database.executeBatch() vs. Database.executeBatchAsync()?
Medium
Direct Answer
executeBatch() is synchronous (older API). executeBatchAsync() is asynchronous (newer). For most use cases, executeBatchAsync() is better (doesn't block triggering transaction). executeBatch() blocks until batch is enqueued (faster feedback but ties up trigger).
Use executeBatchAsync() to avoid blocking trigger.
"executeBatchAsync() enqueues batch without blocking trigger. executeBatch() blocks until enqueue completes. Use executeBatchAsync() for better trigger performance."
28
Batch Job OOM (Out of Memory) at 400k Records — Fix?
Advanced
Direct Answer
Heap limit: 12 MB per batch execution. Processing 400k records (200 per batch = 2000 batches) × large object size = heap exceeded. Solution: Reduce batch size (100 instead of 200), minimize object fields queried (query only needed fields, not *), or split batch into multiple runs (1-2 hour interval).
Key Points
Batch heap limit: 12 MB. Large record size × batch size can exceed limit.
Reduce batch size (100 instead of 200) to halve heap usage.
Query only needed fields (SELECT Id, Name, Amount instead of *).
Clear collections after processing (don't accumulate in static variables).
"Batch OOM at 400k records: reduce batch size (100 vs 200), query only needed fields, avoid static collections that accumulate across batches."
🔗
Integration Errors & Recovery
7 Questions
29
API Callout Timeout (120s) — Retry Strategy?
Advanced
Direct Answer
Sync callout timeout: 120s. On timeout, store in queue (custom object). Queueable queue processes with exponential backoff (2s, 4s, 8s, 16s). Max 3 retries. After 3 failures, move to Dead Letter Queue (DLQ) for manual review. Don't retry synchronously (will timeout again).
Max 3 retries. Move to DLQ after failures for manual review.
"API timeout: queue call in custom object, retry via Queueable with exponential backoff. Max 3 retries, then move to Dead Letter Queue for manual review."
30
API Returns HTTP 429 (Rate Limit) — How to Handle?
Medium
Direct Answer
HTTP 429 = rate limited. Extract Retry-After header from response (tells you how long to wait). Queue request, wait specified time, retry. Don't retry immediately (will get 429 again). Use Queueable queue for serialized, rate-limited processing.
Queueable queue (serialized) naturally rate-limits by processing one at a time.
Don't hammer API on 429 (will fail more).
"HTTP 429: read Retry-After header, wait before retrying. Use Queueable queue for serialized, rate-limited API calls."
31
API Response Partially Fails (400 Bad Request on 1 of 100 Records) — Handling?
Medium
Direct Answer
API returns HTTP 400 (bad request) for record with invalid field. Don't retry entire batch (will fail on same record). Solution: Parse API response, identify failed records, queue only failed records for retry with corrected data. Successful records are marked synced.
Key Points
Parse API response to identify which records failed (usually in response body).
Retry only failed records with corrected data or manual review.
Mark successful records as synced to avoid reprocessing.
"API partial failure: parse response to find failed records. Retry only those records with corrected data. Mark successful records as synced."
32
Webhook Delivery Guarantee: At Most Once or At Least Once?
Medium
Direct Answer
Most webhooks are "at-least-once" (may be delivered multiple times on retries). Solution: Use idempotency key to de-duplicate. Store processed webhook IDs in queue table. On duplicate, return 200 (success) without reprocessing.
Key Points
Webhooks = at-least-once delivery (duplicates possible on retries).
Use idempotency key to de-duplicate (unique ID per webhook from sender).
Store processed keys, skip reprocessing on duplicate.
"Webhooks are at-least-once delivery. Use idempotency key to de-duplicate. Store processed keys, return 200 on duplicate (idempotent API)."
33
External API Down — System Should Degrade Gracefully?
Advanced
Direct Answer
API down = orders fail to sync. Solution: Feature flag to disable sync (orders saved locally without sync), queue syncs for retry when API recovers. Don't block order creation; sync async. Notify admins API is down.
Key Points
Don't block user workflows on external API. Queue syncs, retry later.
Feature flag to disable sync if API is known-down (prevent useless retries).
Alert admins when external API is down (automated, not manual).
"External API down: queue syncs, don't block user workflows. Feature flag to disable retries if down (prevent wasted resources). Alert admins."
34
Data Corruption from API Sync — Detect and Rollback?
Advanced
Direct Answer
Salesforce doesn't have true rollback. Solution: (1) Pre-sync validation: compare source and destination records, alert on mismatch. (2) Versioning: store record version before update, allow manual revert. (3) Recycle Bin: recover deleted records (15-day window). (4) Backup service: 3rd party backup for longer-term recovery.
Key Points
No built-in rollback. Prevent corruption via validation before update.
Version records: store old value before update for manual revert.
Recycle Bin: 15-day retention. Backup service for longer recovery window.
"No true rollback in Salesforce. Validate data before sync update. Version records for manual revert. Use Recycle Bin (15 days) or backup service for recovery."
35
Integration Monitoring: How to Alert on Failures?
Medium
Direct Answer
Store errors in custom Error__c object. Scheduled job queries failures (where AlertSent__c = false), sends email to admin, marks AlertSent__c = true. Set up Slack/Teams notification via webhook for critical failures. Dashboard shows error count/trend.
Key Points
Custom Error__c object = central error log for all integrations.
Dashboard shows error trend (visual monitoring, not just log dumps).
"Store errors in custom Error__c object. Scheduled job monitors and sends alerts (email, Slack). Dashboard tracks error trend. Mark alert sent to avoid duplicates."
📚 Level Up Your Salesforce Interview Prep
Complete Post 1 (35 questions). Continue with Posts 2 & 3 for the full 105-question series:
SF Interview Pro is 100% free and maintained by a Salesforce professional. No ads, no paywalls, and no signup required. If this guide helped you prepare for an interview, earn a certification, or grow your Salesforce career, consider buying me a coffee! ☕💜