This guide covers commonly asked Apex interview questions with practical, real-world scenarios. Prepare with both technical fundamentals and hands-on problem solving.
Question: You have a trigger on the Account object that updates related Opportunities whenever an Account is updated. The trigger is working fine in your sandbox but fails in production when bulk operations happen. What could be the issue, and how would you fix it?
Answer: The trigger is likely hitting governor limits (SOQL query limit of 100 or DML limit of 150). The fix involves bulkification:
trigger AccountTrigger on Account (after update) {
// ❌ BAD: Hits governor limits on bulk operations
// for (Account acc : Trigger.new) {
// List oppsToUpdate = [
// SELECT Id FROM Opportunity WHERE AccountId = :acc.Id
// ];
// }
// ✅ GOOD: Bulkified approach
Set accountIds = new Set();
for (Account acc : Trigger.new) {
accountIds.add(acc.Id);
}
List oppsToUpdate = [
SELECT Id, AccountId FROM Opportunity WHERE AccountId IN :accountIds
];
for (Opportunity opp : oppsToUpdate) {
opp.StageName = 'Prospecting';
}
update oppsToUpdate;
}
IN with a Set/List to fetch multiple records at onceQuestion: You have an Apex trigger that inserts related records. When that related record is created, another trigger fires and updates the original record, which fires the first trigger again. How do you prevent this infinite loop?
Answer: Use a static flag to track whether a trigger has already executed:
public class TriggerHandler {
private static Boolean isExecuting = false;
public static void handleAccountInsert(List newAccounts) {
if (isExecuting) {
return; // Prevent recursion
}
isExecuting = true;
try {
for (Account acc : newAccounts) {
// Process account
acc.Description = 'Processed at ' + DateTime.now();
}
} finally {
isExecuting = false; // Always reset in finally
}
}
}
// Trigger
trigger AccountTrigger on Account (after insert) {
TriggerHandler.handleAccountInsert(Trigger.new);
}
finally block to ensure it always resetsQuestion: Your Apex code needs to call an external API to validate customer information. The API can fail or timeout. How do you handle this safely with proper error reporting?
Answer: Use try/catch with proper timeout handling and logging:
public class CustomerValidator {
public static Boolean validateCustomer(String customerId) {
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/validate?id=' + customerId);
req.setMethod('GET');
req.setTimeout(5000); // 5 second timeout
try {
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
Map response = (Map)
JSON.deserializeUntyped(res.getBody());
return (Boolean) response.get('isValid');
} else {
System.debug('API Error: ' + res.getStatusCode());
logError('ValidateCustomer', 'Invalid response code: ' + res.getStatusCode());
return false;
}
} catch (System.CalloutException e) {
System.debug('Callout timeout or connection error');
logError('ValidateCustomer', 'Callout failed: ' + e.getMessage());
return false;
} catch (Exception e) {
System.debug('Unexpected error: ' + e.getMessage());
logError('ValidateCustomer', 'Unexpected error: ' + e.getMessage());
return false;
}
}
private static void logError(String method, String errorMsg) {
// Log to a custom object or system logs
System.debug(LoggingLevel.ERROR, method + ': ' + errorMsg);
}
}
CalloutException separately for network/timeout issuesQuestion: You need to write a test class that achieves 100% code coverage for your AccountTrigger. What's the best approach for test data setup?
Answer: Use the @testSetup method to create reusable test data:
@isTest
private class AccountTriggerTest {
@testSetup
static void setupTestData() {
// Create test accounts once, reuse in all test methods
List accounts = new List();
for (Integer i = 0; i < 50; i++) {
accounts.add(new Account(
Name = 'Test Account ' + i,
Industry = 'Technology'
));
}
insert accounts;
}
@isTest
static void testBulkAccountUpdate() {
List accounts = [SELECT Id FROM Account];
Test.startTest();
for (Account acc : accounts) {
acc.Phone = '555-0100';
}
update accounts;
Test.stopTest();
// Verify results
List updatedAccounts = [
SELECT Id, Phone FROM Account WHERE Phone = '555-0100'
];
Assert.areEqual(50, updatedAccounts.size(),
'Expected 50 accounts to be updated');
}
@isTest
static void testAccountInsertWithValidation() {
Test.startTest();
Account newAcc = new Account(
Name = 'New Test Account',
Industry = 'Finance'
);
insert newAcc;
Test.stopTest();
Account insertedAcc = [
SELECT Id, Name FROM Account WHERE Id = :newAcc.Id
];
Assert.isNotNull(insertedAcc, 'Account should be inserted');
}
@isTest
static void testAccountDelete() {
Account acc = [SELECT Id FROM Account LIMIT 1];
Test.startTest();
delete acc;
Test.stopTest();
List deletedAccounts = [
SELECT Id FROM Account WHERE Id = :acc.Id
];
Assert.areEqual(0, deletedAccounts.size(),
'Account should be deleted');
}
}
@testSetup to create shared test dataTest.startTest() and Test.stopTest() to reset governor limitsQuestion: Your synchronous Apex code is hitting CPU timeout limits when processing 10,000 records. You need to process them asynchronously. Would you use @future, Queueable, or Batch Apex? Why?
Answer: For this scenario, use Queueable:
@future – Simple, but limited to primitives (no objects), hard to handle failuresQueueable – Can pass objects, chainable, good for moderate jobs (10K+ records)Batch Apex – Best for very large jobs (millions), scheduled processing, auto-retry
public class AccountProcessorQueueable implements Queueable {
private List accounts;
public AccountProcessorQueueable(List accounts) {
this.accounts = accounts;
}
public void execute(QueueableContext ctx) {
try {
for (Account acc : accounts) {
acc.Type = 'Prospect';
}
update accounts;
} catch (Exception e) {
System.debug('Error in Queueable: ' + e.getMessage());
}
}
}
// Usage
List accountsToProcess = [SELECT Id FROM Account LIMIT 10000];
System.enqueueJob(new AccountProcessorQueueable(accountsToProcess));
Question: Your dashboard query that fetches accounts and their opportunities is running slowly. How would you optimize it to reduce query time?
Answer: Use relationships and selective queries:
// ❌ INEFFICIENT: Multiple queries (N+1 problem)
// List accounts = [SELECT Id, Name FROM Account];
// for (Account acc : accounts) {
// List opps = [SELECT Id FROM Opportunity WHERE AccountId = :acc.Id];
// }
// ✅ EFFICIENT: Single query with relationship
List accounts = [
SELECT Id, Name,
(SELECT Id, StageName, Amount FROM Opportunities LIMIT 100)
FROM Account
WHERE Industry = 'Technology'
LIMIT 500
];
// Even better: Use parent-to-child relationship
List opps = [
SELECT Id, Amount, Account.Name
FROM Opportunity
WHERE Account.Industry = 'Technology'
AND StageName IN ('Prospecting', 'Qualification')
ORDER BY Amount DESC
LIMIT 1000
];
WHERE clauses to reduce result setLIMIT to control memory usageQuestion: Your Apex code builds a SOQL query based on user input. A security review flagged potential SOQL injection. How do you fix this?
Answer: Never concatenate user input directly into SOQL. Use bind variables:
// ❌ VULNERABLE: SOQL Injection risk
// String userInput = getUserInput();
// List accounts = Database.query(
// 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\''
// );
// ✅ SAFE: Use bind variables
public static List searchAccounts(String searchTerm) {
String safeSearch = '%' + searchTerm + '%';
return [
SELECT Id, Name FROM Account
WHERE Name LIKE :safeSearch
LIMIT 100
];
}
// ✅ SAFE: Dynamic SOQL with binding
public static List dynamicQuery(String objectName, String whereClause) {
String query = 'SELECT Id FROM ' + objectName; // Object name from admin
if (String.isNotBlank(whereClause)) {
query += ' WHERE ' + whereClause;
}
return Database.query(query); // Use Database.query, not direct String concatenation
}
variable) for all user inputString.escapeSingleQuotes() as a last resortDatabase.query() with parameterized inputs carefullyQuestion: Your batch job processes 100 records but consistently hits CPU timeout (>10s). What can you do to optimize?
Answer: Optimize logic and use async processing:
// ❌ SLOW: Nested loops
List accounts = [SELECT Id FROM Account];
List contacts = [SELECT Id, AccountId FROM Contact];
for (Account acc : accounts) { // 100 iterations
for (Contact con : contacts) { // 1000 iterations = 100K operations!
if (con.AccountId == acc.Id) {
// Process
}
}
}
// ✅ FAST: Use a Map
Map> accountContactMap = new Map>();
for (Contact con : contacts) {
if (!accountContactMap.containsKey(con.AccountId)) {
accountContactMap.put(con.AccountId, new List());
}
accountContactMap.get(con.AccountId).add(con);
}
for (Account acc : accounts) {
List conList = accountContactMap.get(acc.Id);
if (conList != null) {
// Process contacts for this account
}
}
When preparing for Apex interviews, focus on:
Question: You need to call an external API when an Opportunity is created, but triggers don't allow callouts directly. What approach will you use?
Answer: Use Platform Events or @future to decouple the trigger from the callout:
// Approach 1: Use @future for simple callouts
trigger OpportunityTrigger on Opportunity (after insert) {
Set oppIds = new Set();
for (Opportunity opp : Trigger.new) {
oppIds.add(opp.Id);
}
notifyExternalSystem(oppIds);
}
@future(callout=true)
public static void notifyExternalSystem(Set oppIds) {
List opps = [SELECT Id, Name FROM Opportunity WHERE Id IN :oppIds];
for (Opportunity opp : opps) {
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/opportunity');
req.setMethod('POST');
req.setBody(JSON.serialize(new Map{
'id' => opp.Id,
'name' => opp.Name
}));
try {
Http http = new Http();
HttpResponse res = http.send(req);
} catch (Exception e) {
System.debug('Callout failed: ' + e.getMessage());
}
}
}
// Approach 2: Use Platform Events (more scalable)
trigger OpportunityTrigger on Opportunity (after insert) {
List events = new List();
for (Opportunity opp : Trigger.new) {
events.add(new Opportunity_Created__e(
Opportunity_Id__c = opp.Id,
Opportunity_Name__c = opp.Name
));
}
EventBus.publish(events);
}
// Subscriber (separate Apex class)
public class OpportunityEventHandler {
@AuraEnabled
public static void handleOpportunityCreatedEvent(List events) {
for (SObject event : events) {
Opportunity_Created__e oppEvent = (Opportunity_Created__e) event;
callExternalAPI(oppEvent.Opportunity_Id__c, oppEvent.Opportunity_Name__c);
}
}
private static void callExternalAPI(String oppId, String oppName) {
// Make HTTP callout here
}
}
@future(callout=true) – Best for simple, fire-and-forget calloutsPlatform Events – Better for complex, scalable event processingQuestion: A batch job is failing midway due to large data volume. How would you handle partial failures and retries?
Answer: Use Batch Apex with Database.update() to allow partial success:
public class AccountBatchProcessor implements Database.Batchable {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([
SELECT Id, Name FROM Account WHERE Industry = 'Technology'
]);
}
public void execute(Database.BatchableContext bc, List accounts) {
List results = Database.update(accounts, false);
// Log failures for later retry
List failedIds = new List();
for (Integer i = 0; i < results.size(); i++) {
if (!results[i].isSuccess()) {
failedIds.add(accounts[i].Id);
for (Database.Error err : results[i].getErrors()) {
System.debug('Error on ' + accounts[i].Id + ': ' + err.getMessage());
}
}
}
// Log failed records to custom object for retry
if (!failedIds.isEmpty()) {
insertErrorLog(failedIds, 'Update failed');
}
}
public void finish(Database.BatchableContext bc) {
AsyncApexJob job = [
SELECT Id, Status, NumberOfErrors FROM AsyncApexJob WHERE Id = :bc.getJobId()
];
System.debug('Batch finished. Errors: ' + job.NumberOfErrors);
// Send email notification with results
sendNotification(job);
}
private void insertErrorLog(List ids, String reason) {
List logs = new List();
for (String id : ids) {
logs.add(new Error_Log__c(
Record_Id__c = id,
Reason__c = reason,
Retry_Count__c = 0
));
}
insert logs;
}
private void sendNotification(AsyncApexJob job) {
// Send email with batch results
}
}
// Usage
Database.executeBatch(new AccountBatchProcessor(), 200);
Database.update(list, false) to allow partial successQuestion: Users complain that a page is slow due to multiple SOQL queries. How do you optimize the queries?
Answer: Use query optimization techniques:
// ❌ SLOW: 1 + N queries
List accounts = [SELECT Id, Name FROM Account LIMIT 500];
for (Account acc : accounts) {
List contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
acc.ContactCount = contacts.size();
}
// ✅ FAST: Single aggregation query
List results = [
SELECT AccountId, COUNT(Id) contactCount
FROM Contact
GROUP BY AccountId
];
Map accountContactCount = new Map();
for (AggregateResult ar : results) {
accountContactCount.put((Id)ar.get('AccountId'), (Integer)ar.get('contactCount'));
}
List accounts = [SELECT Id, Name FROM Account LIMIT 500];
for (Account acc : accounts) {
acc.ContactCount__c = accountContactCount.get(acc.Id) ?? 0;
}
Question: You need to process 1 million records. Would you use Batch Apex or Queueable, and why?
Answer: Use Batch Apex for 1 million records:
| Criteria | Batch Apex | Queueable |
|---|---|---|
| Scale | Millions of records | 10K–100K records |
| Chainable | No (manual chaining) | Yes (enqueue from execute) |
| Scheduling | Scheduled with Schedulable | Immediate or from trigger |
| Fault Tolerance | Auto-retry per batch | Manual error handling |
// For 1 million records, use Batch
global class ProcessMillionRecordsBatch implements Database.Batchable, Database.Stateful {
global Integer recordsProcessed = 0;
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([SELECT Id FROM Account]);
}
global void execute(Database.BatchableContext bc, List records) {
for (Account acc : records) {
acc.LastProcessedDate__c = DateTime.now();
recordsProcessed++;
}
update records;
}
global void finish(Database.BatchableContext bc) {
System.debug('Processed ' + recordsProcessed + ' records');
}
}
// Execute with batch size of 2000 (max allowed)
Database.executeBatch(new ProcessMillionRecordsBatch(), 2000);
Database.Stateful to track state across batchesDatabase.Batchable for better error handlingQuestion: A trigger is causing recursion and updating the same records again. How do you prevent it?
Answer: Use a recursive check class with context tracking:
public class RecursionControl {
private static Set executedTriggers = new Set();
public static Boolean isFirstExecution(String triggerName) {
if (executedTriggers.contains(triggerName)) {
return false;
}
executedTriggers.add(triggerName);
return true;
}
public static void reset() {
executedTriggers.clear();
}
}
// Trigger
trigger AccountTrigger on Account (after update) {
if (!RecursionControl.isFirstExecution('AccountTrigger')) {
return;
}
// Your logic here
List accountsToUpdate = new List();
for (Account acc : Trigger.new) {
if (acc.BillingCountry != Trigger.oldMap.get(acc.Id).BillingCountry) {
acc.LastLocationChange__c = DateTime.now();
accountsToUpdate.add(acc);
}
}
if (!accountsToUpdate.isEmpty()) {
update accountsToUpdate; // This won't trigger recursion
}
}
Question: You need to ensure only users with proper access can update certain fields via Apex. How do you enforce FLS and CRUD?
Answer: Use with sharing keyword and check permissions:
// Enforce sharing rules
public with sharing class AccountService {
public static void updateAccountFields(Id accountId, String newName) {
// Check CRUD: Can the user read and update Accounts?
if (!Account.SObjectType.getDescribe().isUpdateable()) {
throw new SecurityException('User cannot update Accounts');
}
// Check FLS: Can the user update the Name field?
if (!Schema.sObjectType.Account.fields.Name.isUpdateable()) {
throw new SecurityException('User cannot update Account Name field');
}
Account acc = new Account(Id = accountId, Name = newName);
update acc;
}
public static List getAccounts() {
// Check if user can read Accounts
if (!Account.SObjectType.getDescribe().isAccessible()) {
throw new SecurityException('User cannot read Accounts');
}
// Only return Name field if user has FLS access
if (!Schema.sObjectType.Account.fields.Name.isAccessible()) {
return [SELECT Id FROM Account];
}
return [SELECT Id, Name FROM Account];
}
}
public class SecurityException extends Exception {}
with sharing keyword to enforce org-wide defaultsisAccessible(), isUpdateable(), isCreateable()field.isAccessible()Question: You get UNABLE_TO_LOCK_ROW errors during data loads. How would you design to avoid it?
Answer: Batch updates and reduce transaction overlap:
// ❌ BAD: Updates same record in multiple places
update account1;
doSomeLogic();
update account1; // UNABLE_TO_LOCK_ROW!
// ✅ GOOD: Collect updates and do once
List accountsToUpdate = new List();
accountsToUpdate.add(account1);
// ... do logic ...
accountsToUpdate.add(account2);
// Single update at the end
update accountsToUpdate;
// ✅ BETTER: Use batch processing for large operations
public class BatchAccountUpdater implements Database.Batchable {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([
SELECT Id, Name FROM Account
WHERE Type = 'Prospect'
ORDER BY Id // Important: consistent ordering
]);
}
public void execute(Database.BatchableContext bc, List accounts) {
// Small batches reduce lock contention
for (Account acc : accounts) {
acc.Type = 'Customer';
}
update accounts;
}
public void finish(Database.BatchableContext bc) {}
}
// Execute with smaller batch size to reduce lock time
Database.executeBatch(new BatchAccountUpdater(), 50);
Question: You need real-time vs async processing. When do you choose Future vs Queueable vs Platform Events?
Answer: Choose based on use case:
| Tool | Use Case | Best For |
|---|---|---|
| Synchronous | Real-time, user-facing | Form submissions, validation |
| @future | Simple async, non-chainable | API callouts, async updates |
| Queueable | Chainable async jobs | Multi-step async workflows |
| Platform Events | Real-time, decoupled processing | Microservices, integrations |
// @future: Fire-and-forget API callout
@future(callout=true)
public static void sendApprovalNotification(Set recordIds) {
// Make HTTP callout
}
// Queueable: Chainable async with error handling
public class ProcessOrdersQueueable implements Queueable {
private List orders;
public ProcessOrdersQueueable(List orders) {
this.orders = orders;
}
public void execute(QueueableContext ctx) {
// Process orders
for (Order__c order : orders) {
order.Status = 'Processed';
}
update orders;
// Chain next job if more work needed
if (orders.size() >= 100) {
System.enqueueJob(new SendEmailsQueueable(orders));
}
}
}
// Platform Events: Real-time, event-driven
trigger OrderTrigger on Order__c (after insert) {
List events = new List();
for (Order__c order : Trigger.new) {
events.add(new Order_Created__e(OrderId__c = order.Id));
}
EventBus.publish(events);
}
@future – Best for simple, fire-and-forget operationsQueueable – Best for chaining operations and complex workflowsPlatform Events – Best for real-time, decoupled microservice architectureBatch – Best for scheduled, large-scale operationsQuestion: An integration sends bad JSON sometimes. How do you handle robust error handling and parsing?
Answer: Use try/catch with type checking and validation:
public class JSONParser {
public static Map safeParseJSON(String jsonString) {
try {
if (String.isBlank(jsonString)) {
throw new JSONException('Empty JSON string');
}
Map parsed = (Map)
JSON.deserializeUntyped(jsonString);
return parsed;
} catch (JSONException je) {
System.debug('JSON Parse Error: ' + je.getMessage());
throw new DataParsingException('Failed to parse JSON: ' + je.getMessage());
} catch (Exception e) {
System.debug('Unexpected error: ' + e.getMessage());
logParseError(jsonString, e.getMessage());
throw new DataParsingException('Unexpected parsing error: ' + e.getMessage());
}
}
public static Account parseAccountFromJSON(String jsonString) {
Map data = safeParseJSON(jsonString);
Account acc = new Account();
// Safely extract fields with type checking
if (data.containsKey('name') && data.get('name') instanceof String) {
acc.Name = (String) data.get('name');
}
if (data.containsKey('phone') && data.get('phone') instanceof String) {
acc.Phone = (String) data.get('phone');
}
if (data.containsKey('revenue') && data.get('revenue') instanceof Decimal) {
acc.AnnualRevenue = (Decimal) data.get('revenue');
}
return acc;
}
private static void logParseError(String jsonData, String error) {
// Log to custom object or debug logs
System.debug(LoggingLevel.ERROR, 'Parse Error - Data: ' + jsonData + ', Error: ' + error);
}
}
public class DataParsingException extends Exception {}
instanceof to check types before castingcontainsKey() before accessing map valuesWhen preparing for Apex interviews, master these key competencies: