Getting started
Introduction
APEX is Salesforce's proprietary Java-like programming language for the Force.com platform. Strongly typed, object-oriented, executes server-side.
Basic Syntax
// Class declaration
public class HelloWorld {
public static void sayHello() {
System.debug('Hello World!');
}
}
Quick Example
// Query and update records
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Industry = 'Technology'];
for (Account acc : accounts) {
acc.Rating = 'Hot';
}
update accounts;
Anonymous Execution
// Execute in Developer Console
Account acc = new Account(Name='Acme Corp');
insert acc;
System.debug('Created: ' + acc.Id);
Data Types
Primitives
| Type | Description | Example |
|---|---|---|
Integer |
32-bit number | Integer i = 42; |
Long |
64-bit number | Long l = 123456789L; |
Decimal |
Precise decimals | Decimal d = 3.14159; |
Double |
64-bit floating | Double d = 3.14; |
String |
Text data | String s = 'Hello'; |
Boolean |
True/false | Boolean b = true; |
Date |
Date only | Date d = Date.today(); |
DateTime |
Date and time | DateTime dt = DateTime.now(); |
Blob |
Binary data | Blob b = Blob.valueOf('data'); |
ID |
Salesforce ID | ID accountId = acc.Id; |
Collections
// List (ordered, duplicates allowed)
List<String> names = new List<String>{'Alice', 'Bob'};
names.add('Charlie');
String first = names[0];
// Set (unordered, unique)
Set<Integer> numbers = new Set<Integer>{1, 2, 3};
numbers.add(4);
Boolean hasTwo = numbers.contains(2);
// Map (key-value pairs)
Map<String, Integer> ages = new Map<String, Integer>();
ages.put('Alice', 30);
Integer aliceAge = ages.get('Alice');
sObjects
// Standard object
Account acc = new Account();
acc.Name = 'Acme Corp';
acc.Industry = 'Technology';
// Custom object (API name ends with __c)
Custom_Object__c obj = new Custom_Object__c();
obj.Custom_Field__c = 'value';
// Generic sObject
sObject s = new Account();
s.put('Name', 'Generic Account');
SOQL Queries
Basic Queries
// Simple query
List<Account> accounts = [SELECT Id, Name FROM Account];
// With WHERE clause
List<Contact> contacts = [
SELECT Id, FirstName, LastName, Email
FROM Contact
WHERE LastName = 'Smith'
];
// With LIMIT
Contact c = [
SELECT Id, Email
FROM Contact
WHERE Id = :contactId
LIMIT 1
];
Advanced Queries
// ORDER BY and LIMIT
List<Opportunity> opps = [
SELECT Id, Name, Amount
FROM Opportunity
WHERE Amount > 10000
ORDER BY Amount DESC
LIMIT 10
];
// Relationship queries (parent to child)
List<Account> accounts = [
SELECT Id, Name,
(SELECT Id, FirstName, LastName FROM Contacts)
FROM Account
];
// Child to parent
List<Contact> contacts = [
SELECT Id, FirstName, Account.Name
FROM Contact
];
Query Operators
// Comparison operators
[SELECT Id FROM Account WHERE AnnualRevenue > 1000000]
[SELECT Id FROM Account WHERE Industry != 'Technology']
// IN and NOT IN
[SELECT Id FROM Account WHERE Industry IN ('Tech', 'Finance')]
[SELECT Id FROM Contact WHERE LastName NOT IN :nameSet]
// LIKE (wildcards)
[SELECT Id FROM Account WHERE Name LIKE 'Acme%']
[SELECT Id FROM Contact WHERE Email LIKE '%@example.com']
// AND, OR
[SELECT Id FROM Account WHERE Industry = 'Tech' AND AnnualRevenue > 1000000]
// Date functions
[SELECT Id FROM Opportunity WHERE CloseDate = THIS_MONTH]
[SELECT Id FROM Case WHERE CreatedDate = LAST_N_DAYS:7]
Bind Variables
String industry = 'Technology';
Integer limit = 10;
List<Account> accounts = [
SELECT Id, Name
FROM Account
WHERE Industry = :industry
LIMIT :limit
];
DML Operations
Basic DML
// Insert
Account acc = new Account(Name='Acme Corp');
insert acc;
// Update
acc.Industry = 'Technology';
update acc;
// Upsert (insert or update based on ID/external ID)
upsert acc;
// Delete
delete acc;
// Undelete
undelete acc;
Bulk DML
// Insert multiple records
List<Account> accounts = new List<Account>{
new Account(Name='Acme Corp'),
new Account(Name='Global Inc'),
new Account(Name='Tech Solutions')
};
insert accounts;
// Update multiple
for (Account acc : accounts) {
acc.Industry = 'Technology';
}
update accounts;
// Delete multiple
delete accounts;
Database Methods
// Partial success allowed
Database.SaveResult[] results = Database.insert(accounts, false);
for (Database.SaveResult result : results) {
if (!result.isSuccess()) {
for (Database.Error error : result.getErrors()) {
System.debug('Error: ' + error.getMessage());
}
}
}
// Update with partial success
Database.SaveResult[] updateResults = Database.update(accounts, false);
// Delete with partial success
Database.DeleteResult[] deleteResults = Database.delete(accounts, false);
// Upsert with external ID
Database.UpsertResult[] upsertResults = Database.upsert(accounts, Account.Fields.ExternalId__c, false);
Triggers
Trigger Structure
trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
if (Trigger.isBefore) {
if (Trigger.isInsert) {
// Before insert logic
}
if (Trigger.isUpdate) {
// Before update logic
}
}
if (Trigger.isAfter) {
if (Trigger.isInsert) {
// After insert logic
}
if (Trigger.isUpdate) {
// After update logic
}
}
}
Trigger Context Variables
| Variable | Type | Description |
|---|---|---|
Trigger.new |
List<sObject> |
New versions of records |
Trigger.old |
List<sObject> |
Old versions (update/delete only) |
Trigger.newMap |
Map<Id, sObject> |
Map of new records by Id |
Trigger.oldMap |
Map<Id, sObject> |
Map of old records by Id |
Trigger.isInsert |
Boolean |
True if insert operation |
Trigger.isUpdate |
Boolean |
True if update operation |
Trigger.isDelete |
Boolean |
True if delete operation |
Trigger.isBefore |
Boolean |
True if before trigger |
Trigger.isAfter |
Boolean |
True if after trigger |
Trigger.isUndelete |
Boolean |
True if undelete operation |
Trigger.size |
Integer |
Number of records |
Trigger Example
trigger OpportunityTrigger on Opportunity (before insert, after update) {
if (Trigger.isBefore && Trigger.isInsert) {
// Set default values
for (Opportunity opp : Trigger.new) {
if (opp.Probability == null) {
opp.Probability = 10;
}
}
}
if (Trigger.isAfter && Trigger.isUpdate) {
// Update related records
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : Trigger.new) {
if (opp.AccountId != null) {
accountIds.add(opp.AccountId);
}
}
// Process accounts
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :accountIds];
// ... update logic
}
}
Classes and Objects
Class Definition
public class Calculator {
// Properties
private Integer result;
// Constructor
public Calculator() {
this.result = 0;
}
// Method
public Integer add(Integer a, Integer b) {
result = a + b;
return result;
}
// Getter
public Integer getResult() {
return result;
}
}
Access Modifiers
| Modifier | Description |
|---|---|
private |
Only within class |
public |
Within namespace |
global |
All APEX code |
protected |
Within class and subclasses |
Static Methods
public class MathUtils {
public static Integer square(Integer n) {
return n * n;
}
public static Decimal average(List<Integer> numbers) {
Integer sum = 0;
for (Integer n : numbers) {
sum += n;
}
return Decimal.valueOf(sum) / numbers.size();
}
}
// Usage
Integer result = MathUtils.square(5); // 25
Inheritance
// Parent class
public virtual class Animal {
public virtual void makeSound() {
System.debug('Some sound');
}
}
// Child class
public class Dog extends Animal {
public override void makeSound() {
System.debug('Bark!');
}
}
// Usage
Dog myDog = new Dog();
myDog.makeSound(); // Bark!
Interfaces
// Interface definition
public interface Drawable {
void draw();
}
// Implementation
public class Circle implements Drawable {
public void draw() {
System.debug('Drawing a circle');
}
}
Control Flow
If-Else
if (amount > 10000) {
discount = 0.10;
} else if (amount > 5000) {
discount = 0.05;
} else {
discount = 0;
}
Switch Statement
switch on status {
when 'Open' {
System.debug('Case is open');
}
when 'Closed', 'Resolved' {
System.debug('Case is closed');
}
when else {
System.debug('Unknown status');
}
}
// With sObject type
switch on myObject {
when Account acc {
System.debug('Account: ' + acc.Name);
}
when Contact con {
System.debug('Contact: ' + con.LastName);
}
}
For Loops
// Traditional for loop
for (Integer i = 0; i < 10; i++) {
System.debug(i);
}
// List iteration
List<Account> accounts = [SELECT Id, Name FROM Account];
for (Account acc : accounts) {
System.debug(acc.Name);
}
// Set iteration
Set<String> names = new Set<String>{'Alice', 'Bob', 'Charlie'};
for (String name : names) {
System.debug(name);
}
While Loops
// While loop
Integer i = 0;
while (i < 10) {
System.debug(i);
i++;
}
// Do-while loop
Integer j = 0;
do {
System.debug(j);
j++;
} while (j < 10);
Testing
Test Class Structure
@isTest
private class AccountTest {
@testSetup
static void setup() {
// Create test data (runs once per test class)
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 10; i++) {
accounts.add(new Account(Name='Test ' + i));
}
insert accounts;
}
@isTest
static void testInsert() {
Test.startTest();
Account acc = new Account(Name='Test Account');
insert acc;
Test.stopTest();
// Assertions
Account result = [SELECT Id, Name FROM Account WHERE Id = :acc.Id];
System.assertEquals('Test Account', result.Name);
System.assertNotEquals(null, result.Id);
}
}
Assertions
// Equality
System.assertEquals(expected, actual);
System.assertEquals(expected, actual, 'Error message');
System.assertNotEquals(unexpected, actual);
// Null checks
System.assert(condition);
System.assert(condition, 'Error message');
Test Data Creation
@isTest
static void testWithData() {
// Create test data
Account acc = new Account(Name='Test Account', Industry='Technology');
insert acc;
Contact con = new Contact(
FirstName='John',
LastName='Doe',
AccountId=acc.Id
);
insert con;
Test.startTest();
// Your test logic here
Test.stopTest();
// Verify results
Contact result = [SELECT Id, AccountId FROM Contact WHERE Id = :con.Id];
System.assertEquals(acc.Id, result.AccountId);
}
Mock Callouts
@isTest
global class MockHttpResponse implements HttpCalloutMock {
global HTTPResponse respond(HTTPRequest req) {
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
res.setBody('{"status":"success"}');
res.setStatusCode(200);
return res;
}
}
@isTest
static void testCallout() {
Test.setMock(HttpCalloutMock.class, new MockHttpResponse());
Test.startTest();
// Code that makes HTTP callout
Test.stopTest();
}
Annotations
Common Annotations
| Annotation | Usage | Description |
|---|---|---|
@isTest |
Test classes/methods | Mark as test code |
@testSetup |
Test methods | Setup test data |
@future |
Methods | Async execution |
@AuraEnabled |
Methods | Expose to Lightning |
@InvocableMethod |
Methods | Flow/Process Builder |
@ReadOnly |
Methods | Increase query limits |
@HttpGet |
Methods | REST GET endpoint |
@HttpPost |
Methods | REST POST endpoint |
@HttpPut |
Methods | REST PUT endpoint |
@HttpDelete |
Methods | REST DELETE endpoint |
@HttpPatch |
Methods | REST PATCH endpoint |
Future Methods
public class AsyncProcessor {
@future
public static void processRecords(Set<Id> recordIds) {
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :recordIds];
// Process accounts
update accounts;
}
@future(callout=true)
public static void makeCallout(String endpoint) {
HttpRequest req = new HttpRequest();
req.setEndpoint(endpoint);
req.setMethod('GET');
Http http = new Http();
HttpResponse res = http.send(req);
}
}
AuraEnabled
public class AccountController {
@AuraEnabled
public static List<Account> getAccounts() {
return [SELECT Id, Name FROM Account LIMIT 10];
}
@AuraEnabled
public static void updateAccount(Id accountId, String newName) {
Account acc = [SELECT Id FROM Account WHERE Id = :accountId];
acc.Name = newName;
update acc;
}
}
REST API
@RestResource(urlMapping='/accounts/*')
global class AccountRestService {
@HttpGet
global static Account getAccount() {
RestRequest req = RestContext.request;
String accountId = req.requestURI.substring(req.requestURI.lastIndexOf('/') + 1);
return [SELECT Id, Name FROM Account WHERE Id = :accountId];
}
@HttpPost
global static String createAccount(String name, String industry) {
Account acc = new Account(Name=name, Industry=industry);
insert acc;
return acc.Id;
}
}
Exception Handling
Try-Catch-Finally
try {
Account acc = [SELECT Id FROM Account WHERE Name = 'Acme' LIMIT 1];
acc.Name = 'Acme Corp';
update acc;
} catch (DmlException e) {
System.debug('DML Error: ' + e.getMessage());
} catch (QueryException e) {
System.debug('Query Error: ' + e.getMessage());
} catch (Exception e) {
System.debug('General Error: ' + e.getMessage());
} finally {
System.debug('Cleanup code');
}
Common Exceptions
| Exception | Description |
|---|---|
DmlException |
DML operation failed |
QueryException |
SOQL query issue |
ListException |
List index out of bounds |
NullPointerException |
Null reference access |
MathException |
Math operation error |
TypeException |
Type conversion error |
Custom Exceptions
public class CustomException extends Exception {}
public class AccountProcessor {
public static void processAccount(Account acc) {
if (acc.Name == null) {
throw new CustomException('Account name is required');
}
// Process account
}
}
// Usage
try {
AccountProcessor.processAccount(acc);
} catch (CustomException e) {
System.debug('Custom error: ' + e.getMessage());
}
Governor Limits
Common Limits
| Limit | Synchronous | Asynchronous |
|---|---|---|
| SOQL queries | 100 | 200 |
| DML statements | 150 | 150 |
| Heap size | 6 MB | 12 MB |
| CPU time | 10,000 ms | 60,000 ms |
| SOSL queries | 20 | 20 |
| Records per DML | 10,000 | 10,000 |
| Callouts | 100 | 100 |
Checking Limits
System.debug('SOQL Queries: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());
System.debug('DML Statements: ' + Limits.getDmlStatements() + '/' + Limits.getLimitDmlStatements());
System.debug('Heap Size: ' + Limits.getHeapSize() + '/' + Limits.getLimitHeapSize());
System.debug('CPU Time: ' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());
Bulkification
// BAD - Query in loop
for (Account acc : accounts) {
List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
}
// GOOD - Query once, use map
Set<Id> accountIds = new Set<Id>();
for (Account acc : accounts) {
accountIds.add(acc.Id);
}
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact con : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
if (!contactsByAccount.containsKey(con.AccountId)) {
contactsByAccount.put(con.AccountId, new List<Contact>());
}
contactsByAccount.get(con.AccountId).add(con);
}
Best Practices
Bulkification
// Always handle collections
trigger AccountTrigger on Account (after insert) {
List<Contact> contactsToInsert = new List<Contact>();
for (Account acc : Trigger.new) {
contactsToInsert.add(new Contact(
FirstName='Default',
LastName='Contact',
AccountId=acc.Id
));
}
if (!contactsToInsert.isEmpty()) {
insert contactsToInsert;
}
}
Query Optimization
// Select only needed fields
List<Account> accounts = [SELECT Id, Name FROM Account];
// Use WHERE with indexed fields
List<Account> accounts = [SELECT Id FROM Account WHERE Id IN :accountIds];
// Use LIMIT to reduce records
Account acc = [SELECT Id, Name FROM Account WHERE Name = 'Acme' LIMIT 1];
// Use FOR loops with queries for large datasets
for (List<Account> accounts : [SELECT Id, Name FROM Account]) {
// Process batch of 200 records
}
Trigger Framework
// One trigger per object
trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
AccountTriggerHandler.handle();
}
// Handler class
public class AccountTriggerHandler {
public static void handle() {
if (Trigger.isBefore) {
if (Trigger.isInsert) {
beforeInsert(Trigger.new);
}
if (Trigger.isUpdate) {
beforeUpdate(Trigger.new, Trigger.oldMap);
}
}
}
private static void beforeInsert(List<Account> newAccounts) {
// Logic here
}
private static void beforeUpdate(List<Account> newAccounts, Map<Id, Account> oldMap) {
// Logic here
}
}
Security
// Use WITH SECURITY_ENFORCED
List<Account> accounts = [
SELECT Id, Name
FROM Account
WITH SECURITY_ENFORCED
];
// Check field accessibility
if (Schema.sObjectType.Account.fields.Name.isAccessible()) {
// Query field
}
// Check CRUD permissions
if (Schema.sObjectType.Account.isCreateable()) {
insert acc;
}
// Strip inaccessible fields
List<Account> accounts = [SELECT Id, Name, Industry FROM Account];
SObjectAccessDecision decision = Security.stripInaccessible(AccessType.READABLE, accounts);
accounts = decision.getRecords();
Batch APEX
Batch Class
global class AccountBatch implements Database.Batchable<sObject> {
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([
SELECT Id, Name, Industry
FROM Account
WHERE Industry = null
]);
}
global void execute(Database.BatchableContext bc, List<Account> scope) {
for (Account acc : scope) {
acc.Industry = 'Other';
}
update scope;
}
global void finish(Database.BatchableContext bc) {
System.debug('Batch job completed');
}
}
// Execute batch
AccountBatch batch = new AccountBatch();
Database.executeBatch(batch, 200); // Batch size 200
Schedulable
global class ScheduledBatch implements Schedulable {
global void execute(SchedulableContext sc) {
AccountBatch batch = new AccountBatch();
Database.executeBatch(batch);
}
}
// Schedule job (run daily at 2 AM)
ScheduledBatch schedulable = new ScheduledBatch();
String cronExp = '0 0 2 * * ?';
System.schedule('Daily Account Batch', cronExp, schedulable);
Queueable APEX
Queueable Class
public class AccountQueueable implements Queueable {
private List<Account> accounts;
public AccountQueueable(List<Account> accounts) {
this.accounts = accounts;
}
public void execute(QueueableContext context) {
for (Account acc : accounts) {
acc.Industry = 'Technology';
}
update accounts;
}
}
// Enqueue job
List<Account> accounts = [SELECT Id FROM Account LIMIT 10];
AccountQueueable queueable = new AccountQueueable(accounts);
System.enqueueJob(queueable);
Chaining Queueable Jobs
public class ChainedQueueable implements Queueable {
public void execute(QueueableContext context) {
// First job logic
System.debug('First job executed');
// Chain next job
System.enqueueJob(new SecondQueueable());
}
}
public class SecondQueueable implements Queueable {
public void execute(QueueableContext context) {
System.debug('Second job executed');
}
}
Gotchas
Common Pitfalls
// ❌ Query/DML in loops
for (Account acc : accounts) {
update acc; // Governor limit violation
}
// ✅ Bulkify
update accounts;
// ❌ Modifying Trigger.new in after triggers
if (Trigger.isAfter) {
for (Account acc : Trigger.new) {
acc.Name = 'Changed'; // Error!
}
}
// ✅ Only modify in before triggers
if (Trigger.isBefore) {
for (Account acc : Trigger.new) {
acc.Name = 'Changed'; // OK
}
}
// ❌ SOQL injection
String name = 'Acme';
List<Account> accounts = Database.query('SELECT Id FROM Account WHERE Name = \'' + name + '\'');
// ✅ Use bind variables
List<Account> accounts = [SELECT Id FROM Account WHERE Name = :name];
Null Safety
// Always check for null
if (acc != null && acc.Name != null) {
System.debug(acc.Name);
}
// Use safe navigation (not supported - use null checks)
String name = (acc != null) ? acc.Name : 'Unknown';
// Handle empty lists
List<Account> accounts = [SELECT Id FROM Account WHERE Name = 'NonExistent'];
if (!accounts.isEmpty()) {
Account acc = accounts[0];
}
Also see
- Salesforce Developer Documentation - Official APEX reference
- Trailhead APEX Basics - Interactive learning
- APEX Recipes GitHub - Code examples and patterns
- APEX Developer Guide - Complete language guide
- Salesforce Stack Exchange - Community Q&A
- Wikipedia - Salesforce - Platform overview