Apex
Apex
Definition: Salesforce’s proprietary, strongly typed language for customizing and extending the Salesforce CRM platform.
Paradigm: OOP | Typing: Static
Pros
- Deep native integration with Salesforce data, validation rules, triggers, and workflows.
- Works directly with standard objects, custom objects, SOQL, and platform events.
- Strong demand in enterprise Salesforce consulting and admin/developer hybrid roles.
- Designed around governed enterprise workflows rather than generic application building.
Cons
- Locked to Salesforce, so the language is only useful when that platform is in scope.
- Governor limits constrain CPU time, queries, heap size, and callouts.
- Development is shaped by platform release cycles and metadata-driven behavior.
- Outside the ecosystem, the language has little standalone utility.
Best For
- Customizing Salesforce CRM behavior and business automation.
- Enterprise teams that need platform-native code alongside declarative configuration.
Real Examples
- Apex triggers that react to inserts, updates, and deletes in CRM data.
- Enterprise orgs use Apex for validation, service automation, and integration glue.
Use Cases
- CRM customization, trigger logic, and org-specific business rules.
- Service automation, batch jobs, and integration orchestration in Salesforce.
- Example:
trigger AccountTrigger on Account (before insert) {
for (Account a : Trigger.new) {
if (String.isBlank(a.Name)) {
a.Name.addError('Name required');
}
}
}
Extended Syntax & Features
Apex is a proprietary language with syntax heavily inspired by Java. It compiles and executes directly on the Salesforce Lightning Platform. Apex provides built-in support for DML (Data Manipulation Language) and SOQL (Salesforce Object Query Language), allowing developers to interact with the database natively within their code.
Core Data Types and Variables
Apex supports standard primitive data types, as well as complex collections and custom types known as sObjects (Salesforce Objects).
- Primitives:
Integer,Long,Double,Decimal,Boolean,String,Date,Time,Datetime,ID, andBlob. - Collections:
List,Set,Map. Note that Arrays are essentially the same asListin Apex. - sObjects: Variables that represent any standard or custom object stored in Salesforce, such as
Account,Contact,Opportunity, orMyCustomObject__c.
Integer userAge = 35;
String userName = 'John Doe';
Boolean isActive = true;
Decimal accountBalance = 1500.75;
Date birthDate = Date.newInstance(1990, 5, 20);
ID currentRecordId = '0015g00000abcdeAAA';
List<String> namesList = new List<String>{'Alice', 'Bob', 'Charlie'};
Set<Id> uniqueIds = new Set<Id>();
Map<Id, Account> accountMap = new Map<Id, Account>();
Control Flow and Iteration
Apex relies on traditional procedural control flow blocks such as if-else, switch, for, while, and do-while loops.
- If-Else Blocks: Standard conditional branching.
- Switch Statements: Introduced later in the language evolution to replace extensive
if-elsechains. - For Loops: Including traditional loops, iteration over lists/sets, and the all-important SOQL
forloops.
// Traditional if-else
if (userAge > 18) {
System.debug('User is an adult.');
} else {
System.debug('User is a minor.');
}
// Switch statement
String status = 'Open';
switch on status {
when 'Open' {
System.debug('Task is new.');
}
when 'In Progress' {
System.debug('Task is being worked on.');
}
when else {
System.debug('Task is complete or cancelled.');
}
}
Built-in Query Languages: SOQL and SOSL
One of Apex’s most powerful features is its native support for database queries through SOQL and SOSL.
- SOQL (Salesforce Object Query Language): Designed to query specific records based on criteria. Syntactically similar to SQL but without
SELECT *. - SOSL (Salesforce Object Search Language): Designed for text-based searches across multiple objects simultaneously.
// Inline SOQL Query
List<Contact> contacts = [
SELECT Id, FirstName, LastName, Email
FROM Contact
WHERE AccountId = :currentRecordId
LIMIT 50
];
// Inline SOSL Query
List<List<SObject>> searchList = [
FIND 'Joe'
IN ALL FIELDS
RETURNING Account(Name), Contact(FirstName, LastName)
];
Advanced Concepts
Multitenant Architecture and Governor Limits
Because Salesforce operates in a multitenant cloud environment (many customers share the same database and resources), strict computational rules called “Governor Limits” are enforced. These limits ensure that no single customer can monopolize the shared resources.
- SOQL Limits: A maximum of 100 synchronous SOQL queries per transaction.
- DML Limits: A maximum of 150 DML statements per transaction, operating on a maximum of 10,000 records.
- CPU Time Limits: A maximum of 10,000 milliseconds (10 seconds) of CPU time for synchronous transactions.
- Heap Size Limits: 6 MB of memory for synchronous transactions.
Exceeding these limits throws an uncatchable exception (e.g., LimitException), terminating the entire transaction and rolling back any uncommitted data.
Bulkification
Bulkification is the concept of writing Apex code so that it can properly handle processing multiple records at once. Due to Governor Limits, it is mandatory to write bulkified code, especially in triggers. Code must be written assuming that arrays or lists of records will be processed, rather than single records.
Asynchronous Apex
For processes that require higher limits or long execution times, Apex provides asynchronous models:
- Future Methods: Run in the background asynchronously. Annotated with
@future. Best for callouts and simple operations. Can only accept primitive types. - Queueable Apex: Similar to
@futurebut supports complex objects, allows chaining of jobs, and provides an ID for monitoring. - Batch Apex: Used for processing large volumes of data (up to 50 million records). Interfaces with
Database.Batchable. - Scheduled Apex: Allows scheduling classes to run at specific intervals. Interfaces with
Schedulable.
Sharing and Security (With Sharing vs. Without Sharing)
Apex runs in “System Context” by default, meaning the code has access to all objects and fields, ignoring the user’s permissions and sharing rules. Developers control data visibility using sharing keywords:
with sharing: Enforces the current user’s sharing rules.without sharing: Ignores user sharing rules (executes with full visibility).inherited sharing: Adopts the sharing behavior of the class that called it. This is the recommended default.
Execution Context and Triggers
An execution context includes everything that happens when a process starts to when it finishes, acting as a single transaction. A trigger can cascade into other triggers or workflow rules. A static variable will retain its value for the duration of this context, which is heavily used for preventing recursion.
Ecosystem & Tooling
Salesforce provides a robust, heavily tailored ecosystem for managing and deploying code. Since the code executes entirely server-side on Salesforce’s infrastructure, local execution is not possible.
IDEs and Extensions
- Visual Studio Code (VS Code): The primary and officially recommended IDE for Salesforce development. The Salesforce Extension Pack is a bundle of extensions providing syntax highlighting, code completion, testing tools, and deployment commands.
- Illuminated Cloud: A highly regarded, paid plugin for IntelliJ IDEA, known for intelligent code completion, refactoring tools, and robust integration with the platform.
- Developer Console: A web-based, integrated development environment directly inside the Salesforce UI. Useful for quick debugging, testing, and writing one-off scripts, but not suitable for large-scale development.
Command Line Interfaces
- Salesforce CLI (sf / sfdx): A powerful command-line interface that enables developers to authorize environments, retrieve/deploy metadata, execute anonymous Apex, run tests, and manage scratch orgs.
Testing and CI/CD
- Apex Test Framework: Salesforce requires a minimum of 75% code coverage for all Apex code deployed to a production environment. The built-in testing framework requires writing test classes with the
@isTestannotation. - Scratch Orgs: Temporary, source-driven Salesforce environments used for development and testing. They are designed to be ephemeral and easily spun up via the CLI.
- CI/CD Platforms: Jenkins, GitHub Actions, GitLab CI, and Copado are heavily utilized in enterprise Salesforce practices to move metadata changes between development, staging, and production environments securely.
Code Examples
1. The Classic “Hello World” and Anonymous Apex
Apex code can be executed dynamically through “Anonymous Apex” blocks for quick data manipulation or testing.
// Executable via the Developer Console or Salesforce CLI
String greeting = 'Hello, Salesforce World!';
System.debug(greeting); // This prints to the execution log
2. DML Operations and Bulkification
This example shows how to perform DML on a list of records rather than inside a loop. This is the cornerstone of Apex best practices.
public class AccountService {
// Bulkified method to update Account ratings
public static void updateAccountRatings(List<Account> accountsToUpdate) {
// We only operate on the list passed in. We do not query inside a loop!
for (Account acc : accountsToUpdate) {
if (acc.AnnualRevenue != null && acc.AnnualRevenue > 1000000) {
acc.Rating = 'Hot';
} else {
acc.Rating = 'Cold';
}
}
// One single DML statement to update all modified records
try {
update accountsToUpdate;
} catch (DmlException e) {
System.debug('An error occurred during update: ' + e.getMessage());
}
}
}
3. Asynchronous Execution: Batch Apex
When you need to update thousands of records, synchronous Apex will fail due to limit constraints. Batch Apex processes records in small chunks (default 200).
global class AccountRevenueBatch implements Database.Batchable<sObject>, Database.Stateful {
global Integer recordsProcessed = 0;
// Start method queries the data locator to feed into the execute method
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, AnnualRevenue, Rating FROM Account WHERE AnnualRevenue = null'
);
}
// Execute method processes chunks of the overall result set
global void execute(Database.BatchableContext bc, List<Account> scope) {
List<Account> accountsToUpdate = new List<Account>();
for (Account acc : scope) {
acc.AnnualRevenue = 0; // Defaulting null revenues to zero
acc.Rating = 'Cold';
accountsToUpdate.add(acc);
recordsProcessed++;
}
update accountsToUpdate;
}
// Finish method is called after all chunks have been processed
global void finish(Database.BatchableContext bc) {
System.debug('Total records processed: ' + recordsProcessed);
// Optional: Send an email notification to the administrator
}
}
// To execute: Database.executeBatch(new AccountRevenueBatch(), 200);
4. Apex Trigger Architecture: Trigger Handlers
A single trigger per object is considered a best practice. Logic should be delegated to handler classes.
// AccountTrigger.trigger
trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
if (Trigger.isBefore) {
if (Trigger.isInsert) {
AccountTriggerHandler.beforeInsert(Trigger.new);
} else if (Trigger.isUpdate) {
AccountTriggerHandler.beforeUpdate(Trigger.new, Trigger.oldMap);
}
}
}
// AccountTriggerHandler.cls
public with sharing class AccountTriggerHandler {
public static void beforeInsert(List<Account> newAccounts) {
for (Account acc : newAccounts) {
// Apply business rules
if (String.isBlank(acc.Description)) {
acc.Description = 'Default description applied on creation.';
}
}
}
public static void beforeUpdate(List<Account> newAccounts, Map<Id, Account> oldAccountsMap) {
for (Account acc : newAccounts) {
Account oldAcc = oldAccountsMap.get(acc.Id);
// Check if a specific field was changed
if (acc.Phone != oldAcc.Phone) {
System.debug('Phone number was updated for Account: ' + acc.Name);
}
}
}
}
5. Writing Unit Tests in Apex
Salesforce requires tests to validate business logic and deploy to production.
@isTest
private class AccountServiceTest {
// Test setup method creates data once for all test methods in this class
@testSetup
static void setupTestData() {
List<Account> testAccounts = new List<Account>();
testAccounts.add(new Account(Name = 'High Revenue Corp', AnnualRevenue = 5000000));
testAccounts.add(new Account(Name = 'Low Revenue LLC', AnnualRevenue = 50000));
insert testAccounts;
}
@isTest
static void testUpdateAccountRatings() {
// Query the test data created in @testSetup
List<Account> accts = [SELECT Id, Name, AnnualRevenue, Rating FROM Account];
// Start test execution bounds
Test.startTest();
AccountService.updateAccountRatings(accts);
Test.stopTest(); // Forces asynchronous processes to complete and resets governor limits
// Verify results using System.assert
List<Account> updatedAccts = [SELECT Id, Name, Rating FROM Account];
for (Account acc : updatedAccts) {
if (acc.Name == 'High Revenue Corp') {
System.assertEquals('Hot', acc.Rating, 'High revenue account should be rated Hot');
} else if (acc.Name == 'Low Revenue LLC') {
System.assertEquals('Cold', acc.Rating, 'Low revenue account should be rated Cold');
}
}
}
}
6. Making REST API Callouts
Apex can integrate with external systems via HTTP callouts.
public class CurrencyConverter {
@future(callout=true)
public static void updateExchangeRates() {
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('https://api.exchangerate-api.com/v4/latest/USD');
request.setMethod('GET');
try {
HttpResponse response = http.send(request);
if (response.getStatusCode() == 200) {
// Deserialize JSON response
Map<String, Object> results = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
Map<String, Object> rates = (Map<String, Object>) results.get('rates');
Decimal euroRate = (Decimal) rates.get('EUR');
System.debug('Current USD to EUR rate: ' + euroRate);
// You could subsequently store this in a custom setting or object
} else {
System.debug('Callout failed with status: ' + response.getStatus());
}
} catch(System.CalloutException e) {
System.debug('Callout exception: ' + e.getMessage());
}
}
}
Best Practices
Developing in Apex requires adopting defensive programming techniques due to governor limits and multitenancy. Failure to follow best practices routinely leads to broken organizations, failed deployments, and corrupt data integrations.
1. Bulkify Everything
Never assume your code is running for just a single record. Always write logic to process collections (List, Set, Map). A data import wizard, an API integration, or bulk updating from list views will immediately break single-record-centric code.
2. Avoid SOQL and DML Inside Loops
This is the cardinal sin of Apex. Placing a query or an update statement inside a loop ensures that you will hit the 100 SOQL query limit or 150 DML statement limit when processing batches of records. Instead, loop through records, add the IDs or data to a collection, and perform a single SOQL query or DML operation on the collection outside the loop.
3. One Trigger Per Object
Having multiple triggers on a single object (e.g., two Account triggers) leads to an unpredictable execution order. Standardize by creating one central trigger per object and delegating logic to Handler classes.
4. Context-Specific Handler Methods
Create trigger handlers that define specific methods for different execution contexts (e.g., beforeInsert, afterUpdate). This makes it significantly easier to trace when and why logic executes.
5. Control Recursion
Triggers can inadvertently fire other triggers or fire themselves again, leading to infinite loops and maximum trigger depth errors. Use static boolean variables in a utility class to ensure specific logic only runs once per transaction context.
6. Use Limits Methods
Use the built-in Limits class (e.g., Limits.getQueries(), Limits.getLimitQueries()) to proactively check your usage relative to the bounds. This allows your code to fail gracefully or hand off work to asynchronous processes before crashing.
7. Enforce Object and Field Level Security
Unlike declarative tools, Apex can bypass a user’s permissions. Always verify whether a user is allowed to read, create, update, or delete fields and objects using Schema.DescribeSObjectResult methods (e.g., isAccessible(), isUpdateable()) or by using the WITH SECURITY_ENFORCED clause in your SOQL queries.
8. Comprehensive Code Coverage
Aim for substantially more than the required 75% coverage. Test classes should assert both positive and negative behaviors (what happens when bad data is supplied?), as well as bulk testing (creating 200 records at once to ensure your triggers are properly bulkified). Run your tests without SeeAllData=true to isolate them from real org data.
Referenced by