Apex Interview Questions – Scenario-Based & Technical

This guide covers commonly asked Apex interview questions with practical, real-world scenarios. Prepare with both technical fundamentals and hands-on problem solving.

1. Scenario: Handling Bulk Operations in Triggers

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;
}
  

Key Points

2. Scenario: Preventing Trigger Recursion

Question: 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);
}
  

Key Points

3. Scenario: Handling API Callouts with Error Management

Question: 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);
    }
}
  

Key Points

4. Scenario: Testing with Proper Test Data Setup

Question: 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');
    }
}
  

Key Points

5. Scenario: Asynchronous Processing with Queueable

Question: 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:


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));
  

Key Points

6. Scenario: SOQL Query Optimization

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
];
  

Key Points

7. Scenario: Avoiding SOQL Injection

Question: 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
}
  

Key Points

8. Scenario: Governor Limits – CPU Time

Question: 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
    }
}
  

Summary

When preparing for Apex interviews, focus on:

Advanced Scenarios

9. Scenario: API Callouts in Triggers – The Right Way

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
    }
}
  

Key Points

10. Scenario: Batch Job Failures with Partial Recovery

Question: 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);
  

Key Points

11. Scenario: Optimizing Slow SOQL Queries

Question: 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;
}
  

Key Points

12. Scenario: Batch vs Queueable for Large Datasets

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);
  

Key Points

13. Scenario: Preventing Trigger Recursion with Tracking

Question: 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
    }
}
  

Key Points

14. Scenario: Field-Level Security (FLS) and CRUD Checks

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 {}
  

Key Points

15. Scenario: Handling Row Locks (UNABLE_TO_LOCK_ROW)

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);
  

Key Points

16. Scenario: Real-Time vs Async Processing

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);
}
  

Key Points

17. Scenario: Robust JSON Parsing with Error Handling

Question: 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 {}
  

Key Points

Final Summary

When preparing for Apex interviews, master these key competencies: