This Apex developer guide covers what you need to build backend logic on the Salesforce platform, from data types through triggers and asynchronous execution. It sets out the multi-tenant governor limits, including the 100-query SOQL cap, the 4 async execution types, and the mandatory 75% test coverage. Use it as a reference for writing bulk-safe code and for deciding between Apex and Flow.
What is Apex?
Apex is Salesforce's proprietary, strongly-typed, server-side programming language. It looks like Java, runs on Lightning Platform servers, and sits behind triggers, custom REST endpoints, scheduled jobs, batch processing, and any business logic too complex for Flow.
Three things make Apex distinctive:
- Multi-tenant governor limits. Every transaction is capped on CPU time, SOQL queries, DML statements and heap size, so one tenant's bad code can't degrade another's experience.
- Database-native types. sObject types map directly to Salesforce objects:
Account a = new Account(Name='Acme'); insert a;with no ORM and no manual mapping. - Required test coverage. 75% line coverage to deploy to production, enforced at the platform level. It is a nuisance on day one and one of the reasons Salesforce orgs survive multi-developer teams over years.
Core concepts
Data types
Apex has five categories: primitives (Integer, Long, Double, Decimal, String, Boolean, Date, DateTime, Time, Id, Blob), collections (List, Set, Map), sObjects (Account, Contact, Custom__c), enums, and classes. Choosing the right one matters more than it looks: Decimal for currency because it is exact, Double for math because it is not, Long only when you need values past 2 billion. Full reference: SFDC Data Types in Apex: Primitives, Collections & sObjects.
Classes and methods
public with sharing class AccountService {
public static List<Account> getActiveByIndustry(String industry) {
return [SELECT Id, Name FROM Account WHERE Industry = :industry AND IsActive__c = TRUE];
}
}
with sharing / without sharing / inherited sharing controls record visibility. public / private / global controls package visibility. Most production code uses public with sharing.
Triggers
Triggers are a special Apex type that runs on every DML operation against an object. There are 7 trigger events across 2 timing categories. Full guide: Types of Apex Triggers: Before, After & 7 Trigger Events.
Async Apex
When work exceeds synchronous limits or shouldn't block the UI, Apex gives you four async patterns:
| Pattern | When to use |
|---|---|
Queueable |
Modern default. One job, full governor limits, supports chaining |
Batch Apex |
Massive record sets, millions of rows processed in chunks |
@future |
Legacy fire-and-forget, superseded by Queueable in most cases |
Schedulable |
Cron-style scheduled execution |
Common patterns
SOQL and DML
Apex embeds SOQL natively in square brackets:
List<Account> accs = [SELECT Id, Name FROM Account WHERE Industry = 'Tech'];
for (Account a : accs) {
a.Name = a.Name.toUpperCase();
}
update accs;
Three rules: never put DML inside a loop, bulkify everything, and use bind variables (:myVar) instead of string concatenation. See the SOQL pillar for query depth.
String manipulation
Apex's String class has 40+ methods. The ones you'll use daily are split, join, contains, startsWith, replaceAll, format, and escapeSingleQuotes, which is what stands between dynamic SOQL and an injection bug. Full reference: Apex String Class: 30 Methods Salesforce Developers Use Daily.
Test classes
Every Apex class needs tests:
@isTest
private class AccountServiceTest {
@TestSetup
static void setup() {
insert new Account(Name='Acme', Industry='Tech');
}
@isTest
static void testGetActiveByIndustry() {
List<Account> result = AccountService.getActiveByIndustry('Tech');
System.assertEquals(1, result.size(), 'Expected one matching Account');
}
}
For async testing patterns including the try/catch trap, see Apex Test Class: Catching Thrown Exceptions in Async Batch Jobs.
Governor limits cheat sheet
| Resource | Sync | Async |
|---|---|---|
| SOQL queries | 100 | 200 |
| SOQL rows returned | 50,000 | 50,000 |
| DML statements | 150 | 150 |
| DML rows | 10,000 | 10,000 |
| Heap size | 6 MB | 12 MB |
| CPU time | 10,000 ms | 60,000 ms |
| Callouts | 100 | 100 |
@future calls |
50 | 50 |
| Email recipients | 5,000 / day org-wide | shared |
Hitting a limit throws a LimitException. Design for these limits up front with bulk-safe code, async offload and chunking. Catching the exception afterwards is not a plan.
Deep-dive guides
- SFDC Data Types in Apex: Primitives, Collections & sObjects
- Types of Apex Triggers: Before, After & 7 Trigger Events
- Apex String Class: 30 Methods Salesforce Developers Use Daily
- Apex Test Class: Catching Exceptions in Async Batch Jobs
- Stream Large Datasets with Apex Cursors
- WhatId vs WhoId in Salesforce
- SOQL Complete Guide (Apex's read side)
- Salesforce Order of Execution (where Apex fits in the save lifecycle)
When to choose Apex over Flow
Salesforce's official guidance is "configure first, code last." Use Flow when:
- Logic fits a visual canvas (decision branches, screens, loops over collections).
- An admin should be able to maintain it.
- It's a CRUD-style operation: create/update related records.
Use Apex when:
- The logic is genuinely complex (recursion, deep transformations, fiddly validation).
- You need fine-grained control over governor limits or async chaining.
- You're building reusable libraries (utilities, integration adapters).
- Performance matters, and Apex is faster than Flow for tight loops.
The decision tree, with examples: Apex vs Flow: When to Use Code.
Common Apex mistakes
- DML in a loop. Collect into a List, then run the DML once after the loop.
- No null check before
.equals(). Flip it to'expected'.equals(myStr), or checkString.isNotBlankfirst. - Forgetting
with sharing. Default Apex doesn't enforce sharing, so declare it explicitly every time. - String concatenation in a loop. Use
String.joinfor output instead of+=. - No
Test.startTest()/stopTest(). Async code in tests doesn't run without it, and the assertions pass silently. - Hardcoded IDs. Use bind variables, custom metadata, or a config class.
- No bulk testing. A trigger that works on one record breaks at 200, so test in bulk inside @isTest.
Apex is one of those languages where the syntax is easy and the discipline is hard. The guides linked above cover the discipline: the patterns that make Apex code survive multi-year orgs and multi-developer teams. Start with data types, then triggers, then async, then testing. The other 80% you'll pick up by writing real code.
Leave a Comment