TL;DR
- Most Apex interviews test three things: whether your code survives 200 records at once, whether it respects the running user's access, and whether you can debug a governor-limit failure.
- Learn the limits that actually bite: 100 SOQL queries (200 async), 150 DML statements, 50,000 query rows, 10,000 DML rows, 10 s CPU (60 s async) and 6 MB heap (12 MB async).
- For every async option, know what it can't do: future methods take only primitives, Queueable chains one child job, and Batch is the answer for millions of rows.
These are the Apex questions that come up in Salesforce developer interviews in 2026, with answers that explain why, because that's what the follow-up question tests. They're grouped by level. Fundamentals covers what every developer role expects. Intermediate and Scenario-based are where mid-level interviews are won or lost. Senior / design is about structure and trade-offs.
Answer each question out loud before you read mine. The code samples follow the conventions a reviewer expects today: bulkified, no SOQL or DML inside loops, with sharing by default and user-mode queries where a user's access matters.
Fundamentals
What's the difference between with sharing, without sharing and inherited sharing?
These keywords control record-level access: whether sharing rules, role hierarchy and manual shares apply to the queries and DML in that class.
with sharingenforces the running user's record access.without sharingignores it. Use it deliberately, for example when a rollup must count records the user can't see, and comment why.inherited sharingtakes the mode of the calling class. When the class is the entry point, such as an@AuraEnabledcontroller, it runs aswith sharing. That makes it the safe choice for utility classes.
A class with no keyword usually runs in the caller's mode, but as an entry point you shouldn't rely on that default, which is why inherited sharing exists.
The follow-up trap: none of these keywords enforce object permissions or field-level security. That's a separate job.
What the interviewer is really checking: that you separate record sharing from CRUD and FLS.
How do you enforce object and field permissions in Apex?
Apex runs in system mode by default, so it ignores the running user's object (CRUD) and field-level (FLS) permissions unless you ask for them. Today there are three main tools:
- User-mode database operations. Add
WITH USER_MODEto SOQL, and useinsert as userorDatabase.insert(records, AccessLevel.USER_MODE)for DML. User mode enforces CRUD, FLS and sharing for that operation. Security.stripInaccessibleremoves fields the user can't access from a list of records, instead of throwing. That's useful when you want to return what the user may see and silently drop the rest.WITH SECURITY_ENFORCEDis the older SOQL clause. It only checks fields in the SELECT and FROM clauses and stops at the first problem. PreferWITH USER_MODEfor new code.
List<Account> accounts = [SELECT Id, Name, AnnualRevenue FROM Account WHERE Industry = :industry WITH USER_MODE];
insert as user newContacts;
Which governor limits do you hit most, and why do they exist?
Salesforce is multi-tenant: your code shares servers and databases with other orgs, so every transaction gets a budget. The ones that bite in real projects:
| Limit (per transaction) | Synchronous | Asynchronous |
|---|---|---|
| SOQL queries | 100 | 200 |
| Records retrieved by SOQL | 50,000 | 50,000 |
| DML statements | 150 | 150 |
| Records processed by DML | 10,000 | 10,000 |
| CPU time | 10 seconds | 60 seconds |
| Heap size | 6 MB | 12 MB |
The point isn't reciting the table. It's knowing that limits apply to the whole transaction: your trigger, the flows on the same object, the managed package triggers and everything they call share one budget. Use the Limits class (Limits.getQueries(), Limits.getCpuTime()) to measure where it goes.
What does bulkification mean? Show me a bulkified example.
Bulkified code works the same whether it receives 1 record or 200, because it does a fixed number of queries and DML statements regardless of how many records come in. In practice: collect Ids in a loop, query once with IN, put the results in a map, then loop again and look up.
public with sharing class ContactRegionService {
public static void copyRegionFromAccount(List<Contact> contacts) {
Set<Id> accountIds = new Set<Id>();
for (Contact c : contacts) {
if (c.AccountId != null) {
accountIds.add(c.AccountId);
}
}
Map<Id, Account> accounts = new Map<Id, Account>(
[SELECT Id, Region__c FROM Account WHERE Id IN :accountIds]
);
for (Contact c : contacts) {
Account parent = accounts.get(c.AccountId);
if (parent != null) {
c.Region__c = parent.Region__c;
}
}
}
}
Called from a before-insert trigger, this uses one query and no DML for any number of contacts. I wrote a whole post on Apex bulkification.
In Apex, is "Acme" == "acme" true?
Yes. The == operator on strings in Apex is case-insensitive, which surprises people from Java or JavaScript. Use equals() when case matters:
String a = 'Acme';
String b = 'acme';
Boolean loose = (a == b); // true
Boolean strict = a.equals(b); // false
It matters in real code. A map keyed by String is case-sensitive, so map.get('acme') won't find a key stored as 'Acme', even though == says they're equal. When you build lookup keys from user input or email addresses, normalise them with toLowerCase() first.
What the interviewer is really checking: whether you've been bitten by this, which usually means you've written real Apex.
How long does a static variable live in Apex?
For one transaction. A static variable is initialised when its class is first used in the transaction and is gone when the transaction ends. It isn't shared between users, requests or transactions.
That makes statics useful for caching data within a transaction (query a custom metadata record once, not in every trigger invocation) and for recursion guards in triggers.
Two traps. A Batch Apex job runs each execute call as its own transaction, so a static set in one batch doesn't survive to the next. Database.Stateful keeps instance variables between batches, never statics. And because all 200-record chunks of one DML operation run in the same transaction, a static Boolean "already ran" flag will silently skip every chunk after the first.
What's the difference between DML statements and Database class methods?
insert records; is all-or-nothing: if one record fails, the statement throws a DmlException and none of the records are saved. Database.insert(records, false) allows partial success. Valid records are saved, and you get a SaveResult for each record to inspect.
List<Database.SaveResult> results = Database.insert(contacts, false);
for (Integer i = 0; i < results.size(); i++) {
if (!results[i].isSuccess()) {
for (Database.Error err : results[i].getErrors()) {
System.debug(contacts[i].LastName + ': ' + err.getMessage());
}
}
}
Results come back in the same order as the input list, which is how you match a failure to its record. Use partial success for imports and integrations, where one bad row shouldn't block 199 good ones, and all-or-nothing when the records only make sense together.
Intermediate
When do you use a future method, Queueable, Batch or Schedulable Apex?
@future: fire-and-forget work, often a callout from a trigger. Static, returns void, takes only primitives or collections of primitives. No job Id to monitor, and you can't call one future method from another.- Queueable: the modern default for async work. Accepts complex types including sObjects, returns a job Id, supports chaining (one child job from an executing job) and transaction finalizers.
- Batch Apex: large volumes.
startreturns a query locator that can cover up to 50 million records, andexecuteprocesses them in chunks (200 by default, up to 2,000), each in its own transaction with fresh limits. - Schedulable: runs at a time you set with a cron expression. It usually just starts a Batch or Queueable job.
What the interviewer is really checking: that you pick by data volume and what the job needs, not by habit. "Queueable unless the data is too big, then Batch" is a good default.
Why can't you pass an sObject to a future method?
Because a future method runs later, in a separate transaction. By the time it runs, the record may have been changed or deleted, and a serialised copy would carry stale values. Salesforce forces you to pass Ids and re-query in the future method, so you always work with the current data.
public with sharing class AccountSync {
@future(callout=true)
public static void sendToErp(Set<Id> accountIds) {
List<Account> accounts = [SELECT Id, Name, BillingCountry FROM Account WHERE Id IN :accountIds];
// build the request and call the ERP
}
}
If you genuinely need to pass complex data, use a Queueable, which accepts sObjects and custom types as member variables. Then the trade-off is yours to manage.
What is a mixed DML error, and how do you avoid it?
You get MIXED_DML_OPERATION when one transaction does DML on a setup object, such as User, Group, GroupMember, PermissionSetAssignment or UserRole, and a non-setup object, such as Account or a custom object. Salesforce separates them because setup changes can affect sharing and access for the records you're changing in the same transaction.
The fix is to move one side into its own transaction, usually with a Queueable:
public with sharing class AssignPermissionSetJob implements Queueable {
private final Set<Id> userIds;
private final Id permissionSetId;
public AssignPermissionSetJob(Set<Id> userIds, Id permissionSetId) {
this.userIds = userIds;
this.permissionSetId = permissionSetId;
}
public void execute(QueueableContext context) {
List<PermissionSetAssignment> assignments = new List<PermissionSetAssignment>();
for (Id userId : userIds) {
assignments.add(new PermissionSetAssignment(AssigneeId = userId, PermissionSetId = permissionSetId));
}
insert assignments;
}
}
In test classes, wrap the setup-object DML in System.runAs() instead.
Can you catch a governor limit exception?
No. System.LimitException can't be caught. When a limit is exceeded, the transaction stops and rolls back, and no catch block runs, not even catch (Exception e).
So you can't handle limits after the fact. You prevent them: check usage before an expensive operation with the Limits class, and move work to async processing when you're close to the edge.
if (Limits.getQueries() < Limits.getLimitQueries() - 5) {
// safe to run another query
} else {
System.enqueueJob(new DeferredEnrichmentJob(recordIds));
}
Everything else is catchable, and you should be specific: catch DmlException or QueryException where you can do something useful, and throw your own exception classes (any class extending Exception) so the caller knows what went wrong. For LWC and Aura callers, throw AuraHandledException with a message that's safe to show the user.
What do Test.startTest() and Test.stopTest() actually do?
Two things. The code between them gets a fresh set of governor limits, so the limits your test data setup used don't count against the code you're testing. And any asynchronous work enqueued between them, such as future methods, Queueable jobs or a batch, runs synchronously when Test.stopTest() is called, so you can assert on its results on the next line.
@isTest
private class InvoiceSyncJobTest {
@testSetup
static void setup() {
insert new Account(Name = 'Acme');
}
@isTest
static void stampsSyncTime() {
Test.setMock(HttpCalloutMock.class, new InvoiceApiMock());
Test.startTest();
System.enqueueJob(new InvoiceSyncJob());
Test.stopTest();
Account acc = [SELECT Last_Synced__c FROM Account LIMIT 1];
Assert.isNotNull(acc.Last_Synced__c, 'The job should stamp the sync time');
}
}
Without stopTest(), the job would never run in the test and the assert would fail. More on this in Apex tests beyond 75% coverage.
Why do you get "You have uncommitted work pending" when making a callout?
Because you did DML earlier in the same transaction and then tried an HTTP callout. Salesforce doesn't allow a callout after uncommitted DML. A callout can take seconds, and holding database locks open while waiting on an external system would block other users.
The fixes, in the order I'd suggest them:
- Reorder. Make the callout first, then do the DML with the response.
- Split the transaction. Do the DML now and the callout in a Queueable that implements
Database.AllowsCallouts, or a@future(callout=true)method. - Rethink the design if the callout must happen after a trigger. Triggers can't make synchronous callouts at all, so it has to be async anyway, or published as a platform event for a subscriber to handle.
What the interviewer is really checking: that you understand transactions and locks, not just the workaround.
Scenario-based
Your code throws "Too many SOQL queries: 101" but there's no query inside a loop. Where do you look?
The query is in a loop, just not one you can see in that method. The usual suspects:
- A method called inside a loop that queries internally, such as a helper like
getAccountTier(accountId)called per record. - Trigger recursion. Your trigger updates records, which fires the trigger again, and every pass adds queries.
- Other automation on the same object. Record-triggered flows, other triggers and managed packages share the same 100-query budget.
- Repeated chunks. A DML on 1,000 records fires the trigger five times in one transaction. Five queries per chunk adds up fast.
I'd open the debug log, filter on SOQL_EXECUTE_BEGIN and look at the stack of what called each query, then use Limits.getQueries() at entry and exit points to see which component spends the budget.
How do you debug "Apex CPU time limit exceeded"?
CPU time counts the time your code spends computing on Salesforce's servers. Time spent waiting for the database during SOQL and DML, and waiting on callouts, doesn't count. So this error is about what your code does in memory.
What I look for, roughly in this order:
- Nested loops over large lists, such as matching contacts to accounts by looping over both. Replace the inner loop with a map lookup.
- Too much automation on one object. Triggers, flows and packages all run in the same transaction and share the 10-second budget.
- Recursion that processes the same records several times.
- Expensive work repeated per record, such as describe calls, JSON serialisation or string building inside a loop.
The debug log shows CPU time used, and Limits.getCpuTime() placed around suspect blocks tells you where it goes. If the work is legitimately heavy, move it to Queueable or Batch Apex, which get 60 seconds.
You need to process 2 million records every night. How do you design it?
Batch Apex, started by a Schedulable class. start returns a Database.QueryLocator, which can cover up to 50 million rows. Each execute gets a chunk, 200 by default, in its own transaction with fresh limits, so one bad chunk doesn't roll back the others.
public with sharing class StaleCaseCloser implements Database.Batchable<SObject>, Database.Stateful {
private Integer closedCount = 0;
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([
SELECT Id, Status FROM Case
WHERE Status = 'Waiting on Customer' AND LastModifiedDate < LAST_N_DAYS:30
]);
}
public void execute(Database.BatchableContext bc, List<Case> scope) {
for (Case c : scope) {
c.Status = 'Closed';
}
for (Database.SaveResult result : Database.update(scope, false)) {
if (result.isSuccess()) {
closedCount++;
}
}
}
public void finish(Database.BatchableContext bc) {
System.debug(closedCount + ' cases closed');
}
}
Design points to mention: make it idempotent so a rerun is safe, use partial-success DML and log failures, tune the scope size if triggers on the object are heavy, keep the start filter selective, and use Database.Stateful only for small counters, because the state is serialised between chunks.
Your code fails with "Apex heap size too large". What do you change?
The heap is the memory your transaction holds: 6 MB synchronous, 12 MB asynchronous. The usual cause is querying a large list, often with long text fields, and keeping all of it in memory.
Fixes:
- Use a SOQL for loop. It retrieves records in batches of 200 and lets each batch be released as you go, instead of loading the whole result into one list.
- Select fewer fields. Long text areas and rich text fields are expensive.
- Clear what you're done with. Set big collections to null, or call
clear(), once processed. - Move to Batch Apex when the volume is genuinely large.
for (List<Account> chunk : [SELECT Id, Name FROM Account WHERE Industry = 'Energy']) {
AccountScoring.score(chunk);
}
Watch out for doing DML inside that loop, though. One update per chunk of 200 is fine for a few thousand rows but will hit the 150 DML limit on a big table.
A Queueable job must retry when it fails. How do you build that?
Use a transaction finalizer. Inside the Queueable's execute, call System.attachFinalizer() with a class that implements System.Finalizer. The finalizer runs after the job finishes, whether it succeeded or threw an unhandled exception, and it can inspect the result and enqueue the job again.
public with sharing class SyncRetryFinalizer implements System.Finalizer {
private final Set<Id> recordIds;
private final Integer attempt;
public SyncRetryFinalizer(Set<Id> recordIds, Integer attempt) {
this.recordIds = recordIds;
this.attempt = attempt;
}
public void execute(System.FinalizerContext context) {
if (context.getResult() == System.ParentJobResult.UNHANDLED_EXCEPTION && attempt < 3) {
System.enqueueJob(new OrderSyncJob(recordIds, attempt + 1));
}
}
}
The job attaches it at the start of execute. Cap the retries yourself, log the final failure somewhere an admin will see it, and make the job idempotent so a retry after a partial success doesn't create duplicates.
Senior / design
How do you structure Apex in a large org?
In layers, so each class has one reason to change:
- Triggers contain no logic. They call a handler.
- Services hold business processes, such as "convert this quote to an order". Controllers, triggers, batch jobs and REST endpoints all call the same service, so the logic exists once.
- Selectors own the queries for an object, so field lists and security settings are consistent and easy to find.
- Domain classes hold logic that belongs to one object's records, such as defaulting and validation.
- A unit of work collects DML and commits it in one place, in the right order.
This is the Apex Enterprise Patterns approach (the fflib library is its best-known implementation). You don't need the full library to use the idea. For a small org, a trigger handler plus a service layer already solves most problems.
What the interviewer is really checking: that you can explain why the layers exist, not just name them.
How do you make Apex testable without hitting the database or real services?
Depend on interfaces, not concrete classes, and inject the implementation. In production the service uses the real selector or HTTP client. In tests you pass a fake that returns exactly the data the test needs.
public interface ExchangeRateProvider {
Decimal rateFor(String currencyCode);
}
public with sharing class PriceConverter {
private final ExchangeRateProvider rates;
public PriceConverter(ExchangeRateProvider rates) {
this.rates = rates;
}
public Decimal toUsd(Decimal amount, String currencyCode) {
return amount * rates.rateFor(currencyCode);
}
}
Apex also has a built-in Stub API (System.StubProvider with Test.createStub()) for mocking classes without writing fakes by hand. For HTTP callouts, use HttpCalloutMock.
What to avoid: Test.isRunningTest() branches in production code. They mean the code you test isn't the code you ship.
When would you use platform events instead of calling a Queueable directly?
When the publisher shouldn't know or care who reacts. A Queueable is a direct call to one known piece of code. A platform event is a message that any number of subscribers can handle: an Apex trigger on the event, a flow, or an external system through the Pub/Sub API.
Two details interviewers like:
- Publish behaviour. An event set to Publish After Commit is only delivered if the transaction commits, which is what you want for business events. Publish Immediately delivers even if the transaction later rolls back, which suits logging errors from a transaction that's about to fail.
- Subscribers run separately. A platform event trigger runs in its own transaction and processes events in batches, so an error there doesn't roll back the publisher. You need your own retry and error handling.
Use events for decoupling and integration, and a Queueable when you just need to move your own work out of the current transaction.
How do savepoints and rollbacks work, and what's the trap?
Database.setSavepoint() marks a point in the transaction, and Database.rollback(sp) undoes all DML after it. That's how you make a multi-step operation all-or-nothing when you use partial-success DML or catch exceptions.
Savepoint sp = Database.setSavepoint();
try {
insert order;
insert orderLines;
} catch (DmlException e) {
Database.rollback(sp);
throw new OrderException('Order could not be saved: ' + e.getMessage());
}
The traps:
- Ids aren't cleared. If
orderwas inserted before the rollback, the variable still holds its Id. Inserting it again fails, so create a fresh instance or clear the Id. - They cost DML. Setting a savepoint and rolling back each count against the 150 DML statement limit.
- Rollback doesn't undo everything. It undoes database changes, not static variables you changed along the way.
How to practise these Apex questions out loud
Apex interviews reward people who have broken things and fixed them. Here's how I'd use this page:
- Answer before you read. Say your answer out loud, then compare it with mine. Mark the questions where you hesitated.
- Reproduce the errors. In a Developer Edition org, trigger a mixed DML error, an uncommitted-work callout error and a 101-query error on purpose. Then fix each one. You'll remember the cause for good.
- Write without autocomplete. Write a bulkified handler, a Queueable and a test class on a blank page, then paste them into your org and see what breaks.
- Practise the three-part answer: what it is, why it works that way, and where you used it.
- Time yourself. Two minutes per answer is plenty. If you run longer, you're explaining things you weren't asked.
If you want to see these patterns built from scratch, Salesforce Development (Visualforce & Apex) builds them live in a real org and covers SOQL, DML, collections, async Apex and the test framework, and the Interview Questions course works through common questions. The Lifetime plan also includes the 700+ page Interview Questions Book.
Related pages
- Learn Apex: the full learning path, in order.
- Salesforce developer roadmap: where Apex fits among LWC, Flow and integration.
- Apex bulkification explained: the pattern behind half of these answers.
- Apex test classes beyond 75% coverage: what good tests look like.
- Apex trigger interview questions and SOQL interview questions: the next two rounds of the same interview.
Want 700+ pages of this?
The Salesforce Interview Questions & Answers Book goes far deeper than this page — it's included with the Lifetime plan, alongside the Real-Time Projects series.
Frequently asked questions
What does a typical Apex technical round look like?
Usually a mix of concept questions (sharing, limits, async), a short piece of code to write or review, and one or two debugging scenarios such as a limit exception during a data load. Senior rounds add design questions about layering and testability.
Will I have to write Apex without an IDE?
Often, yes. Many interviews use a shared text editor or a whiteboard. Practise writing a bulkified trigger handler and a test class from a blank page so you don't depend on autocomplete.
Do interviewers care about Apex certification?
A Platform Developer certification helps you get shortlisted, but the technical round tests whether you can reason about real code. Expect follow-up questions that go well past the exam objectives.
How should I prepare if I've only written small amounts of Apex?
Build one small, complete feature end to end in a Developer Edition org: a trigger, a service class, an async job and tests. Interviewers notice the difference between someone who has read about Apex and someone who has debugged it.
How many Apex questions should I prepare?
Depth beats count. Being able to explain twenty core questions properly, including the follow-up 'why', is worth more than memorising a hundred one-line answers.
Keep going
Topic guide
Learn Apex: A Hands-On Salesforce Apex Course for Developers
Career roadmap
Salesforce Developer Roadmap 2026
Blog
Apex Bulkification Explained: From 'Too Many SOQL Queries: 101' to Bulk-Safe Code
Blog
Apex Test Classes: Stop Chasing 75% Coverage
Interview questions
Apex Trigger Interview Questions and Answers (2026)
Interview questions
SOQL and SOSL Interview Questions and Answers (2026)