Salesforce Apex Tutorial for Beginners
โก Smart Summary
Apex programming gives Salesforce developers an object-oriented, strongly typed language for adding custom business logic to the CRM. It powers classes, triggers, and batch jobs while governor limits keep the shared multi-tenant platform stable.

What is Apex in Salesforce?
Apex is an object-oriented and strongly typed programming language developed by Salesforce for building Software as a Service (SaaS) and Customer Relationship Management (CRM) applications. Apex helps developers create third-party SaaS applications and add business logic to system events by providing back-end database support and client-server interfaces.
Apex helps developers add business logic to system events like button clicks, related record updates, and Visualforce pages. Apex has a syntax similar to Java. Register for Salesforce to learn how the CRM works.
Features of Apex Programming Language
Here are the important features of Salesforce Apex:
- Apex is a case-insensitive language.
- You can perform DML operations like INSERT, UPDATE, UPSERT, and DELETE on sObject records using Apex.
- You can query sObject records using SOQL (Salesforce Object Query Language) and SOSL (Salesforce Object Search Language) in Apex.
- Allows you to create a unit test and execute it to verify the code coverage and efficiency of the code in Apex.
- Apex executes in a multi-tenant environment, and Salesforce has defined governor limits that prevent a user from monopolizing the shared resources. Any code that crosses a Salesforce governor limit fails, and an error shows up.
- A Salesforce object can be used as a data type in Apex. For example:
Account acc = new Account();
Here, Account is a standard Salesforce object.
- Apex automatically upgrades with every Salesforce release.
When Should a Developer Choose Apex?
Apex code should only be written if a business scenario is too complex to be implemented using the pre-built, point-and-click functionality provided by Salesforce.
Following are a few scenarios where you need to write Apex code:
- To create web services that integrate Salesforce with other applications.
- To implement custom validation on sObjects.
- To execute custom Apex logic when a DML operation is performed.
- To implement functionality that cannot be built with existing declarative automation tools such as Flow.
- To set up email services that process the contents, headers, and attachments of inbound email.
Once you know when Apex is the right choice, the next step is understanding what happens to your code after you save it.
Working Structure of Apex
Following is the flow of actions for Apex code:
- Developer Action: When the developer saves code to the platform, all the Apex code is compiled into a set of instructions that the Apex runtime interpreter can understand, and these instructions are then saved as metadata on the platform.
- End User Action: When a user event executes Apex code, the platform server retrieves the compiled instructions from the metadata and runs them through the Apex interpreter before returning the result.
The diagram below shows how the developer and end-user actions interact with the Lightning Platform application server:
Apex Development Environment
Apex code can be developed either in a sandbox or in a Developer Edition org of Salesforce.
It is a best practice to develop the code in a sandbox environment and then deploy it to the production environment, as illustrated below:
Apex code development tools: Following are the three tools available to develop Apex code in all editions of Salesforce:
- Developer Console
- Visual Studio Code with the Salesforce Extension Pack (this replaces the retired Force.com IDE)
- Code editor in the Salesforce Setup user interface
With your environment ready, let us look at the building blocks of the language itself, starting with data types.
Data Types in Apex
Following are the data types supported by Apex:
Primitive
Integer, Double, Long, Date, Datetime, Decimal, Time, Blob, String, ID, and Boolean are considered primitive data types. All primitive data types are passed by value, not by reference.
Collections
Three types of collections are available in Apex:
- List: It is an ordered collection of primitives, sObjects, collections, or Apex objects based on indices.
- Set: An unordered collection of unique elements that does not contain duplicates.
- Map: It is a collection of unique keys that map to single values, which can be primitives, sObjects, collections, or Apex objects.
sObject
This is a special data type in Salesforce. It is similar to a table in SQL and contains fields that are similar to columns in SQL.
Enums
An Enum is an abstract data type that stores one value of a finite set of specified identifiers.
Classes, Objects, and Interfaces
User-defined Apex classes and interfaces can also be used as data types. An object refers to an instance of any data type that is supported in Apex.
Apex Syntax
Variable Declaration
As Apex is a strongly typed language, it is mandatory to declare a variable with a data type in Apex.
For example:
Contact con = new Contact();
Here the variable con is declared with Contact as its data type.
SOQL Query
SOQL stands for Salesforce Object Query Language. SOQL is used to fetch sObject records from the Salesforce database. For example:
Account acc = [SELECT Id, Name FROM Account LIMIT 1];
The above query fetches an account record from the Salesforce database.
Loop Statement
A loop statement is used to iterate over the records in a list. The number of iterations is equal to the number of records in the list. For example:
List<Account> listOfAccounts = [SELECT Id, Name FROM Account LIMIT 100];
// iteration over the list of accounts
for(Account acc : listOfAccounts){
//your logic
}
In the above snippet of code, listOfAccounts is a variable of the List data type.
Flow Control Statement
A flow control statement is beneficial when you want to execute some lines of code based on certain conditions.
For example:
List<Account> listOfAccounts = [SELECT Id, Name FROM Account LIMIT 100];
// execute the logic if the size of the account list is greater than zero
if(listOfAccounts.size() > 0){
//your logic
}
The above snippet of code queries account records from the database and checks the list size.
DML Statement
DML stands for Data Manipulation Language. DML statements are used to manipulate data in the Salesforce database. For example:
Account acc = new Account(Name = 'Test Account'); insert acc; //DML statement to create account record.
Apex Access Specifiers
Following are the access specifiers supported by Apex:
Public
This access specifier gives access to a class, method, or variable to be used by Apex within a namespace.
Private
This access specifier gives access to a class, method, or variable to be used locally, or within the section of code where it is defined. All methods and variables that do not have an access specifier defined take the default access specifier of private.
Protected
This access specifier gives access to a method or variable to be used by any inner classes within the defining Apex class.
Global
This access specifier gives access to a class, method, or variable to be used by Apex within a namespace as well as outside of the namespace. It is a best practice not to use the global keyword unless necessary.
Keywords in Apex
With sharing
If a class is defined with this keyword, then all the sharing rules that apply to the current user are enforced. If this keyword is absent, the code executes under system context.
For example:
public with sharing class MyApexClass{
// sharing rules enforced when code in this class executes
}
Without sharing
If a class is defined with this keyword, then the sharing rules that apply to the current user are not enforced.
For example:
public without sharing class MyApexClass{
// sharing rules are not enforced when code in this class executes
}
Static
A variable or method defined with the static keyword is initialized once and associated with the class. Static variables and methods can be called by the class name directly without creating an instance of the class.
Final
A constant or method defined with the final keyword cannot be overridden. For example:
public class myCls {
static final Integer INT_CONST = 10;
}
If you try to override the value of this INT_CONST variable, you will get the exception – System.FinalException: Final variable has already been initialized.
Return
This keyword returns a value from a method. For example:
public String getName() {
return 'Test';
}
Null
It defines a null constant and can be assigned to a variable. For example:
Boolean b = null;
Virtual
If a class is defined with the virtual keyword, it can be extended and overridden.
Abstract
If a class is defined with the abstract keyword, it must contain at least one method with the keyword abstract, and that method should only have a signature.
For example:
public abstract class MyAbstractClass {
abstract Integer myAbstractMethod1();
}
Apex String
A string is a set of characters with no character limit. For example:
String name = 'Test';
There are several built-in methods provided by the String class in Salesforce. Following are a few frequently used methods:
abbreviate(maxWidth)
This method truncates a string to the specified length and returns it if the length of the given string is longer than the specified length; otherwise, it returns the original string. If the value of the maxWidth variable is less than 4, this method throws a runtime exception – System.StringException: Minimum abbreviation width is 4.
For example:
String s = 'Hello World';
String s2 = s.abbreviate(8);
System.debug('s2: ' + s2); //Hello...
capitalize()
This method converts the first letter of a string to title case and returns it.
For example:
String s = 'hello';
String s2 = s.capitalize();
System.assertEquals('Hello', s2);
contains(substring)
This method returns true if the string calling the method contains the specified substring.
String name1 = 'test1';
String name2 = 'test';
Boolean flag = name1.contains(name2);
System.debug('flag:: ' + flag); //true
equals(stringOrId)
This method returns true if the parameter passed is not null and represents the same binary sequence of characters as the string that is calling the method.
When comparing Id values, the lengths of the IDs may not be equal. For example, if a string that represents a 15-character ID is compared with an object that represents an 18-character ID, this method still returns true. For example:
String stringValue15 = '001D000000Ju1zH';
Id idValue18 = '001D000000Ju1zHIAR';
Boolean result = stringValue15.equals(idValue18);
System.debug('result: ' + result); //true
In the above example, the equals method compares a 15-character object Id with an 18-character object Id, and if both IDs represent the same binary sequence, it returns true.
Use this method to make case-sensitive comparisons.
escapeSingleQuotes(stringToEscape)
This method adds an escape character (\) before any single quotation mark in a string and returns the result. This method prevents SOQL injection while creating a dynamic SOQL query. It ensures that all single quotation marks are treated as enclosing strings instead of database commands.
For example:
String s = 'Hello \'Tom\''; String escapedStr = String.escapeSingleQuotes(s); System.debug(escapedStr); // Outputs Hello \'Tom\'
remove(substring)
This method removes all occurrences of the mentioned substring from the string that calls the method and returns the resulting string.
For example:
String s1 = 'Salesforce and force.com';
String s2 = s1.remove('force');
System.debug('s2: ' + s2); // 'Sales and .com'
substring(startIndex)
This method returns a substring that starts from the character at startIndex and extends to the end of the string.
For example:
String s1 = 'hamburger';
String s2 = s1.substring(3);
System.debug('s2: ' + s2); //burger
reverse()
This method reverses all the characters of a string and returns it. For example:
String s = 'Hello';
String s2 = s.reverse();
System.debug('s2:::: ' + s2); // olleH
trim()
This method removes all the leading and trailing white spaces from a string and returns it.
valueOf(toConvert)
This method returns the string representation of the passed-in object.
Strings, classes, and keywords come together when you start packaging logic into reusable units, which is exactly what Apex classes are for.
Apex Class
An Apex class is a blueprint or template from which objects are created. An object is an instance of a class.
There are three ways of creating Apex classes in Salesforce:
- Developer Console
- Visual Studio Code with the Salesforce Extension Pack
- Apex class detail page in Setup
In Apex, you can define an outer class, also called a top-level class, and you can also define classes within an outer class, called inner classes.
It is mandatory to use an access modifier like global or public in the declaration of an outer class.
It is not necessary to use an access modifier in the declaration of inner classes.
An Apex class is defined using the class keyword followed by the class name.
The extends keyword is used to extend an existing class in an Apex class, and the implements keyword is used to implement an interface in an Apex class.
Salesforce Apex does not support multiple inheritance; an Apex class can only extend one existing Apex class but can implement multiple interfaces.
An Apex class can contain a user-defined constructor, and if a user-defined constructor is not available, a default constructor is used. The code in a constructor executes when an instance of a class is created.
Syntax of an Apex class:
public class myApexClass{
// variable declaration
//constructor
public myApexClass(){
}
//methods declaration
}
The new keyword is used to create an instance of an Apex class. Below is the syntax for creating an instance of an Apex class:
myApexClass obj = new myApexClass();
Apex Getter and Setter
An Apex property is similar to an Apex variable. A getter and a setter are necessary for an Apex property. They can be used to execute code before the property value is accessed or changed. The code in the get accessor executes when a property value is read. The code in the set accessor executes when a property value is changed. Any property having only a get accessor is considered read-only, any property having only a set accessor is considered write-only, and any property having both get and set accessors is considered read-write. Syntax of an Apex property:
public class myApexClass {
// Property declaration
access_modifier return_type property_name {
get {
//code
}
set{
//code
}
}
}
Here, access_modifier is the access modifier of the property, return_type is the data type of the property, and property_name is the name of the property.
Below is an example of an Apex property having both get and set accessors:
public class myApex{
public String name{
get{ return name; }
set{ name = value; }
}
}
Here, the property name is name, it is a public property, and it returns a String data type.
It is not mandatory to have code in the get and set blocks. These blocks can be left empty to define an automatic property. For example:
public double MyReadWriteProp{ get; set; }
Get and set accessors can also be defined with their own access modifiers. If an accessor is defined with a modifier, it overrides the access modifier of the property. For example:
public String name{private get; set;}// name is private for read and public for write.
Classes define reusable logic that you call explicitly. Triggers, covered next, run automatically whenever records change.
Apex Trigger
Apex triggers enable you to execute custom Apex before and after a DML operation is performed.
Apex supports the following two types of triggers:
Before triggers: These triggers are used to validate and update a field’s value before the record is saved to the database.
After triggers: These triggers are used to access field values (such as the record ID and the LastModifiedDate field) that are set by the system after a record is committed to the database. These field values can be used to modify other records. Records that fire after triggers are read-only.
It is a best practice to write bulkified triggers. A bulkified trigger can process a single record as well as multiple records at a time.
Syntax of an Apex trigger:
trigger TriggerName on ObjectName (trigger_events) {
//Code_block
}
Here, TriggerName is the name of the trigger, ObjectName is the name of the object on which the trigger is written, and trigger_events is a comma-separated list of events.
Following are the events supported by Apex triggers: before insert, before update, before delete, after insert, after update, after delete, after undelete.
Static keywords cannot be used in an Apex trigger. All the keywords applicable to inner classes can be used in an Apex trigger.
There are implicit variables defined by every trigger that return the run-time context. These variables are defined in the System.Trigger class and are called context variables. The two screenshots below list the context variables supported by Apex triggers:
Following are the considerations for context variables in Apex triggers:
- Do not use trigger.new and trigger.old in DML operations.
- Trigger.new cannot be deleted.
- Trigger.new is read-only in after triggers.
- Trigger.new can be used to change the values of fields on the same object in before triggers only.
The two screenshots below list the considerations about specific actions in different trigger events:
Triggers handle real-time logic, but some jobs are simply too large for a single transaction. That is where batch Apex comes in.
Batch Class in Apex
A batch class in Salesforce is used to process a large number of records that would exceed the Apex governor limits if processed normally. A batch class executes the code asynchronously.
Following are the advantages of a batch class:
- A batch class processes the data in chunks, and if one chunk fails to process successfully, the successfully processed chunks are not rolled back.
- Every chunk of data in a batch class is processed with a new set of governor limits, which ensures that the code executes within the governor execution limits.
The Database.Batchable interface must be implemented by an Apex class to be used as a batch class. It provides three methods that must be implemented by the batch class:
1. start()
This method generates the scope of records or objects to be processed by the interface method execute. It is called only once during the execution of the batch. This method returns either a Database.QueryLocator object or an Iterable. The number of records that can be retrieved by a SOQL query using the Database.QueryLocator object is 50 million, but using an Iterable, the total number of records that can be retrieved by the SOQL query is only 50,000. An Iterable is used to generate a complex scope for the batch class.
Syntax of the start method:
global (Database.QueryLocator | Iterable<sObject>) start(Database.BatchableContext bc) {}
2. execute()
This method is used for processing each chunk of data. The execute method is called for each chunk of records. The default batch size for execution is 200 records. The execute method takes two arguments:
A reference to the Database.BatchableContext object,
A list of sObjects, such as List<sObject>, or a list of parameterized types. Syntax of the execute method:
global void execute(Database.BatchableContext bc, List<P> records){}
3. finish()
The finish method is called once during the execution of the batch class. Post-processing operations can be performed in the finish method. For example: sending a confirmation email. This method is called when all the batches are processed. Syntax of the finish method:
global void finish(Database.BatchableContext bc){}
Database.BatchableContext Object
Each method of the Database.Batchable interface has a reference to a Database.BatchableContext object.
This object is used to track the progress of the batch job.
Following are the instance methods provided by BatchableContext:
- getChildJobId(): This method returns the ID of the batch job that is currently being processed.
- getJobId(): This method returns the ID of the batch job.
Below is the syntax of a batch class:
global class MyBatchClass implements Database.Batchable<sObject> {
global (Database.QueryLocator | Iterable<sObject>) start(Database.BatchableContext bc) {
// collect the batches of records or objects to be passed to execute
}
global void execute(Database.BatchableContext bc, List<P> records){
// process each batch of records
}
global void finish(Database.BatchableContext bc){
// execute any post-processing operations
}
}
Database.executeBatch Method
The Database.executeBatch method is used for executing a batch class.
This method takes two parameters: an instance of the batch class to be processed, and an optional scope parameter to specify the batch size. If it is not specified, the default size of 200 is used.
Syntax of Database.executeBatch:
Database.executeBatch(myBatchObject, scope)
Executing a batch class named MyBatchClass:
MyBatchClass myBatchObject = new MyBatchClass(); Id batchId = Database.executeBatch(myBatchObject, 100);
Database.Stateful
A batch class is stateless by default. Every time the execute method is called, a new copy of the object is received, and all the variables of the class are initialized.
Database.Stateful is implemented to make a batch class stateful.
If your batch class implements the Database.Stateful interface, all the instance variables retain their values, but static variables are reset between transactions.
Asynchronous Apex Beyond Batch
Batch Apex is only one of several ways to run code asynchronously. Salesforce provides four asynchronous options, and choosing the right one depends on how much data you process and whether jobs need to be chained or scheduled.
- Future methods: Annotated with @future, these are best for simple, fire-and-forget operations such as callouts to external web services.
- Queueable Apex: Implements the Queueable interface, accepts complex object types, returns a job ID, and supports chaining one job to another.
- Batch Apex: Processes very large data volumes in chunks, as described above.
- Scheduled Apex: Implements the Schedulable interface so a class runs at a specific time, such as a nightly cleanup job.
| Type | Best For | Key Capability |
|---|---|---|
| Future method | Simple callouts | Fire and forget |
| Queueable Apex | Sequential processing | Job chaining and monitoring |
| Batch Apex | Millions of records | Chunked processing |
| Scheduled Apex | Recurring jobs | Cron-based timing |
Whichever execution model you choose, every transaction is still measured against the platform-wide governor limits listed below.
Apex Governor Limits
Apex governor limits are the limits enforced by the Apex runtime engine to ensure that any runaway Apex code and processes do not monopolize the shared resources and do not disrupt the processing of other users in the multi-tenant environment. These limits are verified against each Apex transaction. Following are the governor limits defined by Salesforce on each Apex transaction:
| Description | Limit |
|---|---|
| SOQL queries that can be done in a synchronous transaction | 100 |
| SOQL queries that can be done in an asynchronous transaction | 200 |
| Records that can be retrieved by a SOQL query | 50,000 |
| Records that can be retrieved by Database.getQueryLocator | 10,000 |
| SOSL queries that can be done in an Apex transaction | 20 |
| Records that can be retrieved by a SOSL query | 2,000 |
| DML statements that can be done in an Apex transaction | 150 |
| Records that can be processed as a result of a DML statement, Approval.process, or database.emptyRecycleBin | 10,000 |
| Callouts that can be done in an Apex transaction | 100 |
| Cumulative timeout limit on all the callouts that are being performed in an Apex transaction | 120 seconds |
| Limit on Apex jobs that can be added to the queue with System.enqueueJob | 50 |
| Execution time limit for each Apex transaction | 10 minutes |
| Limit on characters that can be used in an Apex class and trigger | 1 million |
| CPU time limit for a synchronous transaction | 10,000 milliseconds |
| CPU time limit for an asynchronous transaction | 60,000 milliseconds |
| Total heap size | 6 MB (synchronous) / 12 MB (asynchronous) |
How to Write a Test Class in Apex
Salesforce requires that at least 75 percent of your Apex code is covered by unit tests before you can deploy it to production, and every trigger must have some coverage. Writing test classes is therefore a core Apex skill, not an optional extra.
A test class is annotated with @isTest, and each test method is also marked with @isTest. Test methods do not commit data to the database and do not see most existing org data, so each test creates its own records. The Test.startTest() and Test.stopTest() methods give the code under test a fresh set of governor limits, and assertions verify that the logic behaved as expected.
Here is a simple test class for account creation logic:
@isTest
private class AccountHandlerTest {
@isTest
static void testCreateAccount() {
Account acc = new Account(Name = 'Test Account');
Test.startTest();
insert acc;
Test.stopTest();
Account result = [SELECT Id, Name FROM Account WHERE Id = :acc.Id];
System.assertEquals('Test Account', result.Name);
}
}
Follow these guidelines when writing tests:
- Use the @testSetup method to create shared test data once for all test methods in the class.
- Test bulk behavior by inserting 200 records, not just one.
- Cover positive, negative, and restricted-user scenarios.
- Always include meaningful System.assert statements; coverage without assertions proves nothing.
Apex Best Practices
Beginners often write Apex that works for one record but fails in real-world bulk operations. The practices below keep your code inside governor limits and easier to maintain:
- Bulkify everything: Write logic that handles collections of records, because triggers can receive up to 200 records in a single batch.
- Keep SOQL and DML out of loops: Query before the loop, collect changes in a list, and perform one DML statement after the loop.
- One trigger per object: Keep triggers logic-free and delegate the work to handler classes, which makes the order of execution predictable.
- Use with sharing: Enforce record-level security unless there is a documented reason to run in system context.
- Avoid hardcoding IDs: Record IDs differ between sandboxes and production, so query for them or use Custom Metadata instead.
- Monitor limits in code: The Limits class methods, such as Limits.getQueries(), let you check resource consumption at run time.
๐ก Tip: Run your code against 200 records in a sandbox before deploying. Most governor limit failures appear only at bulk volume, and catching them early is far cheaper than debugging them in production.






