Skip to content
Salesforce Interview Q&A 19 min read 9 sections

Salesforce Apex Interview Questions and Answers

Apex is where a Salesforce developer interview stops being conversational. The technical round moves from the data model into code, and the panel wants to know whether your code has ever run against 200 records in one transaction. The thirty questions below are grouped the way these rounds usually m

RL

RizeX Labs

RizeX Labs

Published

Last updated

Related programme

Salesforce Training

Admin, Apex, Lightning Web Components and integrations in a live batch, with an industry project, mock interviews and a network of 270 hiring companies.

See the curriculum

3000+ learners · 270+ hiring partners

Apex is where a Salesforce developer interview stops being conversational. The technical round moves from the data model into code, and the panel wants to know whether your code has ever run against 200 records in one transaction. The thirty questions below are grouped the way these rounds usually move: governor limits and bulkification first, because that filter is applied early, then Apex language features, then test classes, then debugging and configuration data. Each answer is written the way a working developer answers it, with the code and the limit values you should be able to produce without looking them up.

Governor limits and bulkification

What are Governor Limits?

Governor limits are per-transaction caps Salesforce enforces on Apex so no single tenant consumes shared resources on a multitenant instance. Cross one and the runtime throws a System.LimitException, the transaction rolls back, and you cannot catch it in a try/catch. Counters reset each transaction, and asynchronous contexts get a fresh set with some limits raised. The values that actually get asked about:

Limit Synchronous Asynchronous
SOQL queries issued 100 200
Records retrieved by SOQL 50,000 50,000
Records from Database.getQueryLocator 10,000 10,000
SOSL queries issued 20 20
DML statements issued 150 150
Records processed by DML 10,000 10,000
CPU time 10,000 ms 60,000 ms
Total heap size 6 MB 12 MB
Callouts per transaction 100 100
Cumulative callout timeout 120 seconds 120 seconds
@future calls per transaction 50 0
Jobs added by System.enqueueJob 50 1
sendEmail calls 10 10
Trigger recursion stack depth 16 16

Know that the query count (100) and the query row count (50,000) are two different limits. Candidates mix them constantly.

How do you handle bulk data in Apex?

Write every piece of Apex assuming the input is a collection, because a trigger already receives up to 200 records per chunk and an integration sends far more. Query once outside the loop into a Map<Id, SObject>, build a Set<Id> so you query only the parents you need, and collect records into a List for one DML after the loop. Past that, move the work into Batch Apex, where each chunk gets its own limits.

What is Bulkification?

Bulkification is writing Apex that processes a collection with a fixed, small number of queries and DML statements, no matter how many records arrive.

The trainer's note here is worth repeating. Candidates define bulkification correctly, word for word, then write the loop version the moment they are asked to code it. They fail on demonstration, not on knowledge. Practise the map pattern by hand until it is the first thing your fingers produce.

The version that fails:

trigger OpportunityTrigger on Opportunity (after update) {
    for (Opportunity opp : Trigger.new) {
        // SOQL inside the loop
        Account acc = [SELECT Id, Total_Won_Amount__c
                       FROM Account
                       WHERE Id = :opp.AccountId];

        if (opp.StageName == 'Closed Won') {
            Decimal current = acc.Total_Won_Amount__c == null
                ? 0 : acc.Total_Won_Amount__c;
            acc.Total_Won_Amount__c = current + opp.Amount;
            update acc;   // DML inside the loop
        }
    }
}

With 200 records this breaks the query limit at record 101 and the DML limit at record 151. The same logic bulkified:

trigger OpportunityTrigger on Opportunity (after update) {
    Set<Id> accountIds = new Set<Id>();

    for (Opportunity opp : Trigger.new) {
        if (opp.StageName == 'Closed Won' && opp.AccountId != null) {
            accountIds.add(opp.AccountId);
        }
    }
    if (accountIds.isEmpty()) {
        return;
    }

    Map<Id, Account> accounts = new Map<Id, Account>([
        SELECT Id, Total_Won_Amount__c
        FROM Account
        WHERE Id IN :accountIds
    ]);

    for (Opportunity opp : Trigger.new) {
        Account acc = accounts.get(opp.AccountId);
        if (acc == null || opp.StageName != 'Closed Won') {
            continue;
        }
        Decimal current = acc.Total_Won_Amount__c == null
            ? 0 : acc.Total_Won_Amount__c;
        acc.Total_Won_Amount__c = current + opp.Amount;
    }

    update accounts.values();
}

One query and one DML statement, whether the trigger receives one record or two hundred.

How do you avoid SOQL in loops?

Pull the loop's filter values into a Set, run a single query with WHERE Id IN :theSet, and load the result into a Map keyed on the field you look up. Inside the loop, map.get(key) costs nothing against the limits. Use relationship queries so parent fields arrive in the same query, and the SOQL for-loop form on large result sets, since it processes records in batches of 200.

How do you prevent DML in loops?

Build a List during the loop and issue one insert or update after it closes. Check the list is not empty first, because an empty DML statement still counts against your 150. Where the logic touches several objects, keep one list per object and order the statements so parents are saved before children.

Explain Apex CPU time limit.

CPU time is the processing time your transaction spends on the Salesforce application servers: 10,000 milliseconds synchronous, 60,000 asynchronous. It counts computation, including loops, string handling and the work done by triggers, Flows and validation rules in the same transaction. It does not count time waiting on the database for SOQL and DML, or waiting on a callout. It is the hardest limit to fix late, because it is rarely one bad line. It is usually a nested loop over two collections, and the fix is a map lookup.

What is Apex Heap Size?

Heap is the memory your transaction holds at once, capped at 6 MB synchronous and 12 MB asynchronous. You blow it by querying too many fields on too many rows. Query only the fields you use, use the SOQL for-loop so records are handled in chunks, and mark Visualforce variables transient where they do not belong in view state.

Explain Apex Transaction Lifecycle.

A transaction begins at an entry point, runs all synchronous Apex that entry point triggers, and ends by committing everything or rolling everything back. Entry points include a DML operation from the UI or API, an @AuraEnabled method, a REST service, one execute chunk of a batch job, a @future or Queueable execution, and anonymous Apex. Three consequences to state:

  • Governor limits are scoped to the transaction and reset when a new one starts.
  • Static variables live for the transaction and are cleared afterwards, which is why they serve as recursion guards.
  • An uncaught exception rolls the whole transaction back, while an exception you catch leaves earlier DML in place unless you roll back to a savepoint.

What the interviewer is checking on limits and bulkification

  • Whether you can produce the map-and-set pattern from memory, not merely describe it.
  • Whether you know the query count and the query row count are separate limits.
  • Whether you understand that a trigger fires in chunks of 200 inside one transaction.
  • Whether asynchronous Apex is a limits decision for you, not a way to run things later.

Apex language features

What is the difference between a Trigger and a Class?

A trigger is not a class. It is code bound to an object and to specific DML events, invoked by the platform, with no constructor and no access modifiers. An Apex class is a normal type you instantiate and call from anywhere. In practice a trigger should hold almost no logic: one trigger per object, handing Trigger.new, Trigger.oldMap and the context to a handler class. For deployment, every trigger must also carry some coverage of its own.

What is the Static keyword in Apex?

A static member belongs to the class rather than an instance, and in Apex its lifetime is the current transaction, not shared across users or asynchronous boundaries. There is a trap interviewers like here. If Data Loader updates 10,000 Accounts in one call, the trigger fires in chunks of 200, but all those chunks run inside the same transaction, so a static boolean set to true in the first chunk silently skips the remaining 9,800 records. A static Set<Id> of processed record Ids is the safer guard.

Explain with sharing and without sharing.

with sharing makes the class respect the running user's record access: sharing rules, role hierarchy and organisation-wide defaults. without sharing runs in system mode and sees every record. inherited sharing takes the mode of its caller and defaults to with sharing when the class is the entry point, which makes it right for reusable service classes.

The detail that marks a senior answer: sharing keywords control record access only, not object permissions or field-level security. For those, use WITH USER_MODE in SOQL, Security.stripInaccessible(), or AccessLevel.USER_MODE on Database operations.

What are Apex Interfaces?

An interface is a contract of method signatures with no implementation and no state. An implementing class must provide every method, and a class can implement several, which is how the platform works around single inheritance. Most platform features are interfaces: Database.Batchable, Queueable, Schedulable, HttpCalloutMock and Comparable. In your own code they let you swap a live integration service for a stub.

What are Apex Abstract classes?

An abstract class is a partial implementation. It can hold instance variables, constructors and concrete methods alongside abstract ones, it cannot be instantiated, and a class can extend only one. Choose it when subclasses share real behaviour you do not want to repeat, and an interface when you only need to guarantee a shape. Apex also requires a method to be marked virtual or abstract before a subclass can override it, and the subclass must use override.

How do you handle exceptions in Apex?

Wrap the risky operation in try/catch/finally and catch the specific type before the generic one: DmlException, QueryException, CalloutException, NullPointerException. The Exception class gives you getMessage(), getStackTraceString(), getLineNumber(), and for DML failures getNumDml() and getDmlMessage(i).

Two things candidates miss. System.LimitException cannot be caught, so catch (Exception e) will not save you from a governor limit. And in a trigger the correct way to reject a record is record.addError('message') rather than throwing, because addError blocks only that record and shows a clean message on the page.

What is a Custom Exception?

A user-defined class that extends Exception. The name must end in Exception, and it inherits the standard constructors automatically, so public class InvoicePostingException extends Exception {} is a complete declaration. You can add member variables to carry context such as a record Id, and a caller can then catch your business failure specifically while everything else bubbles up. Use them for rule violations in service classes, then translate them into addError() at the trigger boundary.

What is SObject cloning?

clone() on an sObject creates a copy in memory. It takes up to four arguments: whether to preserve the Id, whether to deep clone, whether to preserve read-only timestamps such as CreatedDate, and whether to preserve auto-number values. Pass them explicitly rather than relying on defaults, because code that assumes the Id behaviour is the code that breaks when a clone is inserted and turns into an update of the original record. A plain clone is shallow; deepClone() also copies child relationship records included in the query.

What is the Transient keyword?

transient marks an instance variable that is not saved and not carried in the Visualforce view state, which makes it the main tool for staying under the view state size limit on heavy pages. It also excludes the variable when the object is serialized, which matters for state carried across a Database.Stateful batch or through JSON.serialize().

What is Dynamic Apex?

Dynamic Apex works out object and field names at runtime instead of hard-coding them. The building blocks are describe calls such as Schema.getGlobalDescribe() and DescribeFieldResult, dynamic SOQL through Database.query(), and generic SObject handling with put(), get() and newSObject(). The security point earns the marks: never concatenate user input into a query string. Use bind variables, or String.escapeSingleQuotes() where a bind is not possible, and run the query in user mode so field permissions are enforced instead of bypassed.

What the interviewer is checking on language features

  • Whether you can say why the logic belongs in a handler class rather than the trigger body.
  • Whether you know static state survives across the 200-record chunks of one DML call.
  • Whether you know sharing keywords do nothing for field-level security.
  • Whether you use addError() in triggers instead of throwing raw exceptions at users.

Testing

What is Test.startTest()?

Test.startTest() marks the start of the code actually under test. Everything before it is setup, and the limits that setup consumes are counted separately. When Test.startTest() executes, the block that follows receives a fresh set of governor limits. Each test method may call it once. It does not create a new transaction and it does not change the running user, so inserting two hundred setup records no longer eats the budget you need for the assertion.

What is Test.stopTest()?

Test.stopTest() closes the block and restores the limit context that was in place before Test.startTest() was called.

The behaviour interviewers probe is asynchronous execution. Any @future method, Queueable job, Batch Apex job or scheduled job enqueued between startTest and stopTest runs synchronously at the point Test.stopTest() executes. That is why assertions go after stopTest(), and it is the most common reason a test that "should work" reports zero results. For a batch job enqueued inside the block, only one execute() call runs, so assert against the first chunk rather than the full data set.

@isTest
static void rollupRunsForAllAccounts() {
    List<Account> accounts = TestDataFactory.buildAccounts(200);
    insert accounts;                  // setup, own limits

    Test.startTest();
    System.enqueueJob(new AccountRollupQueueable());
    Test.stopTest();                  // the job runs here

    Integer done = [SELECT COUNT() FROM Account WHERE Rollup_Done__c = true];
    Assert.areEqual(200, done, 'Every account should be rolled up');
}

This is where the trainer's point about syntax against execution shows up. Candidates write Test.startTest() from memory and still cannot say when the queued job runs or which limit set it runs under.

How do you write effective test classes?

Coverage is a deployment gate, not a goal. You need 75% org-wide coverage with all tests passing, and every trigger needs coverage, but a test with no assertion is worthless whatever percentage it produces. A good test class:

  • Creates its own data, through a TestDataFactory and a @TestSetup method that runs once per class.
  • Asserts on outcomes with Assert.areEqual() or System.assertEquals(), with a message saying what failed.
  • Covers the bulk case with 200 records, not one.
  • Covers the negative case: bad input, a missing required field, a validation rule firing.
  • Uses System.runAs() to test sharing and permission behaviour.
  • Never hard-codes record Ids and never depends on org data.

What is SeeAllData=false?

It is the default isolation mode for test classes on API version 24.0 and later. Test methods cannot see records that already exist in the org and see only the data they create, which makes tests repeatable across a sandbox, a scratch org and production. Some objects stay visible regardless, because tests could not run otherwise: User, Profile, Organization, RecordType, ApexClass, static resources and custom metadata type records.

Two details worth knowing. Custom settings records are ordinary data and are not visible by default, so your test must create them. And if the class is annotated @isTest(SeeAllData=true), marking an individual method SeeAllData=false does not restore isolation, because the class-level setting wins. The reverse works, so a method can opt in when the class is isolated.

How do you mock callouts?

Apex cannot make a real HTTP callout inside a test. You supply a mock implementation and register it with Test.setMock() before the callout runs, typically just inside Test.startTest(). The options are HttpCalloutMock for callouts written in Apex, StaticResourceCalloutMock and MultiStaticResourceCalloutMock when the response body is stored as a static resource, and WebServiceMock for callouts generated from a WSDL.

What is HttpCalloutMock?

An interface with one method, HttpResponse respond(HttpRequest req). You implement it in a test class, build an HttpResponse with the body and status code you want, and return it. Because you receive the request, the same method can assert that your code sent the right HTTP method, endpoint and body. Register it with Test.setMock(HttpCalloutMock.class, new CreditApiMock());, then add a second mock returning 500 so the error branch is covered.

What the interviewer is checking on testing

  • Whether your assertions sit after Test.stopTest() when asynchronous code is involved.
  • Whether you can explain what the fresh limit set inside the block is for.
  • Whether you create your own data instead of reaching for SeeAllData=true.
  • Whether your tests cover 200 records and the failure path.

Debugging and data configuration

What is the difference between Database.insert and insert?

The insert statement is all or nothing. If one record in the list fails, a DmlException is thrown and nothing in that statement is saved.

Database.insert(records, allOrNone) gives you control. With true it behaves like the DML statement. With false it allows partial success: valid records are saved, invalid ones are reported back, and no exception is thrown for row-level failures.

Database.SaveResult[] results = Database.insert(newLeads, false);

for (Integer i = 0; i < results.size(); i++) {
    if (!results[i].isSuccess()) {
        for (Database.Error err : results[i].getErrors()) {
            System.debug(err.getStatusCode() + ': ' + err.getMessage()
                         + ' on ' + err.getFields());
        }
    }
}

Points to state clearly. The results list is in the same order as the input list, which is how you map a failure back to its record. Because nothing is thrown, silence means nothing, so you must inspect the results or the errors vanish. allOrNone = false does not skip validation rules or required fields, it only isolates the failure to that row. And the call is still one DML statement, with every row counting toward the 10,000 row limit.

What is Partial Success in DML?

Partial success is the outcome of a Database method run with allOrNone set to false. Good rows commit, bad rows come back as errors, and the transaction continues. Use it when the records are independent, for example an integration loading a thousand leads where twelve have a bad country code. Rejecting 988 good records because of twelve bad ones is the wrong business answer. Do not use it where records depend on each other, such as an order and its line items. And note that it covers row-level failures only: if the transaction later throws an uncaught exception, everything already saved rolls back with it.

How do you debug production issues?

Start from the symptom and narrow down rather than reading code and guessing. This question separates people who have supported an org from people who have only built in a sandbox. The trainer puts it bluntly: "your trigger is not firing, what do you check" tells a panel more in thirty seconds than any definition question.

A workable order:

  1. Reproduce it and identify the exact user, record and time.
  2. Set a trace flag on that user in Setup, reproduce again, and read the debug log. Raise APEX_CODE to FINEST when you need variable values.
  3. Read the log for the entry point, the limit counters in the cumulative section, and the first exception.
  4. Check whether the cause is code at all. Validation rules, Flows and duplicate rules appear in the same log.
  5. Check Setup for failed Apex jobs, and the Apex exception email for the class name and line number.

Two practical constraints. A single debug log is truncated past 20 MB, so narrow your log levels instead of setting everything to FINEST. And writing an error row to a custom object from a catch block fails when the transaction rolls back, because the log row rolls back with it. A platform event configured to publish immediately survives the rollback.

What is Apex Replay Debugger?

A debugger in Visual Studio Code, part of the Salesforce Extension Pack, that replays a debug log as though you were stepping through the code live. You set the trace flag, reproduce the problem, download the log, then set breakpoints and step through it inspecting variable values. It is a replay, not a live session, so you cannot change values or alter the path. The live equivalent is the Apex Interactive Debugger, a paid feature that works in sandboxes. Replay Debugger matters for production work because you can pull a production log and step through it locally.

What is Custom Metadata versus Custom Settings?

Both hold configuration outside your code. The difference shows at deployment time and at runtime.

Custom Metadata Types Custom Settings
Records deploy between orgs Yes, records are metadata No, only the definition
Packageable records Yes No
Editable in Apex at runtime No, read only Yes, standard DML
SOQL limit cost Queries do not count against SOQL limits getInstance() reads come from cache
Per-user or per-profile values No Yes, with the Hierarchy type
Relationships to other records Yes No

Choose custom metadata for configuration that belongs with the code and should travel through your release pipeline, such as integration endpoints or a trigger framework's switches. Choose hierarchy custom settings when a value must differ per user or per profile, or when running code must write it back.

What is Platform Encryption?

Shield Platform Encryption encrypts data at rest in the database, in files and attachments, and in the search index, using keys derived from a tenant secret the customer controls. Users without the "View Encrypted Data" permission see masked values. It is not the old Classic Encryption, which was a separate field type limited to 175 characters.

For a developer the constraints matter most. Probabilistic encrypted fields cannot be filtered, sorted or grouped in SOQL, and cannot be used in formula fields or criteria-based sharing rules. Deterministic encryption supports exact-match filtering. Encrypted fields cannot be external Ids or unique fields.

What the interviewer is checking on debugging and configuration

  • Whether you inspect Database.SaveResult or call the method and hope.
  • Whether you can name a case where partial success is the wrong choice.
  • Whether your debugging starts with a trace flag and a log rather than a guess.
  • Whether you know custom metadata records deploy and custom settings data does not.

Preparing these answers properly

The pattern across all thirty questions is the same. The definition earns you nothing on its own. What earns the offer is producing the bulkified version on a whiteboard, saying exactly when a queued job runs in a test, and describing the last production bug you traced through a debug log. Work through this material with real code in a developer org rather than by reading it. If you want that practice structured, with assignments reviewed and mock interviews on these exact questions, look at the Salesforce training in Pune with placement support at RizeX Labs.

Last updated 25 September 2026