Apex Interview Questions with Answers
Most Asked Apex Interview Questions for Software Engineer Roles
Introduction
This page contains a carefully selected collection of Apex Interview Questions and Answers designed for students, Salesforce beginners, software developers, and experienced professionals preparing for Salesforce developer interviews. The questions cover both theoretical concepts and practical programming examples commonly asked during technical interviews. Apex is Salesforce's strongly typed, object-oriented programming language used to build business logic on the Salesforce Platform. It enables developers to create triggers, classes, asynchronous jobs, REST services, integrations, and custom automation while working securely with Salesforce data. This interview guide covers beginner, intermediate, advanced, and scenario-based Apex interview questions including Apex syntax, triggers, SOQL, SOSL, governor limits, collections, asynchronous Apex, exception handling, testing, batch processing, integrations, and best practices followed by Salesforce developers.
Why Apex?
- Purpose-built for the Salesforce platform – tightly integrated with CRM data and metadata
- Strongly typed and object-oriented – ensures reliability and maintainability
- Built-in governor limits – enforces efficient, scalable code in multi-tenant environments
- Full support for CRUD, SOQL, SOSL, and DML operations – seamless data manipulation
- Asynchronous capabilities – batch jobs, queueable, and scheduled Apex for background processing
- Robust testing framework – built-in support for test classes and code coverage
- Highly demanded skill – essential for Salesforce developers and architects worldwide
Most Asked Apex Interview Questions
Apex is a strongly typed, object-oriented programming language developed by Salesforce for building enterprise applications on the Salesforce Platform. It runs entirely on the Salesforce cloud and is used to implement business logic, automate processes, and integrate with external systems.
- Object-oriented – supports classes, interfaces, inheritance
- Strongly typed – compile-time type checking
- Database integration – native SOQL and DML operations
- Multitenant aware – designed for Salesforce's shared environment
- Asynchronous support – future methods, batch, queueable
public class MyHelloWorldClass {
public static void sayHello() {
System.debug('Hello World from Apex!');
}
}
Apex is object-oriented, strongly typed, multitenant aware, integrated directly with the database, and provides built-in support for transactional triggers as well as complex asynchronous processing loops.
Account acc = new Account(Name = 'Acme Corp');
insert acc;
Governor limits are runtime limits enforced by the Salesforce multitenant engine to prevent shared execution threads from monopolizing system resources.
// Bulkified query and list insert
List<Contact> contactsToInsert = new List<Contact>();
for (Account acc : [SELECT Id FROM Account LIMIT 100]) {
contactsToInsert.add(
new Contact(
LastName = 'Doe',
AccountId = acc.Id
)
);
}
insert contactsToInsert;
SOQL (Salesforce Object Query Language) is used to read data structures and records from individual object structures.
List<Opportunity> opps = [
SELECT Id, Name, Amount, StageName
FROM Opportunity
WHERE CloseDate = TODAY
];
SOSL (Salesforce Object Search Language) is used to scan text patterns across multiple index tables at once.
List<List<SObject>> searchList = [
FIND 'Acme*'
IN ALL FIELDS
RETURNING
Account(Name),
Contact(FirstName, LastName)
];
SOQL retrieves exact records from a single object index, whereas SOSL performs multi-object keyword searches efficiently across text columns.
// SOQL (Exact retrieval)
Account acc = [
SELECT Id
FROM Account
WHERE Name = 'Acme'
LIMIT 1
];
// SOSL (Fuzzy search)
List<List<SObject>> results = [
FIND 'Acme'
IN NAME FIELDS
RETURNING Account(Id, Name)
];
A trigger is Apex code that runs dynamically before or after specific DML operations, such as record insertions, edits, or removals.
trigger AccountTrigger on Account (before insert, before update) {
for (Account acc : Trigger.new) {
if (acc.Industry == null) {
acc.Industry.addError('Industry is required.');
}
}
}
Data Manipulation Language statements modify records in the Salesforce database (e.g., insert, update, delete, undelete, upsert).
List<Account> newAccs = new List<Account>{
new Account(Name = 'Tech Corp'),
new Account(Name = 'Media Group')
};
insert newAccs;
A class is a structural template or object-oriented blueprint containing processing methods and properties.
public class Vehicle {
private String model;
public Vehicle(String modelName) {
this.model = modelName;
}
}
Apex provides four levels of visibility control: public, private, protected, and global.
global class GlobalService {
public static void performTask() {
// Shared across namespaces and API integrations
}
}
A static variable is scoped to the class itself rather than individual instances, and persists across execution contexts.
public class Counter {
public static Integer staticCount = 0; // Class-level shared state
public Integer instanceCount = 0; // Object-level individual state
}
A constructor is a special class method called automatically during object instantiation to initialize state.
public class Employee {
public String name;
public Employee(String empName) {
this.name = empName;
}
}
Batch Apex processes massive datasets asynchronously by splitting operations into smaller, manageable transaction blocks called chunks.
public class AccountUpdateBatch implements Database.Batchable<SObject> {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id FROM Account'
);
}
public void execute(
Database.BatchableContext bc,
List<Account> scope
) {
// Process each chunk safely
}
public void finish(Database.BatchableContext bc) {
// Post-processing logic
}
}
A future method (annotated with @future) executes asynchronously in its own thread block when resources become available.
public class AsyncHelper {
@future
public static void callExternalWebService(String payload) {
// Async processing
}
}
A test class contains verification logic to test Apex code functionality, and is required to hit a minimum 75% code coverage threshold for production deployments.
@isTest
private class AccountServiceTest {
@isTest
static void testAccountCreation() {
Account acc = new Account(Name = 'Test Acc');
insert acc;
System.assertNotEquals(
null,
acc.Id,
'Account ID should be generated'
);
}
}
Bulkification is the practice of designing code to handle sets of records efficiently in a single operation, rather than processing individual records one-by-one inside loop blocks.
// BAD (Trigger with nested query)
// for (Account a : Trigger.new) {
// List<Contact> c = [
// SELECT Id
// FROM Contact
// WHERE AccountId = :a.Id
// ];
// }
// GOOD (Bulkified query mapping)
Set<Id> accIds = new Set<Id>();
for (Account a : Trigger.new) {
accIds.add(a.Id);
}
List<Contact> contacts = [
SELECT Id, LastName
FROM Contact
WHERE AccountId IN :accIds
];
A wrapper class is a custom container object used to bind multiple distinct data types or sObjects together into a single logical structure.
public class TableWrapper {
public Account accRecord { get; set; }
public Boolean isSelected { get; set; }
public TableWrapper(Account a) {
this.accRecord = a;
this.isSelected = false;
}
}
A map is an un-ordered collection that stores data in key-value pairs, where each key uniquely maps to a single corresponding value.
Map<Id, Account> accountMap =
new Map<Id, Account>([
SELECT Id, Name
FROM Account
LIMIT 10
]);
Account target =
accountMap.get('0018000000GvNX0AAN');
A set is an unordered collection of elements that enforces uniqueness, preventing duplicate entries from being added to the collection.
Set<String> uniqueCodes =
new Set<String>{
'US',
'UK',
'CA',
'US'
};
System.debug(uniqueCodes.size()); // Outputs 3
A list is an ordered collection of elements indexed by position. It can contain duplicate values and functions like a standard dynamic array.
List<String> names = new List<String>();
names.add('Akash');
names.add('Rahul');
String first = names.get(0);
Queueable Apex is an asynchronous design pattern that builds upon future methods, allowing you to monitor job status and chain sequential asynchronous execution flows.
public class AsyncProcessor implements Queueable {
public void execute(QueueableContext context) {
// Chainable async operation
System.enqueueJob(new SecondaryProcessor());
}
}
An sObject is a generic data type that can represent any standard or custom Salesforce object record in memory.
sObject genericRecord =
new Account(Name = 'Generic Corp');
String objName =
genericRecord
.getSObjectType()
.getDescribe()
.getName();
Schema Builder is a visual design utility inside Salesforce used to manage data models, define objects, and establish relationship connections. At the code level, you describe these schemas programmatically.
Map<String, Schema.SObjectType> gd =
Schema.getGlobalDescribe();
Schema.DescribeSObjectResult descResult =
gd.get('Account').getDescribe();
The with sharing keyword enforces the organization-wide defaults and sharing rules of the running user for all database queries and transactions in that class.
public with sharing class SecureRecordController {
// Respects sharing rules
// of current logged-in user
}
The without sharing keyword executes class logic in system mode, ignoring the current user's sharing rules and record permissions.
public without sharing class AdminDataOverrider {
// Skips sharing rules
// and operates with system-level access
}
The upsert command checks if a record exists based on an ID or unique external ID field; it updates the record if found, or inserts a new record if it is not.
Account acc = new Account(
Name = 'Upsert Corp',
External_ID__c = 'EXT-101'
);
upsert acc External_ID__c;
The Database class provides system methods for database manipulation, supporting partial successes via the allOrNone parameter option.
Database.SaveResult[] srList =
Database.insert(accountList, false);
// false allows partial success
Trigger context variables (e.g., Trigger.new, Trigger.oldMap, Trigger.isInsert) provide operational state and data from the triggering transaction.
trigger ContactTrigger on Contact (before update) {
for (Contact newCon : Trigger.new) {
Contact oldCon =
Trigger.oldMap.get(newCon.Id);
if (newCon.Email != oldCon.Email) {
System.debug(
'Email changed from '
+ oldCon.Email
);
}
}
}
A callout executes HTTP requests from Apex to integrate and exchange payload data with external web services.
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint(
'https://api.example.com/data'
);
request.setMethod('GET');
HttpResponse response =
http.send(request);
Exception handling uses try-catch-finally blocks to intercept runtime errors, handle failures gracefully, and execute mandatory cleanup actions.
try {
insert new Lead(LastName = 'Smith');
}
catch (DmlException e) {
System.debug(
'DML failed: ' + e.getMessage()
);
}
finally {
System.debug(
'Transaction finished processing.'
);
}
Apex methods annotated with @AuraEnabled allow client-side Lightning Web Components (LWC) to call server-side actions, retrieve database records, or trigger business logic.
public class ContactController {
@AuraEnabled(cacheable=true)
public static List<Contact> getContactList() {
return [
SELECT Id, FirstName, LastName
FROM Contact
LIMIT 10
];
}
}
Platform Events support an event-driven architecture, enabling applications to run independently by publishing and subscribing to real-time notification streams.
Order_Event__e eventObj =
new Order_Event__e(
Order_Id__c = 'ORD-009',
Status__c = 'Shipped'
);
Database.SaveResult sr =
EventBus.publish(eventObj);
A Mixed DML error occurs when you try to modify a setup object (like User) and a non-setup object (like Account) in the same transaction context.
User u = [
SELECT Id
FROM User
WHERE Alias = 'admin'
];
System.runAs(u) {
// Update User (Setup Object)
}
// Update Account (Non-Setup Object)
// outside block
The Apex Scheduler executes specific classes at scheduled intervals by implementing the Schedulable interface.
public class CleanupScheduler
implements Schedulable {
public void execute(SchedulableContext sc) {
// Delete orphaned logs dynamically
}
}
Custom Metadata Types are application configurations that can be packaged and deployed between environments. They are read from cache without consuming SOQL query limits.
App_Setting__mdt setting =
App_Setting__mdt.getInstance(
'Global_Config'
);
Boolean featureEnabled =
setting.Is_Active__c;
Field-Level Security (FLS) manages user access to specific fields on objects. It should be validated dynamically in Apex before querying or mutating field data.
if (
Schema.sObjectType.Contact
.fields.Email.isAccessible()
) {
// Query/Read field safely
}
Dynamic SOQL allows you to construct and execute SOQL query strings at runtime using the Database.query() method.
String dynamicField = 'Phone, Email';
String queryStr =
'SELECT Id, '
+ dynamicField
+ ' FROM Lead LIMIT 5';
List<Lead> leads =
Database.query(queryStr);
A Savepoint defines a specific state in a transaction that you can roll back to in case of errors, without reverting the entire transaction.
Savepoint sp =
Database.setSavepoint();
try {
insert new Account(
Name = 'Success'
);
insert new Contact();
// Fails due to missing LastName
}
catch (Exception e) {
Database.rollback(sp);
// Reverts transaction
}
Custom Settings are application configurations cached in memory. They provide fast read access without using SOQL query limits.
Discount_Setting__c discount =
Discount_Setting__c.getInstance();
Decimal rate =
discount.Global_Rate__c;
CRUD permissions control user access to entire objects, while FLS permissions control user access to specific fields on those objects.
if (
Schema.sObjectType.Account.isCreateable()
&&
Schema.sObjectType.Account
.fields.Rating.isCreateable()
) {
insert new Account(
Name = 'Protected Inc',
Rating = 'Hot'
);
}
List<Account> accs = [
SELECT Id, Name
FROM Account
];
Account acc =
new Account(Name='Test');
insert acc;
acc.Name = 'Updated';
update acc;
delete acc;
@future
public static void asyncMethod() {
// Long running asynchronous process
}
global class MyBatch
implements Database.Batchable<SObject> {
global Database.QueryLocator start(
Database.BatchableContext BC
) {
return Database.getQueryLocator(
'SELECT Id FROM Account'
);
}
global void execute(
Database.BatchableContext BC,
List<Account> scope
) {
// Batch chunks executed here
}
global void finish(
Database.BatchableContext BC
) {
// Cleanup actions
}
}
trigger AccountTrigger on Account (
before insert
) {
for (Account acc : Trigger.new) {
// Before insert logic
}
}
public class MyQueue
implements Queueable {
public void execute(
QueueableContext context
) {
// Async processing
}
}
String q =
'SELECT Id FROM Account';
List<Account> accs =
Database.query(q);
try {
// Execution with risk
}
catch (DmlException e) {
System.debug(
'Exception: ' + e.getMessage()
);
}
Advanced SOQL includes using aggregate functions (COUNT(), SUM(), AVG()), GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET, and SOQL for loops to process large result sets efficiently.
- Aggregate queries:
SELECT COUNT(Id) FROM Account - GROUP BY:
SELECT Type, COUNT(Id) FROM Account GROUP BY Type - SOQL for loop:
for (Account acc : [SELECT Id FROM Account]) - Relationship queries:
SELECT Name, (SELECT LastName FROM Contacts) FROM Account - Date literals:
WHERE CreatedDate = TODAY
// Aggregate query: count accounts by type
AggregateResult[] results = [
SELECT Type, COUNT(Id) total
FROM Account
GROUP BY Type
];
for (AggregateResult ar : results) {
System.debug('Type: ' + ar.get('Type') + ' Count: ' + ar.get('total'));
}
Use dot notation to traverse relationships. For parent-to-child, use a subquery inside the main query.
- Parent-to-child:
SELECT Name, (SELECT LastName FROM Contacts) FROM Account - Child-to-parent:
SELECT Account.Name, LastName FROM Contact - Multiple levels:
SELECT Account.Owner.Name FROM Contact
// Parent-to-child subquery
List<Account> accountsWithContacts = [
SELECT Name, (SELECT LastName FROM Contacts)
FROM Account
WHERE Id IN :accountIds
];
// Child-to-parent dot notation
List<Contact> contacts = [
SELECT Account.Name, LastName
FROM Contact
WHERE AccountId IN :accountIds
];
SOSL supports searching across multiple objects, field‑specific search, fuzzy search, and returning snippets of matched text.
- Search scope:
FIND {searchTerm} IN ALL FIELDS - Returning specific fields:
RETURNING Account(Name, Phone) - Search groups:
FIND {searchTerm} IN NAME FIELDS RETURNING Contact
// SOSL search across multiple objects
List<List<SObject>> searchResults = [
FIND 'Acme*' IN ALL FIELDS
RETURNING Account(Name, Phone), Contact(FirstName, LastName)
];
Advanced DML includes Database class methods for partial success, upsert with external IDs, merge, and lead convert.
- Database.insert(records, false) – allow partial success
- upsert – insert or update based on external ID
- merge – merge duplicate records
- Database.convertLead – convert leads
// Database.insert with partial success
Database.SaveResult[] srList = Database.insert(accountList, false);
for (Database.SaveResult sr : srList) {
if (!sr.isSuccess()) {
for (Database.Error err : sr.getErrors()) {
System.debug(err.getMessage());
}
}
}
// Upsert using external ID
Account acc = new Account(Name = 'Upsert Corp', ExtId__c = 'EXT-001');
upsert acc ExtId__c;
Best practices include one trigger per object, bulkification, using trigger context variables correctly, avoiding SOQL/DML inside loops, and delegating logic to handler classes.
- Single trigger per object
- Bulkify – process collections, not single records
- Use helper classes for business logic
- Avoid SOQL in loops – use maps
- Validate before DML – use addError
// Best practice: one trigger per object, bulkified, using helper class
trigger AccountTrigger on Account (before insert, before update) {
AccountHandler.handleTrigger(Trigger.old, Trigger.new, Trigger.oldMap, Trigger.newMap, Trigger.operationType);
}
The Trigger Handler pattern is a design pattern where you centralize all trigger logic in a separate class (handler), making triggers lean and maintainable.
- Handler class – contains methods for each trigger event
- Trigger – only calls the handler
- Testable – handler logic can be unit‑tested independently
// Handler class for trigger logic
public class AccountHandler {
public static void handleTrigger(List<Account> oldList, List<Account> newList, Map<Id, Account> oldMap, Map<Id, Account> newMap, System.TriggerOperation op) {
if (op == System.TriggerOperation.BEFORE_INSERT) {
validateAccounts(newList);
} else if (op == System.TriggerOperation.BEFORE_UPDATE) {
validateAccounts(newList);
}
}
private static void validateAccounts(List<Account> accs) {
for (Account a : accs) {
if (a.Name == null) a.Name.addError('Name required');
}
}
}
Use bind variables (:variable) in dynamic SOQL instead of concatenating strings.
- Bind variables:
Database.query('SELECT Id FROM Account WHERE Name = :name') - Escape special characters – avoid raw string concatenation
- Use with sharing to enforce security
// Using bind variables prevents injection
String searchName = 'Acme';
List<Account> accounts = Database.query('SELECT Id FROM Account WHERE Name = :searchName');
Advanced techniques include stateful batch (using Database.Stateful), chaining batch jobs, and using start() to query large datasets.
- Stateful – maintain instance variables across chunks
- Chain – call
Database.executeBatch(nextJob)in finish() - Scope – set
scopeparameter for chunk size
// Stateful batch example
global class StatefulBatch implements Database.Batchable<sObject>, Database.Stateful {
global Integer recordCount = 0;
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id FROM Account');
}
global void execute(Database.BatchableContext bc, List<Account> scope) {
recordCount += scope.size();
}
global void finish(Database.BatchableContext bc) {
System.debug('Total processed: ' + recordCount);
}
}
Use System.enqueueJob inside the execute method to chain another Queueable job.
- Chain:
System.enqueueJob(new SecondQueueable()); - Monitor:
AsyncApexJobto track status
public class FirstQueueable implements Queueable {
public void execute(QueueableContext ctx) {
// do work
System.enqueueJob(new SecondQueueable());
}
}
Use System.schedule with a cron expression to schedule a class implementing Schedulable.
- Cron expression:
'0 0 2 * * ?'– daily at 2 AM - Schedule:
System.schedule('Job Name', cronExp, new MySchedulable()); - Manage:
CronTriggerto view scheduled jobs
// Schedule a job dynamically
String cronExp = '0 0 2 * * ?'; // 2 AM daily
System.schedule('Daily Cleanup', cronExp, new CleanupScheduler());
Use System.assertEquals, System.assertNotEquals, System.assert, and System.assertThrows to validate expected behavior.
- assertEquals:
System.assertEquals(expected, actual) - assertNotEquals:
System.assertNotEquals(expected, actual) - assert:
System.assert(condition) - assertThrows:
System.assertThrows(Exception.class, () => { ... })
@isTest
static void testMethod() {
Integer expected = 5;
Integer actual = someMethod();
System.assertEquals(expected, actual, 'Values should match');
System.assertNotEquals(0, actual);
System.assert(actual > 0);
System.assertThrows(Exception.class, () => { throw new Exception('test'); });
}
A Test Data Factory is a reusable class that creates test data (records) with consistent defaults, making tests easier to write and maintain.
- Factory class – static methods to create sObjects
- Consistency – ensures valid data for tests
- Reusability – share across test classes
@isTest
public class TestDataFactory {
public static Account createAccount(String name) {
return new Account(Name = name);
}
public static List<Contact> createContacts(Integer count, Id accountId) {
List<Contact> cons = new List<Contact>();
for (Integer i = 0; i < count; i++) {
cons.add(new Contact(LastName = 'Test' + i, AccountId = accountId));
}
return cons;
}
}
It allows a test method to access all org data, not just test data. Use sparingly – it makes tests less isolated and more fragile.
- Default: tests see only data created in test context
- When to use: when you need reference data that cannot be created in tests
- Drawback: can cause flaky tests if org data changes
@isTest(SeeAllData=true)
static void testWithAllData() {
// This test can see real org data
// Use sparingly
}
Use Schema.sObjectType methods like isCreateable(), isReadable(), etc., to check permissions before performing DML or queries.
- CRUD:
Schema.Account.isAccessible() - FLS:
Schema.Account.Name.isAccessible() - Without Sharing bypasses these checks – handle with care
if (Schema.sObjectType.Account.isCreateable() &&
Schema.sObjectType.Account.fields.Name.isCreateable()) {
insert new Account(Name = 'Safe');
}
with sharing enforces the user's sharing rules; without sharing runs in system mode, ignoring sharing rules.
- with sharing – respects record-level access
- without sharing – bypasses sharing, used for administrative operations
- Inherited sharing – uses sharing rules of the calling class
public with sharing class SharingClass {
// enforces sharing rules
}
public without sharing class SystemClass {
// bypasses sharing
}
public inherited sharing class InheritedClass {
// inherits from caller
}
Use Http, HttpRequest, and HttpResponse classes to send HTTP requests to external services.
- Create request:
HttpRequest req = new HttpRequest(); - Set endpoint:
req.setEndpoint('https://api.example.com'); - Set method:
req.setMethod('GET'); - Send:
HttpResponse res = new Http().send(req); - Handle response:
res.getStatusCode(),res.getBody()
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/data');
req.setMethod('GET');
req.setHeader('Content-Type', 'application/json');
HttpResponse res = new Http().send(req);
if (res.getStatusCode() == 200) {
String body = res.getBody();
}
Use HttpCalloutMock to mock the callout response in tests.
- Implement:
class MyMock implements HttpCalloutMock - Response:
return new HttpResponse(); - Set:
Test.setMock(HttpCalloutMock.class, new MyMock());
@isTest
class MyMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setBody('{"status":"ok"}');
return res;
}
}
@isTest
static void testCallout() {
Test.setMock(HttpCalloutMock.class, new MyMock());
// call your method that does callout
}
Use the @RestResource annotation to define a REST service, with methods annotated @HttpGet, @HttpPost, etc.
- Class:
@RestResource(urlMapping='/myresource/*') - GET:
@HttpGet– retrieve data - POST:
@HttpPost– create records - Request/Response:
RestContext.request,RestContext.response
@RestResource(urlMapping='/myresource/*')
global class MyRestResource {
@HttpGet
global static List<Account> getAccounts() {
return [SELECT Id, Name FROM Account LIMIT 10];
}
@HttpPost
global static String createAccount(String name) {
Account a = new Account(Name = name);
insert a;
return a.Id;
}
}
@RemoteAction allows Visualforce pages (and JavaScript) to call Apex methods asynchronously via JavaScript remoting.
- Annotation:
@RemoteActionon a static method - Client‑side:
Visualforce.remoting.Manager.invokeAction - Alternatives: LWC uses
@AuraEnabledinstead
public class RemoteActionExample {
@RemoteAction
public static String sayHello(String name) {
return 'Hello ' + name;
}
}
Use EventBus.publish to send a platform event. The event must be defined as a custom object with the __e suffix.
- Create event:
My_Event__e evt = new My_Event__e(Field__c = 'value'); - Publish:
EventBus.publish(evt); - Subscribe:
@AuraEnabledor trigger on the event
My_Event__e evt = new My_Event__e(Message__c = 'Hello');
EventBus.publish(evt);
CDC captures changes to records and exposes them via Platform Events. Apex can listen to CDC events using triggers on the associated event object.
- Event object:
AccountChangeEventfor Account changes - Trigger:
trigger on AccountChangeEvent - Use cases: real‑time integration, auditing
trigger AccountChangeTrigger on AccountChangeEvent (after insert) {
for (AccountChangeEvent evt : Trigger.new) {
// process change event
}
}
Custom Settings are per‑user or per‑org data stored in memory; Custom Metadata Types are deployable configuration that can be packaged and is read‑only at runtime.
- Custom Settings: editable in UI, per‑user/org, cached
- Custom Metadata: packaged, deployable, org‑wide, not editable at runtime
- Performance: both are cached, but Custom Metadata is read‑only and more static
// Custom Setting
Decimal rate = CustomSetting__c.getInstance().Rate__c;
// Custom Metadata
App_Config__mdt config = App_Config__mdt.getInstance('Global');
Boolean enabled = config.Is_Active__c;
Custom Labels are multilingual text strings stored in Salesforce. In Apex, you retrieve them using System.Label.LabelName.
- Define: Setup → Custom Labels
- Access:
String msg = System.Label.MyLabel; - Benefits: easy translations, centralized text
String welcome = System.Label.WelcomeMessage;
Use Messaging.SingleEmailMessage for simple emails, or Messaging.sendEmail for mass emails.
- Single:
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); - Set fields:
mail.setToAddresses(['email@example.com']); - Send:
Messaging.sendEmail(new Messaging.Email[] { mail }); - Email Template: use
setTemplateIdto use a Visualforce or HTML email template
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(new String[] { 'user@example.com' });
mail.setSubject('Hello');
mail.setPlainTextBody('Body text');
Messaging.sendEmail(new Messaging.Email[] { mail });
Use ContentVersion (Files) for modern document management. Insert a ContentVersion record and link it to a parent record via ContentDocumentLink.
- ContentVersion:
ContentVersion cv = new ContentVersion(ContentLocation = 'S', PathOnClient = 'file.pdf', ...); - Insert:
insert cv; - Link:
ContentDocumentLink cdl = new ContentDocumentLink(LinkedEntityId = parentId, ContentDocumentId = cv.ContentDocumentId);
ContentVersion cv = new ContentVersion();
cv.ContentLocation = 'S'; // stored in Salesforce
cv.PathOnClient = 'file.pdf';
cv.Title = 'My File';
cv.VersionData = Blob.valueOf('File content');
insert cv;
// Link to parent
ContentDocumentLink cdl = new ContentDocumentLink();
cdl.LinkedEntityId = parentRecordId;
cdl.ContentDocumentId = cv.ContentDocumentId;
insert cdl;
A SOQL for loop automatically queries records in batches of 200, preventing heap size errors when processing large datasets.
- Syntax:
for (Account acc : [SELECT Id FROM Account]) - Optimization: uses internal query locator to avoid bulk memory usage
- Best practice: use for DML on large sets
// Processes in batches of 200
for (Account acc : [SELECT Id, Name FROM Account WHERE CreatedDate = TODAY]) {
// Do something with each account
}
Aggregate queries use functions like COUNT(), SUM(), AVG(), MIN(), MAX() to summarize data. They are ideal for reports and dashboards.
- Example:
SELECT Type, COUNT(Id) FROM Account GROUP BY Type - Group by: use
GROUP BYfor category summaries - Having:
HAVING COUNT(Id) > 10
AggregateResult[] res = [
SELECT Account.Type, COUNT(Id) total
FROM Account
GROUP BY Account.Type
HAVING COUNT(Id) > 5
];
Dynamic DML uses Database.insert, Database.update, etc., with the SObject type and field names as strings, allowing runtime flexibility.
- Use: when object/field names are not known at compile time
- Example:
Database.insert(new SObject('Account')); - Drawback: loses compile‑time type safety
// Dynamic DML with SObject type
SObject sobj = Schema.getGlobalDescribe().get('Account').newSObject();
sobj.put('Name', 'Dynamic Account');
Database.insert(sobj);
It returns a map of all SObject types accessible to the user. Used for dynamic code that needs to discover objects and fields at runtime.
- Usage:
Map<String, Schema.SObjectType> gd = Schema.getGlobalDescribe(); - Check object existence:
gd.containsKey('Account') - Performance: call sparingly, as it's expensive
Map<String, Schema.SObjectType> gd = Schema.getGlobalDescribe();
if (gd.containsKey('Account')) {
Schema.DescribeSObjectResult dsr = gd.get('Account').getDescribe();
System.debug('Account fields: ' + dsr.fields.getMap().keySet());
}
The Database.QueryLocator is used in the start() method of a batch class to define the scope of records to be processed. It allows large datasets to be processed in chunks.
- Return type:
Database.QueryLocator - Usage:
return Database.getQueryLocator('SELECT Id FROM Account'); - Optimization: efficiently handles large query results
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id FROM Account WHERE CreatedDate = TODAY');
}
It marks a batch class as stateful, allowing instance variables to retain their values across chunk executions. Useful for accumulating totals or counters.
- Annotation:
global class MyBatch implements Database.Batchable<sObject>, Database.Stateful - Use case: counting total records processed across all chunks
- Caution: state can be large and cause heap issues
global class MyBatch implements Database.Batchable<sObject>, Database.Stateful {
global Integer total = 0;
// ...
global void execute(Database.BatchableContext bc, List<sObject> scope) {
total += scope.size();
}
// total is preserved across chunks
}
Yes. You can specify without sharing on the Queueable class to bypass sharing rules, or with sharing to enforce them.
- Default: if not specified, uses sharing rules of the calling context
- Override: explicitly declare
public without sharing class MyQueueable - Best practice: explicitly define to avoid surprises
public without sharing class MyQueueable implements Queueable {
public void execute(QueueableContext ctx) {
// runs in system mode
}
}
Define a class that extends Exception. Then throw it using throw new MyException('message');
- Definition:
class MyException extends Exception - Throw:
throw new MyException('Error occurred'); - Catch:
catch (MyException e) { ... }
public class MyException extends Exception {}
// throw new MyException('Something went wrong');
Use System.debug statements, check debug logs in the Developer Console, and use checkpoints or Apex debugger.
- System.debug:
System.debug('Value: ' + variable); - Log levels:
System.debug(LoggingLevel.INFO, 'message'); - Developer Console: execute anonymous, query editor, and logs
System.debug(LoggingLevel.INFO, 'Variable value: ' + myVar);
// Check debug logs in Developer Console
Minimize SOQL queries, avoid DML in loops, use collections, bulkify code, and leverage batch/queueable for large operations.
- SOQL: query outside loops; use maps to avoid repeated queries
- DML: use collections, not single records
- Bulkification: design for multirecord operations
- Asynchronous: use future/queueable/batch for heavy lifting
// BAD: SOQL in loop
// for (Account a : accounts) {
// List<Contact> cons = [SELECT Id FROM Contact WHERE AccountId = :a.Id];
// }
// GOOD: bulk query
Set<Id> accIds = new Map<Id, Account>(accounts).keySet();
Map<Id, List<Contact>> contactMap = new Map<Id, List<Contact>>();
for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accIds]) {
if (!contactMap.containsKey(c.AccountId)) contactMap.put(c.AccountId, new List<Contact>());
contactMap.get(c.AccountId).add(c);
}
It causes multiple SOQL queries, quickly hitting governor limits. Always extract data beforehand and use maps for lookups.
- Problem: each iteration consumes a query
- Fix: query once, store in map, then iterate
- Governor limit: max 100 SOQL queries per transaction
// BAD: query inside loop
for (Account a : Trigger.new) {
List<Contact> cons = [SELECT Id FROM Contact WHERE AccountId = :a.Id];
}
// GOOD: query once, use map
Set<Id> accIds = new Map<Id, Account>(Trigger.new).keySet();
List<Contact> allContacts = [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accIds];
Map<Id, List<Contact>> contactsByAcc = new Map<Id, List<Contact>>();
for (Contact c : allContacts) {
if (!contactsByAcc.containsKey(c.AccountId)) contactsByAcc.put(c.AccountId, new List<Contact>());
contactsByAcc.get(c.AccountId).add(c);
}
Query related records once, put them into a map keyed by the lookup field, then retrieve in a loop.
- Example:
Map<Id, Account> accountMap = new Map<Id, Account>([SELECT Id FROM Account]); - Usage:
Account acc = accountMap.get(contact.AccountId); - Benefit: O(1) lookups, no extra queries
Map<Id, Account> accountMap = new Map<Id, Account>([SELECT Id, Name FROM Account WHERE Id IN :accountIds]);
for (Contact c : contacts) {
Account acc = accountMap.get(c.AccountId);
// use acc
}
DML statements inside loops cause multiple DML operations, consuming governor limits quickly and reducing performance.
- Problem: each iteration is a separate DML call
- Fix: collect records in a list and DML outside the loop
- Governor limit: max 150 DML statements per transaction
// BAD: DML inside loop
for (Account a : accounts) {
update a;
}
// GOOD: collect in list
List<Account> toUpdate = new List<Account>();
for (Account a : accounts) {
toUpdate.add(a);
}
update toUpdate;
Use maps for lookups, sets for distinct values, and lists for DML operations. Process data in collections rather than individual records.
- Pattern 1: collect IDs in set, query once
- Pattern 2: use map to store related records
- Pattern 3: accumulate records in list, DML once
// Use set for distinct IDs
Set<Id> ids = new Set<Id>();
for (Account a : accounts) ids.add(a.Id);
// Use map for lookups
Map<Id, Account> accMap = new Map<Id, Account>([SELECT Id FROM Account]);
// Use list for DML
List<Contact> toInsert = new List<Contact>();
// fill list
insert toInsert;
Yes, but it's not recommended. Best practice is one trigger per object, with logic delegated to handler classes.
- Why one? – avoids unpredictable execution order
- If multiple: order is not guaranteed
- Best practice: single trigger, handler pattern
// Not recommended to have multiple triggers
// Best: one trigger per object
// Trigger on Account (before insert)
trigger AccountTrigger on Account (before insert) {
AccountHandler.handleInsert(Trigger.new);
}
Order: Before triggers, After triggers, Assignment rules, Auto‑response rules, Workflow rules, Process Builder, Escalation rules, and finally Commit.
- Before insert/update – modify fields before saving
- After insert/update – use updated record IDs for related operations
- System order: see Salesforce docs for full sequence
// Order: Before triggers → After triggers → Assignment rules → Auto-response → Workflow → Process Builder → Escalation → Commit.
// Example showing before/after usage
trigger OrderTrigger on Order (before insert, after insert) {
if (Trigger.isBefore) {
// modify fields
} else if (Trigger.isAfter) {
// create related records
}
}
Trigger.new– new records (before/after insert, after update)Trigger.old– old records (before/after update, before delete)Trigger.newMap– map of new records by ID (after insert, after update)Trigger.oldMap– map of old records by ID (before update, before delete)Trigger.isInsert,Trigger.isUpdate,Trigger.isDelete,Trigger.isUndelete
// Trigger.new - new records
// Trigger.old - old records
// Trigger.newMap - map of new by Id
// Trigger.oldMap - map of old by Id
// Trigger.isInsert, isUpdate, isDelete, isUndelete, isBefore, isAfter
if (Trigger.isInsert && Trigger.isBefore) {
for (Account a : Trigger.new) {
a.Name = a.Name.toUpperCase();
}
}
Use addError on a record or on a field to prevent the DML operation and display a user‑friendly error message.
- Record error:
record.addError('Invalid data'); - Field error:
record.fieldName.addError('Invalid value'); - Works in before/after triggers
if (Trigger.isBefore) {
for (Account a : Trigger.new) {
if (a.Name == null) {
a.Name.addError('Name cannot be null');
}
// field-level error
if (a.Industry == null) {
a.Industry.addError('Industry required');
}
}
}
Before triggers are used to modify field values before they are saved. After triggers are used when you need the record ID (e.g., for creating related records).
- Before: validation, field updates, cross‑object calculations
- After: creating child records, sending emails, updating external systems
// Before: modify fields before save
// After: need record IDs, create related records
trigger ContactTrigger on Contact (before insert) {
for (Contact c : Trigger.new) {
c.Description = 'New';
}
}
trigger AccountTrigger on Account (after insert) {
for (Account a : Trigger.new) {
// Create a default contact using the new Account Id
insert new Contact(LastName = 'Default', AccountId = a.Id);
}
}
A trigger framework is a structured approach (often using a base class) to handle trigger events, enforce order, and improve maintainability.
- Base class – handles trigger context variables and event dispatching
- Handler classes – implement specific logic for each event
- Benefits: consistent pattern, easier testing, less code duplication
// Base trigger framework (simplified)
public abstract class TriggerHandler {
public static void run() {
// handle context, invoke specific handlers
}
}
// Then each trigger calls TriggerHandler.run();
Salesforce requires at least 75% overall code coverage for Apex classes and triggers to be deployed to production. Triggers must have 1% coverage on their own.
- Overall: 75% of all Apex code must be covered
- Triggers: must have at least 1% coverage
- Best practice: aim for 100% to avoid deployment issues
// Minimum 75% overall coverage
// Triggers need at least 1% coverage
// Write test classes to cover all logic
@isTest
class MyTest {
@isTest static void testMethod() {
// cover code
}
}
Use @IsTest annotations and the Test class methods like Test.startTest() and Test.stopTest() to create test data that is automatically rolled back.
- Test.startTest() – resets governor limits for the code block
- Test.stopTest() – executes asynchronous calls
- Data isolation: test data doesn't commit to the org
@isTest
static void testIsolation() {
Test.startTest();
// perform DML, async calls
Test.stopTest(); // executes async code
// assertions after
}
Mock frameworks like stub (using System.StubProvider) allow you to create test doubles for dependencies, isolating the class under test.
- Stub: implement
System.StubProviderto return mock data - Test.setMock: used for HTTP callouts and other external dependencies
- Benefits: fast, reliable, isolated unit tests
// Using StubProvider (example)
public class MyMock implements System.StubProvider {
public Object handleMethodCall(Object stubbedObject, String stubbedMethodName, Type returnType, List<Type> listOfParamTypes, List<String> listOfParamNames, List<Object> listOfArgs) {
if (stubbedMethodName == 'getData') {
return 'Mock Data';
}
return null;
}
}
// Use Test.createStub() to apply
No, directly calling out from a trigger is not allowed. Instead, you must call a @future or Queueable method that performs the callout.
- Why? – triggers are synchronous, callouts require async
- Solution: use
@future(callout=true)or Queueable - Best practice: use Queueable for better monitoring
// In trigger, call a future method
@future(callout=true)
public static void doCallout(String param) {
// make callout
}
// Trigger: MyClass.doCallout('test');
1. Bulkify – always design for multirecord operations.
2. Avoid SOQL/DML in loops – use maps and collections.
3. Use with sharing unless you have a strong reason not to.
4. Handle exceptions – try-catch and addError where appropriate.
5. Write test classes – aim for 100% coverage and use data factories.
// Top 5 best practices summarized:
// 1. Bulkify – process collections, not single records.
// 2. Avoid SOQL/DML in loops – use maps and sets.
// 3. Use with sharing (unless system mode needed).
// 4. Handle exceptions properly (try-catch, addError).
// 5. Write comprehensive test classes (>=75% coverage).