Language Overview
Apex is Salesforce’s native programming language used to write custom business logic, automate workflows, and integrate with external systems — all directly within the Salesforce platform. It’s similar to Java in syntax, but purpose-built for Salesforce's multitenant, cloud-based environment.
Why Do We Need Apex?
- Declarative tools fall short: When Process Builder or Flows can't handle complex logic like nested loops, multi-object transactions, or conditional branching, Apex is the go-to.
- Complex custom automation: Create custom triggers to react to DML events such as insert, update, delete — for example, updating child records when a parent is modified.
- API integrations: Use Apex to call out to external systems using REST or SOAP services — e.g., push Opportunity data to an ERP system on record close.
- Batch and Scheduled Jobs: For large data volume processing or recurring tasks like monthly reports or cleanup jobs.
Features of Apex
- Database-Aware: Use SOQL/SOSL to query Salesforce records, and DML to create, update, or delete them.
- Event-Driven: React to changes using Triggers that fire before or after data changes.
- Asynchronous Support: Perform long-running tasks using future methods, Queueable, Batch Apex, or Schedulable classes.
- Reusable Code: Create Apex classes and methods that can be called from triggers, Flows, or Lightning Components.
- Secure and Scoped: All code runs with org-specific governor limits to avoid abuse in a shared environment.
Design Considerations Before You Build
- Start Simple: Could this be done with Flow or a Validation Rule instead? Always prefer declarative tools first.
- One Trigger per Object: Use handler classes to keep trigger logic clean and maintainable.
- Bulkify Always: Code must handle multiple records. Avoid queries or DML inside loops.
- Think of Governor Limits: E.g., max 100 SOQL queries per transaction. Optimize your code accordingly.
- Use Frameworks (Optional): For large orgs, consider Apex trigger frameworks like TDTM or fflib.
// Simple Apex Class: Count Contacts per Account
public class AccountProcessor {
public static void countContacts(List<Id> accountIds) {
// Fetch related contacts using a subquery
List<Account> accounts = [SELECT Id, (SELECT Id FROM Contacts)
FROM Account WHERE Id IN :accountIds];
for(Account acc : accounts) {
acc.Number_of_Contacts__c = acc.Contacts.size(); // Custom field
}
update accounts; // Persist updates
}
}
Development Tools
To write and deploy Apex code, Salesforce offers various tools for different skill levels. Whether you're building enterprise-grade apps or learning to code, the tools below will support your workflow.
Primary Development Tools
-
Visual Studio Code with Salesforce Extensions:
- Recommended for: Intermediate to advanced developers
- Provides features like syntax highlighting, IntelliSense, test execution, and deployment via Salesforce CLI
- Ideal for working with
SFDXprojects, source control (Git), and CI/CD pipelines - - scenario: Build a Lightning Web Component and Apex class, push to a scratch org, run tests — all from the terminal
-
Developer Console (built into Salesforce UI):
- Recommended for: Quick testing, small scripts, or admins learning code
- Includes SOQL Query Editor, Debug Logs viewer, Test Runner, and Logs Inspector
- - scenario: Debug a trigger failure by running test methods and inspecting logs directly
Developer Environment Setup
-
Step 1: Sign up for a Salesforce Developer Org
Create a free Salesforce Developer account at developer.salesforce.com/signup. This gives you your own playground with full API access and Apex support. -
Step 2: Install Visual Studio Code (VS Code)
Download from code.visualstudio.com and install the Salesforce Extension Pack from the Extensions marketplace. -
Step 3: Install Salesforce CLI
Visit developer.salesforce.com/tools/sfdxcli. CLI is required for creating orgs, authorizing logins, and running deployments from VS Code. -
Step 4: Authorize Your Org
Open VS Code terminal and run:It opens a browser to login. Once done, your org is connected to VS Code.sfdx auth:web:login --setalias DevOrg --instanceurl https://login.salesforce.com -
Step 5: Create Your First Project
Start writing Apex classes and components from here!sfdx force:project:create --projectname MyFirstProject
Data Types in Apex
Apex supports a variety of data types to help you manage different business logic needs. These include: primitive types (basic numeric/text values), sObjects (Salesforce records), collections (Lists, Maps, Sets), and custom classes.
Primitive Data Types (With Examples)
-
Integer: Whole numbers between -2,147,483,648 and 2,147,483,647
Integer totalLeads = 250;
Integer remaining = totalLeads - 50; -
Double: Decimal values with floating point precision
Double temperature = 36.75;
Double discount = 12.5 / 100; -
Long: Larger whole numbers than Integer, useful for large counters or timestamps
Long pageViews = 34567890123L;
Long userId = System.now().getTime();// Timestamp -
Decimal: High-precision decimal values ideal for currency
Decimal invoiceTotal = 1999.99;
Decimal tax = invoiceTotal * 0.18;
System.debug('Final Price: ' + (invoiceTotal + tax)); -
String: Text data enclosed in single quotes
String company = 'Acme Corporation';
String greeting = 'Hello ' + company + '!'; -
Boolean: Logical true/false values
Boolean isLoggedIn = true;
if (isLoggedIn) { System.debug('Welcome back!'); } -
Date, Datetime, Time: Useful for scheduling, time tracking, and date math
Date today = Date.today();
Datetime now = Datetime.now();
Time alarm = Time.newInstance(8, 30, 0, 0);
Date endDate = today.addDays(30); -
ID: Represents a Salesforce record's unique identifier (15 or 18 chars)
Id contactId = '0038Z00001ABCxY';
Contact c = [SELECT Name FROM Contact WHERE Id = :contactId]; -
Blob: Binary large objects—used for file content, images, encryption
Blob fileBody = Blob.valueOf('PDF content here');
String base64 = EncodingUtil.base64Encode(fileBody); -
Enum: Define your own fixed set of values for better control
public enum Status { Draft, Submitted, Approved }
Status current = Status.Submitted;
if (current == Status.Submitted) { System.debug('Pending review'); }
Variables in Apex
Variables in Apex are named containers used to store values that may change as your program runs. Think of them as labeled boxes that temporarily hold data. Each variable must be declared with a data type (such as String, Integer, or a custom object like Account), and the variable's accessibility depends on where it is defined—called its scope.
Key Concepts
-
Declaration: You must declare variables with a data type. Optionally, you can assign an initial value.
String name = 'Salesforce';
Boolean isActive = true;
Integer quantity; -
Scope: Determines where a variable can be accessed in your code:
Global / Public– accessible across classes or even from external packagesPrivate– accessible only within the same classLocal– declared within methods or code blocks and accessible only there
-
Reassignment: You can update a variable's value during execution.
Integer count = 1;
count = count + 1;
Types of Variable Scope (Examples)
-
1. Instance Variable (Class-level, used across multiple methods):
public class Cart {
Integer totalItems = 0;
public void addItem() {
totalItems++;
}
public Integer getTotalItems() {
return totalItems;
}
} -
2. Local Variable (Declared inside a method):
public void calculateTax() {
Decimal price = 100.0;
Decimal taxRate = 0.18;
Decimal tax = price * taxRate;
System.debug('Total Tax: ' + tax);
} -
3. Static Variable (Shared across all instances of the class):
public class Invoice {
public static Integer globalCounter = 0;
public Invoice() {
globalCounter++;
}
public static Integer getInvoiceCount() {
return globalCounter;
}
}Use case: Track total number of invoices created system-wide.
-
4. Final Variable (Read-only after initial assignment):
public void showLimit() {
final Integer MAX_LIMIT = 500;
// MAX_LIMIT = 600; // Compile-time error
System.debug('Limit is: ' + MAX_LIMIT);
} -
5. Global Variable (Rarely used; exposed to managed packages):
global class Library {
global String version = 'v1.0';
global void showVersion() {
System.debug(version);
}
}
- Example: Lead Assignment Tracker
// Class to track number of leads assigned to a user
public class LeadAssigner {
private Integer leadCount = 0;
public void assignLead(String repName) {
leadCount++;
System.debug('Assigned lead #' + leadCount + ' to: ' + repName);
}
}
// Usage:
LeadAssigner la = new LeadAssigner();
la.assignLead('Priya'); // Assigned lead #1 to: Priya
la.assignLead('Amit'); // Assigned lead #2 to: Amit
In this example, leadCount is a private instance variable. It retains its value across multiple method calls on the same object, which makes it perfect for tracking cumulative actions like assignments.
Bonus Example: Using Static Variable to Track All Leads Globally
public class GlobalLeadTracker {
public static Integer totalLeads = 0;
public static void addLead() {
totalLeads++;
System.debug('Total Leads in System: ' + totalLeads);
}
}
// Usage:
GlobalLeadTracker.addLead(); // Total Leads in System: 1
GlobalLeadTracker.addLead(); // Total Leads in System: 2
A static variable like totalLeads is useful when you want a count that applies across all users and sessions—like system-wide tracking of records.
Understanding where and how to define your variables in Apex is key to writing clean, maintainable, and efficient logic. Use the right variable scope to avoid bugs, unnecessary memory usage, and improve code readability.
Collections in Apex
Collections in Apex are powerful structures that let you store and manage multiple values together. There are three main types of collections: List, Set, and Map. These are commonly used in queries, loops, batch processing, and business logic.
1. List (Ordered Collection, Allows Duplicates)
A List is an ordered collection of elements. It allows duplicates and preserves insertion order. Think of it as a dynamic array that can grow or shrink as needed.
Useful List Methods:
.add(), .addAll(). .remove(index), .size(), .isEmpty(), .clear(), .contains()
// List Example: Managing a list of fruits
List fruits = new List{'Apple', 'Mango', 'Apple'};
// .add(): Add an item
fruits.add('Banana');
// .remove(index): Remove item at specific index
fruits.remove(1); // Removes 'Mango'
// .size(): Get number of elements
System.debug('Total fruits: ' + fruits.size()); // 3
// .isEmpty(): Check if list is empty
System.debug(fruits.isEmpty()); // false
// .clear(): Remove all elements
fruits.clear();
// .contains(): Check if a value exists
fruits.add('Orange');
System.debug(fruits.contains('Orange')); // true
// Access by index
System.debug(fruits[0]); // Orange
// Loop through list
for (String f : fruits) {
System.debug('Fruit: ' + f);
}
2. Set (Unordered, No Duplicates)
A Set is an unordered collection that ensures all elements are unique. Use it when duplicates are not allowed.
Useful Set Methods:
.add(), .contains(), .remove(), .size(), .isEmpty(), .clear()
// Set Example: Unique user IDs
Set userIds = new Set{101, 102, 102, 103}; // 102 appears only once
// .add(): Add value
userIds.add(104);
// .contains(): Check existence
System.debug(userIds.contains(101)); // true
// .remove(): Remove value
userIds.remove(102); // 102 removed
// .size(): Number of unique elements
System.debug('Total IDs: ' + userIds.size()); // 3
// .isEmpty(): Check if set is empty
System.debug(userIds.isEmpty()); // false
// .clear(): Remove all elements
userIds.clear();
System.debug('Now empty? ' + userIds.isEmpty()); // true
3. Map (Key-Value Pair)
A Map holds data in key-value pairs. Keys must be unique. Use it when you need fast access to a value using a specific key (like a dictionary).
Useful Map Methods:
.put(), .get(key), .containsKey(), .keySet(), .values(), .size(), .remove(key), .clear()
// Map Example: Track stock quantities
Map stock = new Map();
// .put(): Add or update key-value pair
stock.put('Apples', 100);
stock.put('Bananas', 50);
// .get(): Get value using key
System.debug('Apples in stock: ' + stock.get('Apples')); // 100
// .containsKey(): Check if key exists
System.debug(stock.containsKey('Bananas')); // true
// .keySet(): Get all keys
for (String item : stock.keySet()) {
System.debug('Item: ' + item);
}
// .values(): Get all values
for (Integer qty : stock.values()) {
System.debug('Quantity: ' + qty);
}
// .size(): Number of key-value pairs
System.debug('Total items: ' + stock.size()); // 2
// .remove(): Delete a key
stock.remove('Apples');
// .clear(): Remove everything
stock.clear();
System.debug('Is empty now? ' + stock.isEmpty()); // true
Combined - Use Case
Here's how you can use all three collections to assign leads to sales reps based on region.
// Step 1: Get unique regions using Set
Set regions = new Set();
for (Lead l : [SELECT Region__c FROM Lead WHERE CreatedDate = LAST_N_DAYS:30]) {
if (l.Region__c != null) {
regions.add(l.Region__c);
}
}
// Step 2: Assign reps to regions using Map
Map repByRegion = new Map{
'East' => 'Kunal',
'West' => 'Sara',
'North' => 'Amit'
};
// Step 3: Store output in a List
List assignments = new List();
// Step 4: Loop through regions and prepare message
for (String r : regions) {
if (repByRegion.containsKey(r)) {
String msg = 'Assign leads in ' + r + ' to ' + repByRegion.get(r);
assignments.add(msg);
System.debug(msg);
} else {
String msg = 'No rep assigned for region: ' + r;
assignments.add(msg);
System.debug(msg);
}
}
Conditional Statements in Apex
Conditional statements allow your Apex code to make decisions and take different actions based on specific conditions. These include if, if-else, else if, and switch statements.
They are essential when building logic flows, validations, and branching behavior in triggers, classes, and controllers.
Using if and if-else
The if statement checks whether a condition is true. You can extend it using else and else if blocks to handle alternative logic paths.
Integer score = 85;
if (score >= 90) {
System.debug('Grade: A');
} else if (score >= 75) {
System.debug('Grade: B');
} else if (score >= 60) {
System.debug('Grade: C');
} else {
System.debug('Grade: F');
}
// Output: Grade: B
Example: Opportunity Discount Check
Let’s say you want to apply a different message based on the discount given on an Opportunity record:
Decimal discount = 20;
if (discount > 30) {
System.debug('High discount - Needs Manager Approval');
} else if (discount > 10) {
System.debug('Medium discount - Auto-approved');
} else {
System.debug('Low discount - No approval required');
}
// Output: Medium discount - Auto-approved
Best Practice Tips for If-Else
- Avoid deeply nested
if-elseblocks; useswitchor early returns if possible. - Always handle the
elsecase to prevent unexpected behavior. - Use comparison operators like
==,!=,<,>=, etc. correctly.
Using switch Statement
The switch statement is useful when you want to compare a single variable against multiple possible values.
This improves readability over long chains of if-else if.
String paymentMode = 'UPI';
switch on paymentMode {
when 'CreditCard' {
System.debug('Processing Credit Card payment');
}
when 'UPI' {
System.debug('Processing UPI payment');
}
when 'NetBanking' {
System.debug('Processing Net Banking payment');
}
when else {
System.debug('Unknown payment method');
}
}
// Output: Processing UPI payment
Example: Support Case Priority Handling
Suppose you're handling support cases and want to log different messages based on priority levels:
String casePriority = 'High';
switch on casePriority {
when 'Low' {
System.debug('Log and assign to tier-1 support');
}
when 'Medium' {
System.debug('Notify supervisor and assign to tier-2 support');
}
when 'High' {
System.debug('Escalate immediately to tier-3');
}
when else {
System.debug('Unknown priority. Please review the record.');
}
}
// Output: Escalate immediately to tier-3
Switch Statement Tips
switchin Apex supports only onewhen elseblock, and it must come last.- You can match multiple values in a single
whenblock:when 'A', 'B' { ... }. - Only scalar types like
String,Integer,Enumare allowed inswitchconditions.
Example: Enum with Switch
Apex Enums are often used in switch for better readability. Here's an example using a custom enum:
public enum UserType { CUSTOMER, PARTNER, EMPLOYEE }
UserType type = UserType.PARTNER;
switch on type {
when CUSTOMER {
System.debug('Provide self-service portal access');
}
when PARTNER {
System.debug('Provide partner community access');
}
when EMPLOYEE {
System.debug('Provide internal access');
}
}
// Output: Provide partner community access
Summary
Conditional logic is at the heart of any business application. Apex offers both flexible if-else chains and clean switch statements for controlling flow.
Use them wisely to keep your logic readable and efficient. For complex or repetitive conditions, consider moving logic into reusable methods or handlers.
Loops in Apex
Loops in Apex allow you to execute a block of code repeatedly, either for a known number of times or while a certain condition remains true. Apex supports several types of loops including for, while, and do-while.
Loops are commonly used for bulk processing records, iterating through lists, and performing repetitive operations.
1. For Loop (Traditional)
A traditional for loop lets you iterate a specific number of times using an index variable.
// Print numbers 1 to 5
for (Integer i = 1; i <= 5; i++) {
System.debug('Number: ' + i);
}
2. For-Each Loop (List or Set)
This loop is used when iterating through collections like List, Set, or Map values.
// List of lead names
List<String> leadNames = new List<String>{'Anil', 'Priya', 'Ravi'};
for (String name : leadNames) {
System.debug('Lead Name: ' + name);
}
3. For Loop with SOQL (Inline Query Loop)
This special loop allows direct iteration over records from a SOQL query. It's memory efficient and best used with large datasets.
// Efficient way to loop over Accounts
for (Account acc : [SELECT Id, Name FROM Account WHERE CreatedDate = LAST_N_DAYS:30]) {
System.debug('Account: ' + acc.Name);
}
Best Practices for SOQL in Loops
- Do not put SOQL inside traditional
fororwhileloops; it can hit governor limits. - Use
for (sObject : [SOQL])directly when possible. - Always use selective filters in your query to avoid pulling too much data.
4. While Loop
A while loop runs as long as a condition is true. It checks the condition before every iteration.
Integer count = 0;
while (count < 3) {
System.debug('Count is: ' + count);
count++;
}
5. Do-While Loop
A do-while loop runs the code block once before checking the condition. It's useful when you need the code to run at least once regardless of the condition.
Integer number = 0;
do {
System.debug('Number is: ' + number);
number++;
} while (number < 3);
Example: Update Low-Value Opportunities
Let’s use a loop to iterate through Opportunity records and update the Stage if the amount is too low:
List<Opportunity> oppList = [SELECT Id, Amount, StageName FROM Opportunity WHERE StageName != 'Closed Won'];
for (Opportunity opp : oppList) {
if (opp.Amount < 5000) {
opp.StageName = 'Closed Lost';
}
}
update oppList;
Example: Bulk Lead Email Logging
Suppose you want to log emails for a list of leads, using a for loop:
List<Lead> leads = [SELECT Id, Name FROM Lead WHERE CreatedDate = LAST_N_DAYS:7];
List<Task> emailTasks = new List<Task>();
for (Lead l : leads) {
Task t = new Task(
Subject = 'Follow-up Email',
WhatId = l.Id,
Status = 'Not Started',
Priority = 'Normal'
);
emailTasks.add(t);
}
insert emailTasks;
Looping Tips and Governor Limits
- Apex has a limit of 100 SOQL queries and 150 DML operations per transaction — never put DML or SOQL inside a loop!
- Always bulkify your logic by processing collections instead of individual records.
- Use
breakorcontinuecautiously — they can make logic harder to follow.
Summary
Apex loops are essential for processing data efficiently in Salesforce. From traditional for loops to SOQL-based iterations, each loop has its ideal use case.
To write scalable and governor-limit-safe code, always use loops with best practices in mind—especially when working with large datasets or automation logic.
Classes and Objects in Apex
In Apex, classes and objects are the building blocks of object-oriented programming. A class is a blueprint that defines properties (variables) and behaviors (methods), while an object is a specific instance of that class. This model helps you build modular, reusable, and maintainable code.
Defining a Class
A basic class in Apex contains fields, constructors, and methods. Here's a simple example:
public class MyCar {
public String model;
// Constructor to initialize the model
public MyCar(String model) {
this.model = model;
}
// Method to simulate driving the car
public void drive() {
System.debug(model + ' is driving');
}
}
Creating and Using an Object
You can create an object from the class using the new keyword:
MyCar car1 = new MyCar('Mahindra XUV 700');
car1.drive(); // Output: Mahindra XUV 700 is driving
Another Example: Employee Class
Let’s create a class to represent an employee with a method to calculate annual salary:
public class Employee {
public String name;
public Decimal monthlySalary;
public Employee(String name, Decimal monthlySalary) {
this.name = name;
this.monthlySalary = monthlySalary;
}
public Decimal calculateAnnualSalary() {
return monthlySalary * 12;
}
}
// Usage
Employee emp = new Employee('Ravi Kumar', 50000);
System.debug(emp.name + '\'s annual salary is ₹' + emp.calculateAnnualSalary());
Understanding Object-Oriented Concepts
Apex supports key OOP principles:
- Encapsulation: Restrict access to class members using access modifiers like
privateorpublic. This protects internal data. - Inheritance: One class can reuse another class’s code using the
extendskeyword. - Polymorphism: Different classes can define their own version of the same method (overriding behavior).
Example: Inheritance & Polymorphism
// Parent class
public virtual class Animal {
public virtual void speak() {
System.debug('Animal speaks');
}
}
// Subclass 1
public class Dog extends Animal {
public override void speak() {
System.debug('Dog barks');
}
}
// Subclass 2
public class Cat extends Animal {
public override void speak() {
System.debug('Cat meows');
}
}
// Usage
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.speak(); // Output: Dog barks
a2.speak(); // Output: Cat meows
Access Modifiers Recap
- public: Visible everywhere in your org's Apex code.
- private: Visible only inside the same class.
- protected: Visible in the same class and any subclasses.
- global: Visible across namespaces (used for managed packages).
Static vs Instance Members
- Static Members: Shared across all objects. Called using class name.
- Instance Members: Specific to each object instance.
public class Counter {
public static Integer totalCount = 0;
public Integer individualCount = 0;
public void increment() {
totalCount++;
individualCount++;
}
}
// Usage
Counter c1 = new Counter();
Counter c2 = new Counter();
c1.increment();
c2.increment();
System.debug('Total count: ' + Counter.totalCount); // 2
System.debug('C1 count: ' + c1.individualCount); // 1
System.debug('C2 count: ' + c2.individualCount); // 1
With Sharing, Without Sharing, Inherited Sharing
Sharing rules in Apex control whether the class respects the record-level sharing rules of the current user. By default, Apex classes run in **system mode**, ignoring sharing rules. You can enforce or inherit them like this:
- with sharing: Enforces sharing rules for the user running the code.
- without sharing: Ignores sharing rules. Full access to all records.
- inherited sharing: Inherits the sharing settings of the class that called it.
// Class with enforced sharing rules
public with sharing class LeadHandler {
public static List getMyLeads() {
return [SELECT Name FROM Lead]; // Only leads user has access to
}
}
// Class without sharing (full access)
public without sharing class AdminUtility {
public static void deleteAllLeads() {
delete [SELECT Id FROM Lead]; // Ignores user sharing, can delete all
}
}
// Class that inherits caller's context
public inherited sharing class ServiceDispatcher {
public static void route() {
// Uses the sharing mode of the caller class
}
}
Best Practice: Always use with sharing when working with user-facing logic, especially in Lightning or Apex Controllers. Use inherited sharing for utility or domain classes to maintain context safety.
Summary
Apex classes and objects make it easy to organize business logic using OOP principles. Whether you're building reusable utilities or modeling - entities like Orders, Employees, or Vehicles, mastering classes is foundational to writing robust Apex code. Don’t forget to consider sharing context when building secure and scalable applications.
SOQL (Salesforce Object Query Language)
SOQL is used in Apex to query Salesforce records. It’s similar to SQL but designed specifically for Salesforce's data model.
You use it to retrieve data from standard or custom objects like Account, Contact, Opportunity, and any custom objects you create.
With SOQL, you can fetch only the fields you need, apply filters, sort records, and even join related objects in one query.
Basic Syntax
In Apex, you write SOQL queries inside square brackets [ ] and assign the result to a List of SObjects.
You typically use a SELECT clause to pick fields and a FROM clause to specify the object.
// Get Accounts with Industry populated
List<Account> accs = [SELECT Id, Name FROM Account WHERE Industry != NULL LIMIT 10];
Explanation: Fetches 10 Accounts where Industry is not null.
Here’s another example fetching Contacts created in the last 15 days:
List<Contact> recentContacts =
[SELECT Id, FirstName, LastName FROM Contact
WHERE CreatedDate = LAST_N_DAYS:15];
Filter and Sort Data
You can use WHERE to filter records, AND/OR for combining conditions, ORDER BY to sort, and LIKE for pattern matching.
// High-value closed Opportunities
List<Opportunity> opps =
[SELECT Id, Name, Amount FROM Opportunity
WHERE StageName = 'Closed Won' AND Amount > 100000
ORDER BY Amount DESC];
Example using LIKE and multiple filters:
// Get Accounts with name starting with 'Tata'
List<Account> tataAccounts =
[SELECT Id, Name FROM Account
WHERE Name LIKE 'Tata%' AND Industry = 'Manufacturing'];
Working with Related Objects (Relationship Queries)
SOQL supports object relationships:
Child-to-Parent (Dot Notation): Access fields from parent objects
Parent-to-Child (Subquery): Access related child records via subqueries
// Child to Parent: Contact to Account
List<Contact> contacts =
[SELECT Id, FirstName, LastName, Account.Name FROM Contact];
// Parent to Child: Account with related Contacts
List<Account> accounts =
[SELECT Id, Name, (SELECT FirstName, Email FROM Contacts) FROM Account];
Extra Example: Opportunities and their related Owner names:
List<Opportunity> opps =
[SELECT Id, Name, Owner.Name FROM Opportunity WHERE Amount > 50000];
Aggregate Functions
Use COUNT(), SUM(), AVG(), MIN(), MAX() to calculate summaries. Combine with GROUP BY and HAVING to filter groups.
// Count of Opportunities by Stage
AggregateResult[] results =
[SELECT StageName, COUNT(Id) total FROM Opportunity GROUP BY StageName];
for (AggregateResult ar : results) {
System.debug(ar.get('StageName') + ': ' + ar.get('total'));
}
Example: Total Sales per Sales Rep
// Sum of Opportunity Amount grouped by Owner
AggregateResult[] salesByRep =
[SELECT Owner.Name, SUM(Amount) totalSales FROM Opportunity
WHERE StageName = 'Closed Won' GROUP BY Owner.Name];
for (AggregateResult ar : salesByRep) {
System.debug(ar.get('Owner.Name') + ': ₹' + ar.get('totalSales'));
}
Dynamic SOQL
Dynamic SOQL allows building queries at runtime. Useful in generic Apex code or admin utilities.
// Dynamic query using variable object
String objectName = 'Lead';
String field = 'Company';
String value = 'Infosys';
String query = 'SELECT Id, Name FROM ' + objectName +
' WHERE ' + field + ' = \'' + value + '\'';
List<SObject> leads = Database.query(query);
Another Example: Dynamically fetch Accounts by Industry
String ind = 'Energy';
String queryStr = 'SELECT Id, Name FROM Account WHERE Industry = \'' + ind + '\'';
List<SObject> accs = Database.query(queryStr);
⚠️ Security Note: Always sanitize inputs to avoid SOQL injection attacks when building dynamic queries.
Best Practices
- Use SELECT fields you actually need — avoid SELECT *
- Use LIMIT to restrict result size and improve performance
- Always filter with WHERE to avoid querying unnecessary data
- Never write SOQL inside loops — bulkify your logic
- Index filter fields when dealing with large data volumes
SOQL Examples
// Get top 5 Accounts created this month
List<Account> topAccounts =
[SELECT Id, Name FROM Account
WHERE CreatedDate = THIS_MONTH
ORDER BY CreatedDate DESC LIMIT 5];
// Get all Contacts in Mumbai
List<Contact> mumbaiContacts =
[SELECT Id, FirstName, City FROM Contact
WHERE MailingCity = 'Mumbai'];
// Count Leads grouped by Company
AggregateResult[] leadsByCompany =
[SELECT Company, COUNT(Id) FROM Lead GROUP BY Company];
SOQL is one of the most powerful tools in Apex. Mastering it means faster queries, scalable code, and more efficient data retrieval across your Salesforce org.
SOSL (Salesforce Object Search Language)
SOSL is used to search text across multiple objects and fields in a single query. Unlike SOQL, which targets one object at a time, SOSL is powerful for global keyword searches. It is ideal when you're unsure where the data resides or want to implement a global search bar.
Basic Syntax
SOSL queries return a list of lists. Each inner list contains records of a specific SObject type. The structure allows you to search and retrieve data from multiple objects at once.
// Search all fields for 'Smith' across Contact and Account
List<List<SObject>> searchResults =
[FIND 'Smith*' IN ALL FIELDS
RETURNING Contact(Id, FirstName, LastName),
Account(Id, Name)];
This retrieves any Contact or Account record where any field starts with "Smith".
How to Access Results
List<Contact> contactsFound = (List<Contact>) searchResults[0];
List<Account> accountsFound = (List<Account>) searchResults[1];
System.debug('Found ' + contactsFound.size() + ' Contacts');
System.debug('Found ' + accountsFound.size() + ' Accounts');
Each list is indexed in the order the SObjects appear in the RETURNING clause.
Advanced: Specify Fields or Objects
// Search only in Name fields of Contact and Lead
List<List<SObject>> results =
[FIND 'Kumar' IN Name Fields
RETURNING Contact(Id, Name),
Lead(Id, Company, Email)];
This improves performance by restricting the search scope to Name fields only.
// Example on custom object
List<List<SObject>> results =
[FIND 'Tech' IN ALL FIELDS
RETURNING Product__c(Id, Name, Category__c)];
This searches all fields in the custom object Product__c for the keyword "Tech".
Dynamic SOSL
You can use dynamic SOSL when building queries at runtime, especially in Lightning Components or Apex Controllers.
String keyword = 'Sharma*';
String dynamicSOSL = 'FIND \'' + keyword + '\' IN ALL FIELDS RETURNING Contact(Id, Name)';
List<List<SObject>> results = Search.query(dynamicSOSL);
Always escape and validate input to avoid SOSL injection attacks.
Best Use Cases
- Implementing a global search feature across Contacts, Accounts, and Leads.
- Searching when you don't know the exact object or field the keyword resides in.
- Customer support portals or Lightning quick find components.
- Mass search scenarios like import deduplication, auditing, or reconciliation processes.
SOQL vs SOSL
- SOQL: Query records from a single object or related objects with filters.
- SOSL: Perform full-text search across multiple objects and fields.
- SOQL is used when object and field are known; SOSL is used for broader discovery.
- SOQL supports aggregations, joins, and relationships; SOSL supports fast keyword-based search.
Example
// Global search in a service portal
String searchTerm = 'David';
List<List<SObject>> matches =
[FIND :searchTerm IN ALL FIELDS
RETURNING Contact(Id, FirstName, LastName),
Case(Id, Subject),
Lead(Id, Name, Company)];
Ideal for search bars where the user could be a Contact, a Lead, or referenced in a Case.
DML Statements in Apex
DML (Data Manipulation Language) in Apex is used to perform operations like inserting, updating, deleting, or restoring records in Salesforce. These statements interact directly with the database and are subject to governor limits. Understanding and optimizing your DML usage is critical for scalable and efficient Apex development.
1. insert – Create New Records
Use the insert statement to add one or more new records into the database.
// Insert a single Account
Account acc = new Account(Name = 'Cloud Inc');
insert acc;
// Insert multiple Contacts
List<Contact> contacts = new List<Contact>{
new Contact(FirstName='Riya', LastName='Sen', Email='riya@cloud.com'),
new Contact(FirstName='Anil', LastName='Patel', Email='anil@cloud.com')
};
insert contacts;
// Insert a custom object record
Student__c student = new Student__c(Name='Ajay Sharma', Grade__c='10th');
insert student;
2. update – Modify Existing Records
Use update to modify values in existing records. Records must already exist in the database (i.e., they have valid Ids).
// Update the Account name
acc.Name = 'Cloud Technologies';
update acc;
// Update multiple Contacts
for (Contact con : contacts) {
con.Phone = '9999999999';
con.Title = 'Manager';
}
update contacts;
// Example: Mark Opportunities as Closed Lost
List<Opportunity> opps = [SELECT Id, StageName FROM Opportunity WHERE StageName != 'Closed Won'];
for (Opportunity opp : opps) {
opp.StageName = 'Closed Lost';
}
update opps;
3. delete – Remove Records
Use delete to remove records from Salesforce. Deleted records are soft deleted (go to the Recycle Bin) unless they are hard deleted by system-level users.
// Delete a single Account
delete acc;
// Delete multiple Contacts
delete contacts;
// Delete all leads created by a specific user
List<Lead> userLeads = [SELECT Id FROM Lead WHERE CreatedById = :UserInfo.getUserId()];
delete userLeads;
4. upsert – Insert or Update Based on Id or External Id
upsert is a powerful operation that inserts a record if it doesn’t exist, or updates it if it does. You can upsert using either the record Id or an external Id field defined in Salesforce.
// Using record Id
Account acc2 = new Account(Id = acc.Id, Name = 'Cloud Hub');
upsert acc2;
// Using external Id
Contact c = new Contact(FirstName='Deepak', LastName='Joshi');
c.External_Id__c = 'EXT123'; // Custom External Id field
upsert c External_Id__c;
// Bulk upsert
List<Student__c> students = new List<Student__c>{
new Student__c(External_Ref__c='STU001', Name='Rahul'),
new Student__c(External_Ref__c='STU002', Name='Priya')
};
upsert students External_Ref__c;
5. undelete – Restore Deleted Records
Use undelete to recover records from the Recycle Bin. It works within 15 days after deletion.
// Undelete previously deleted Contacts
undelete contacts;
// Restore deleted leads
List<Lead> deletedLeads = [SELECT Id, Name FROM Lead WHERE IsDeleted = TRUE ALL ROWS];
undelete deletedLeads;
Using DML with Error Handling
To avoid runtime exceptions due to failed DML operations, use Database methods such as Database.insert(), Database.update(), and so on. These methods return detailed results and give you more control over partial success.
// Safe DML using Database class
Database.SaveResult result = Database.insert(acc, false);
if (!result.isSuccess()) {
for (Database.Error err : result.getErrors()) {
System.debug('Insert failed: ' + err.getMessage());
}
}
Governor Limits for DML
- Maximum DML statements per transaction: 150
- Maximum records processed per DML: 10,000
- Combine records into a single DML where possible to optimize performance
Best Practices
- Always bulkify your code. Use Lists to avoid hitting limits.
- Use
Databasemethods for better control and error tracking. - Never perform DML operations inside
forloops. - Group logic before executing DML to reduce statements.
- Use
try-catchfor exception safety when required.
Database Methods in Apex
The Database class in Apex offers advanced control over DML operations. Unlike traditional DML statements (insert, update, delete), these methods allow you to:
- Handle partial success without halting the entire execution
- Capture and log detailed errors for each record
- Choose whether to rollback changes using the
allOrNoneflag - Use external IDs for intelligent
upsertoperations
Why Use Database Methods?
Database methods are ideal for bulk operations or when you want graceful error handling. For instance, when inserting 500 student records from a form, you wouldn’t want the whole process to fail due to one invalid entry. Instead, you can allow partial success and fix only the problematic data.
Example: Insert with Partial Success
This example inserts multiple Accounts, with some intentionally invalid, and handles each result individually.
// One valid, one invalid Account (Name is required)
List<Account> accList = new List<Account>{
new Account(Name = 'Cloud Factory'),
new Account()
};
Database.SaveResult[] results = Database.insert(accList, false);
for (Database.SaveResult r : results) {
if (r.isSuccess()) {
System.debug('Inserted: ' + r.getId());
} else {
for (Database.Error err : r.getErrors()) {
System.debug('Insert Error: ' + err.getMessage());
}
}
}
Update with Error Handling
When updating customer records, some may have missing fields. You can prevent one failure from stopping all updates.
// Updating patient records in a hospital system
List<Account> accsToUpdate = [SELECT Id, Name FROM Account LIMIT 2];
accsToUpdate[0].Name = null; // Invalid
accsToUpdate[1].Name = 'Valid Update';
Database.SaveResult[] updResults = Database.update(accsToUpdate, false);
for (Database.SaveResult r : updResults) {
if (r.isSuccess()) {
System.debug('Updated: ' + r.getId());
} else {
for (Database.Error err : r.getErrors()) {
System.debug('Update Error: ' + err.getMessage());
}
}
}
Upsert Using External Id
External IDs let you integrate data from outside systems. If a matching External ID exists, Salesforce updates it; otherwise, it creates a new record.
// Use case: Syncing student records from an external portal
Contact c = new Contact(FirstName = 'Rahul', LastName = 'Mehra');
c.External_Id__c = 'STU-1023';
Database.UpsertResult result = Database.upsert(c, 'External_Id__c', false);
if (result.isSuccess()) {
System.debug('Upserted Contact: ' + result.getId());
} else {
for (Database.Error err : result.getErrors()) {
System.debug('Upsert Error: ' + err.getMessage());
}
}
Delete with Partial Handling
Instead of failing the entire delete operation, partial handling allows you to skip invalid records and delete what’s valid.
List<Account> accsToDelete = [SELECT Id FROM Account LIMIT 2];
accsToDelete.add(new Account()); // Invalid record (no Id)
Database.DeleteResult[] delResults = Database.delete(accsToDelete, false);
for (Database.DeleteResult r : delResults) {
if (r.isSuccess()) {
System.debug('Deleted: ' + r.getId());
} else {
for (Database.Error err : r.getErrors()) {
System.debug('Delete Error: ' + err.getMessage());
}
}
}
Available Database Methods
Database.insert()– Safe insertion with error feedbackDatabase.update()– Update records and monitor individual success/failureDatabase.delete()– Remove records while skipping invalid onesDatabase.upsert()– Insert or update intelligently based on Id or External IdDatabase.undelete()– Restore soft-deleted records from Recycle BinDatabase.merge()– Merge duplicate records (only for Leads, Contacts, Accounts)Database.convertLead()– Convert leads into Contacts/Accounts/OpportunitiesDatabase.rollback()– Revert changes to a savepoint
Best Practices
- Always set
allOrNone = falseto prevent one bad record from ruining the batch. - Use
isSuccess()andgetErrors()for clean error management. - Log errors to a custom object or platform event for traceability in production.
- Avoid writing
Database.*calls insideforloops. Collect records, then execute once. - Use
SavePointandDatabase.rollback()when you need transactional control.
Trigger
Apex Triggers are like little behind-the-scenes assistants — they quietly react to database changes and automate tasks without anyone pressing a button. These events include inserting, updating, deleting, or even undeleting records.
Triggers help enforce business rules, auto-fill fields, create related records, or send alerts the moment something changes in your data. They keep your Salesforce org smart and responsive.
Trigger Syntax Explained
Here's the basic structure of any trigger:
trigger <triggerName> on <ObjectName> (<triggerEvents>) {
// Your trigger logic here
}
- triggerName: Any name you choose (e.g., AccountTrigger)
- ObjectName: The object it runs on (e.g., Account, Contact)
- triggerEvents: Events like before insert, after update, etc.
Basic Trigger Template
A single trigger can listen to multiple events, like this:
trigger AccountTrigger on Account (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
if (Trigger.isBefore && Trigger.isInsert) {
// Set default values
}
if (Trigger.isAfter && Trigger.isUpdate) {
// Notify user of updates
}
if (Trigger.isBefore && Trigger.isDelete) {
// Stop deletion if business rule fails
}
if (Trigger.isAfter && Trigger.isDelete) {
// Log deleted account
}
if (Trigger.isAfter && Trigger.isUndelete) {
// Alert team that record was restored
}
}
Examples
- Auto-fill Fields: Assign a default Rating to Accounts
- Duplicate Prevention: Stop duplicate Contacts by checking email/phone
- Auto-Creation: Automatically create a Case when Opportunity is Closed Lost
- Sync Records: Update Contacts if their Account name changes
- Clean-Up: Delete custom child records when parent is deleted
Trigger Best Practice Example
trigger ContactTrigger on Contact (before insert, after update) {
if (Trigger.isBefore && Trigger.isInsert) {
for (Contact con : Trigger.new) {
if (con.FirstName == null) {
con.FirstName = 'Guest';
}
}
}
if (Trigger.isAfter && Trigger.isUpdate) {
for (Contact con : Trigger.new) {
System.debug('Updated Contact ID: ' + con.Id);
}
}
}
Example: Auto-Assign Rating
Assign a default Rating of "Warm" to new Accounts without one:
trigger AccountTrigger on Account (before insert) {
for(Account acc : Trigger.new) {
if(acc.Rating == null) {
acc.Rating = 'Warm';
}
}
}
Example: Prevent Duplicate Contacts
Prevent insertion of Contacts with duplicate Email or Phone (skip nulls):
trigger ContactTrigger on Contact (before insert) {
Set<String> emailSet = new Set<String>();
Set<String> phoneSet = new Set<String>();
for (Contact con : Trigger.new) {
if (con.Email != null) emailSet.add(con.Email.toLowerCase());
if (con.Phone != null) phoneSet.add(con.Phone);
}
List<Contact> existing = [SELECT Id, Email, Phone FROM Contact
WHERE Email IN :emailSet OR Phone IN :phoneSet];
for (Contact con : Trigger.new) {
for (Contact exist : existing) {
if ((exist.Email != null && exist.Email.equalsIgnoreCase(con.Email)) ||
(exist.Phone != null && exist.Phone == con.Phone)) {
con.addError('Duplicate Contact found with same Email or Phone.');
}
}
}
}
Example: Auto-Create Case on Opportunity Closed Lost
When an Opportunity is marked Closed Lost, auto-create a Case to follow-up:
trigger OpportunityTrigger on Opportunity (after update) {
List<Case> caseList = new List<Case>();
for (Opportunity opp : Trigger.new) {
Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
if (opp.StageName == 'Closed Lost' && oldOpp.StageName != 'Closed Lost') {
caseList.add(new Case(
Subject = 'Follow-up for Lost Deal',
Description = 'Auto-created due to lost opportunity: ' + opp.Name,
Status = 'New',
Origin = 'Salesforce',
AccountId = opp.AccountId
));
}
}
if (!caseList.isEmpty()) insert caseList;
}
Example: Sync Contacts on Account Name Change
If an Account's name changes, reflect it in related Contact's Description field:
trigger AccountTrigger on Account (after update) {
Set<Id> changedAccIds = new Set<Id>();
for (Account acc : Trigger.new) {
Account oldAcc = Trigger.oldMap.get(acc.Id);
if (acc.Name != oldAcc.Name) {
changedAccIds.add(acc.Id);
}
}
List<Contact> contactsToUpdate = new List<Contact>();
for (Contact con : [SELECT Id, AccountId, Description FROM Contact
WHERE AccountId IN :changedAccIds]) {
con.Description = 'Updated due to Account name change';
contactsToUpdate.add(con);
}
if (!contactsToUpdate.isEmpty()) update contactsToUpdate;
}
Example: Delete Child Records When Parent is Deleted
When a custom parent record (like Project__c) is deleted, also delete its Task__c child records:
trigger ProjectTrigger on Project__c (after delete) {
List<Task__c> tasksToDelete = [SELECT Id FROM Task__c
WHERE Project__c IN :Trigger.oldMap.keySet()];
if (!tasksToDelete.isEmpty()) delete tasksToDelete;
}
Why Use Triggers Instead of Workflow or Flow?
- More Control: You can write complex conditions and logic in code
- Cross-Object Logic: Easily update or read related records
- Error Handling: Add try-catch blocks to manage failures gracefully
- Flexibility: Ideal for dynamic logic, queries, and multiple DML operations
Trigger Context Variables
Apex provides built-in context variables inside triggers to help you identify when and how the trigger is executing. These variables give you access to the records and their states — so you can write logic that behaves differently in each scenario.
Trigger.isBefore– True if the trigger runs before the record is saved to the databaseTrigger.isAfter– True if the trigger runs after the record is savedTrigger.isInsert– True if the operation is an insertTrigger.isUpdate– True if the operation is an updateTrigger.isDelete– True if the operation is a deleteTrigger.isUndelete– True if records are being restored from Recycle BinTrigger.new– List of the new record values (available in insert, update, undelete)Trigger.old– List of the old record values (available in update, delete)Trigger.newMap– A map of new records by ID (useful in update and after insert)Trigger.oldMap– A map of old records by ID (useful in update and delete)
Example: Detect Field Changes
Let’s compare the new and old values to detect if the Rating field has changed:
trigger AccountTrigger on Account (before update) {
for (Account acc : Trigger.new) {
Account oldAcc = Trigger.oldMap.get(acc.Id);
if (acc.Rating != oldAcc.Rating) {
System.debug('Rating changed from ' + oldAcc.Rating + ' to ' + acc.Rating);
}
}
}
Example: Alert on Email Change
Track when a Contact's email address is updated:
trigger ContactTrigger on Contact (after update) {
for (Contact con : Trigger.new) {
Contact oldCon = Trigger.oldMap.get(con.Id);
if (con.Email != oldCon.Email) {
System.debug('Email changed for ' + con.LastName +
' from ' + oldCon.Email + ' to ' + con.Email);
}
}
}
Example: Prevent Deletion Based on Condition
Stop deletion of Accounts that are marked as Type = "Customer - Direct":
trigger AccountTrigger on Account (before delete) {
for (Account acc : Trigger.old) {
if (acc.Type == 'Customer - Direct') {
acc.addError('You cannot delete a direct customer account.');
}
}
}
Trigger Best Practices
Triggers can be powerful, but without structure, they quickly become unmanageable. These best practices help you write clean, efficient, and scalable triggers — so your Salesforce org doesn’t spiral into chaos as it grows.
Key Best Practices
- One Trigger Per Object: Avoid multiple triggers on the same object. It keeps execution order predictable and easier to debug.
- Use Trigger Handler Classes: Move business logic to a separate Apex class. This makes code modular, reusable, and testable.
- Bulkify Your Code: Always write your code to handle 200+ records at once — even if today you're dealing with one. Triggers are always bulk.
- Avoid SOQL/DML Inside Loops: Doing DML or queries inside loops can cause governor limit exceptions. Use collections like Lists, Sets, and Maps outside the loop.
- Use Maps for Efficient Lookups: When matching or comparing records, Maps save CPU time and reduce SOQL calls.
- Use Context Variables Carefully:
Use
Trigger.isInsert,Trigger.isBefore, etc., to organize logic for different operations cleanly. - Avoid Recursive Updates: Prevent your trigger from calling itself again and again using static variables or flags.
- Test for Bulk and Edge Cases: Write test methods that simulate real-world scenarios: bulk updates, missing fields, and limit conditions.
Trigger Handler Example
Here’s how to move logic to a separate handler class. It keeps the trigger lean and business logic isolated — making it easy to manage and test.
// Handler class
public class AccountTriggerHandler {
public static void handleBeforeInsert(List<Account> newList) {
for (Account acc : newList) {
if (acc.Rating == null) {
acc.Rating = 'Warm';
}
}
}
public static void handleAfterUpdate(List<Account> newList, Map<Id, Account> oldMap) {
for (Account acc : newList) {
Account oldAcc = oldMap.get(acc.Id);
if (acc.Rating != oldAcc.Rating) {
System.debug('Rating changed from ' + oldAcc.Rating + ' to ' + acc.Rating);
}
}
}
}
// Trigger file
trigger AccountTrigger on Account (before insert, after update) {
if (Trigger.isBefore && Trigger.isInsert) {
AccountTriggerHandler.handleBeforeInsert(Trigger.new);
}
if (Trigger.isAfter && Trigger.isUpdate) {
AccountTriggerHandler.handleAfterUpdate(Trigger.new, Trigger.oldMap);
}
}
Asynchronous Apex Overview
Asynchronous Apex lets you execute operations that take a long time to process—like external callouts, complex calculations, or massive DML updates—without blocking the user's screen or hitting strict synchronous governor limits. It helps you scale efficiently and keep the user experience smooth.
Why Use Asynchronous Apex?
- Work around governor limits: Async methods get higher CPU time (up to 60 seconds) and separate limits.
- Enable callouts from triggers: You can't do HTTP callouts directly in a trigger—use async instead.
- Handle mixed DML operations: Async Apex separates setup and non-setup objects, avoiding mixed DML errors.
- Process large volumes of data: Batch Apex lets you process millions of records without timing out.
- Run tasks on schedule: Scheduled Apex runs code at set intervals (daily, weekly, monthly, etc.).
Async Apex Options
- @future methods: For lightweight, fire-and-forget jobs like API calls or basic updates.
- Queueable Apex: Better than @future—supports complex data types, chaining, and retry logic.
- Batch Apex: Splits large data sets into chunks and processes them asynchronously. Ideal for data-heavy jobs.
- Scheduled Apex: Schedules your Apex logic to run at specific times using cron expressions.
How the Async Queue Works
When you enqueue an async job, Salesforce adds it to a queue. The job waits until resources are available, then runs in the background. This guarantees fair distribution and reliability, even during high-load periods.
Use Cases
- @future: Send a welcome email or push data to an ERP after a record is inserted.
- Queueable: Assign leads based on custom rules, then notify sales reps in a chained job.
- Batch Apex: Clean up unused records weekly, enrich leads from an external system, or calculate loyalty points.
- Scheduled Apex: Run monthly billing, deactivate stale users nightly, or sync product inventory daily.
Choosing the right async tool depends on your needs. Start with Queueable for flexibility, Batch for large data, and Scheduled when time-based automation is key.
Future Methods
Future methods in Apex allow you to perform time-consuming or resource-heavy operations asynchronously. Instead of forcing users to wait while the backend completes a task, you can push that work into the background, creating smoother and faster user experiences.
You define future methods using the @future annotation. These methods are best suited for tasks like making HTTP callouts, running background updates, and avoiding mixed DML errors.
Key Characteristics
- Run asynchronously in a separate thread after the current transaction finishes
- Must be declared
staticand returnvoid - Can only accept primitive types, collections of primitives, or objects that implement
Serializable - Cannot be chained or nested (i.e., one future method cannot call another)
- Support callouts when annotated as
@future(callout=true) - Do not guarantee execution order or exact timing
Basic Syntax
public class FutureExample {
@future
public static void logAccountName(String accId) {
Account acc = [SELECT Name FROM Account WHERE Id = :accId];
System.debug('Account Name: ' + acc.Name);
}
}
Example 1: Callout to External API
Future methods support HTTP callouts, which are often needed for integrating with third-party services.
public class CalloutHandler {
@future(callout=true)
public static void fetchDataFromAPI(String url) {
Http http = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint(url);
req.setMethod('GET');
HttpResponse res = http.send(req);
System.debug('Response: ' + res.getBody());
}
}
Example 2: Assigning Default Values to Accounts
Assign a default value to account ratings in bulk without blocking the transaction.
public class AccountProcessor {
@future
public static void assignDefaultRating(List<Id> accountIds) {
List<Account> accountsToUpdate = [SELECT Id, Rating FROM Account WHERE Id IN :accountIds];
for(Account acc : accountsToUpdate) {
if(acc.Rating == null) {
acc.Rating = 'Warm';
}
}
update accountsToUpdate;
}
}
Example 3: Logging Trigger-Based Activities
Calling a future method from a trigger ensures you're not blocked by limits during data creation.
trigger AccountTrigger on Account (after insert) {
for(Account acc : Trigger.new) {
FutureExample.logAccountName(acc.Id);
}
}
Example 4: Logging Custom Events for Analytics
Use a future method to insert a custom log entry into an Analytics object without delaying a user transaction.
public class AnalyticsLogger {
@future
public static void logEvent(String eventType, String details) {
Event__c evt = new Event__c(Event_Type__c = eventType, Details__c = details);
insert evt;
}
}
Best Practices
- Don't call from loops—collect data first, then pass it in bulk
- Stay within 50 future calls per transaction
- Use Queueable Apex when you need to pass complex types or chain jobs
- Don't rely on immediate execution—jobs may be delayed
- Avoid nesting or chaining future methods
Common Use Cases
- Send welcome or verification emails after user registration
- Make HTTP callouts to external APIs (e.g., update user info in marketing tool)
- Process DML updates or inserts that must happen outside the original transaction
- Log analytics or audit data post user actions
- Run background enrichment based on third-party data sources
- Update related child records (e.g., auto-close tasks on parent record status change)
In conclusion, future methods are great for lightweight asynchronous tasks. If you find yourself needing more flexibility—like chaining jobs or working with complex input—it's better to use Queueable Apex.
Batch Apex
Batch Apex allows you to process thousands to millions of records in an asynchronous and scalable way. It’s perfect for long-running jobs like cleaning up data, performing mass updates, or integrating with external systems. Since each batch runs in its own transaction, it protects against hitting governor limits and enables better error recovery.
To use Batch Apex, your class must implement the Database.Batchable<SObject> interface, which includes three critical methods:
- start: Prepares the data to be processed.
- execute: Performs the logic on each batch of records.
- finish: Executes post-processing logic (like logging or triggering another job).
Batch Class Structure
global class AccountUpdateBatch implements Database.Batchable<sObject> {
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id, Name FROM Account');
}
global void execute(Database.BatchableContext bc, List<Account> scope) {
for(Account acc : scope) {
acc.Name += ' - Updated';
}
update scope;
}
global void finish(Database.BatchableContext bc) {
System.debug('Batch job finished');
}
}
Executing a Batch
Instantiate your batch class and call Database.executeBatch(). You can optionally specify a batch size to control how many records are processed per chunk.
// Run the batch with 200 records per batch
AccountUpdateBatch batch = new AccountUpdateBatch();
Database.executeBatch(batch, 200);
Example 1: Deactivating Old Contacts
Suppose you need to deactivate contacts not updated in over 5 years. Batch Apex helps you do this without breaching limits.
global class ContactCleanupBatch implements Database.Batchable<sObject> {
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([
SELECT Id, IsActive__c FROM Contact WHERE LastModifiedDate < LAST_N_YEARS:5
]);
}
global void execute(Database.BatchableContext bc, List<Contact> scope) {
for(Contact c : scope) {
c.IsActive__c = false;
}
update scope;
}
global void finish(Database.BatchableContext bc) {
System.debug('Inactive contacts updated.');
}
}
Example 2: Archiving Closed Opportunities
This example marks closed opportunities older than 2 years as archived by setting a custom field.
global class OpportunityArchiver implements Database.Batchable<sObject> {
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([
SELECT Id, Archived__c FROM Opportunity
WHERE StageName = 'Closed Won' AND CloseDate < LAST_N_YEARS:2
]);
}
global void execute(Database.BatchableContext bc, List<Opportunity> scope) {
for(Opportunity opp : scope) {
opp.Archived__c = true;
}
update scope;
}
global void finish(Database.BatchableContext bc) {
System.debug('Old opportunities archived.');
}
}
Example 3: Stateful Batch for Aggregation
You can track running totals or shared values using the Database.Stateful interface.
global class LeadCounterBatch implements Database.Batchable<sObject>, Database.Stateful {
public Integer totalCount = 0;
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id FROM Lead');
}
global void execute(Database.BatchableContext bc, List<Lead> scope) {
totalCount += scope.size();
}
global void finish(Database.BatchableContext bc) {
System.debug('Total leads processed: ' + totalCount);
}
}
Best Practices
- Keep logic in
execute()efficient and bulk-safe - Do not use future or queueable calls inside
execute() - Send alerts or logs in the
finish()method - Use
Database.Statefulto preserve state across batches - Chain multiple batches using
finish()for dependent jobs
Common Use Cases
- Mass updating or cleaning records
- Deactivating stale users or contacts
- Archiving opportunities or cases
- Sending time-based summary emails
- Performing asynchronous integrations
Batch Apex is your go-to solution for scalable, robust data processing in Salesforce. Whether you're cleaning data, performing audits, or syncing external systems, Batch Apex gives you the control and power to handle it efficiently within platform limits.
Queueable Apex
Queueable Apex is a powerful tool in Salesforce for running asynchronous operations. It bridges the gap between Future methods and Batch Apex by offering more flexibility, such as passing complex data types, chaining jobs, and handling exceptions elegantly. It's ideal for background tasks that need finer control, multiple steps, or processing medium-sized data sets.
To use it, implement the Queueable interface and call System.enqueueJob(). Each job runs in a separate transaction, making it robust for operations that might exceed synchronous limits.
Key Features
- Supports passing complex data structures (Lists, Maps, custom objects)
- Allows chaining multiple jobs with sequential execution
- Returns Job ID for status monitoring
- Better exception handling using
try-catch - Each chained job runs in a new transaction
Basic Queueable Example
public class AccountQueueable implements Queueable {
private List<Account> accounts;
public AccountQueueable(List<Account> accountsToProcess) {
this.accounts = accountsToProcess;
}
public void execute(QueueableContext context) {
for(Account acc : accounts) {
acc.Name += ' - Queued';
}
update accounts;
// Optional: Chain another job
if(accounts.size() > 100) {
System.enqueueJob(new NotifyUserQueueable());
}
}
}
Enqueueing the Job
List<Account> accs = [SELECT Id, Name FROM Account WHERE CreatedDate = TODAY];
System.enqueueJob(new AccountQueueable(accs));
Use Case 1: Logging Lead Activity and Sending Notifications
Suppose you want to create tasks for a list of leads and send a summary email afterward. Doing this synchronously could cause limits. Queueable Apex makes it seamless.
public class LeadActivityLogger implements Queueable {
private List<Lead> leads;
public LeadActivityLogger(List<Lead> leads) {
this.leads = leads;
}
public void execute(QueueableContext context) {
List<Task> tasks = new List<Task>();
for(Lead l : leads) {
tasks.add(new Task(
WhatId = l.Id,
Subject = 'Initial Follow-up',
Status = 'Not Started'
));
}
insert tasks;
System.enqueueJob(new NotifyManagerQueueable());
}
}
Use Case 2: Archiving Opportunities with Chaining
Imagine a scenario where you want to archive closed-lost opportunities and then log a summary. Queueable Apex lets you chain these jobs neatly.
public class ArchiveOpportunitiesQueueable implements Queueable {
public void execute(QueueableContext context) {
List<Opportunity> opps = [SELECT Id, StageName FROM Opportunity WHERE StageName = 'Closed Lost'];
for(Opportunity opp : opps) {
opp.StageName = 'Archived';
}
update opps;
// Chain job to log operation
System.enqueueJob(new LogArchivalJob(opps.size()));
}
}
Use Case 3: Splitting Large Tasks Across Jobs
You can use queueables to process very large lists by splitting them into manageable parts.
public class SplitTaskProcessor implements Queueable {
List<Task> tasks;
public SplitTaskProcessor(List<Task> tasks) {
this.tasks = tasks;
}
public void execute(QueueableContext context) {
Integer batchSize = 100;
for(Integer i = 0; i < tasks.size(); i += batchSize) {
List<Task> batch = tasks.subList(i, Math.min(i + batchSize, tasks.size()));
System.enqueueJob(new TaskUpdater(batch));
}
}
}
Best Practices
- Limit chaining depth to 50 jobs per transaction
- Avoid using synchronous-only methods like
getContent()inside queueables - Use
try-catchto log errors or retry failed jobs - Return and track Job IDs for monitoring via the Apex Jobs UI
- Break large logic into smaller jobs using chaining or queuing
Common Scenarios
- Mass email logging or notification dispatching
- Background processing after a DML operation or user interaction
- Chained logic like updating, then summarizing, then notifying
- Callouts to external services (via chained jobs)
- Cleaning up or modifying related records after a primary update
Queueable Apex offers a perfect balance of power and ease of use. It’s ideal for tasks too complex for Future methods but not large enough for Batch Apex. With chaining, complex workflows become modular and manageable.
Scheduled Apex
Scheduled Apex allows you to execute Apex code at specific times—either once or on a recurring schedule. Think of it like setting an alarm for your code. It’s perfect for automating routine tasks such as nightly data cleanups, regular email reminders, syncing with external systems, or triggering batch jobs at off-peak hours.
To use Scheduled Apex, implement the Schedulable interface and define your logic in the execute() method. You then schedule it using a CRON expression with System.schedule().
Scheduling Syntax
global class ScheduledAccountUpdate implements Schedulable {
global void execute(SchedulableContext sc) {
// Business logic to run on schedule
AccountUpdateBatch batch = new AccountUpdateBatch();
Database.executeBatch(batch);
}
}
Scheduling a Job
Here's how to schedule the job to run daily at 2 AM:
// Schedule to run every day at 2 AM
String cronExp = '0 0 2 * * ?';
System.schedule('Daily Account Update', cronExp, new ScheduledAccountUpdate());
CRON Expression Format
- Seconds (0–59)
- Minutes (0–59)
- Hours (0–23)
- Day of Month (1–31)
- Month (1–12 or JAN–DEC)
- Day of Week (1–7 or SUN–SAT)
- Year (optional)
Example 1: Clean Up Old Tasks Weekly
global class WeeklyTaskCleaner implements Schedulable {
global void execute(SchedulableContext context) {
List<Task> oldTasks = [SELECT Id FROM Task WHERE CreatedDate < LAST_N_DAYS:30];
delete oldTasks;
}
}
// Schedule every Sunday at 1 AM
String cron = '0 0 1 ? * SUN';
System.schedule('Weekly Task Cleanup', cron, new WeeklyTaskCleaner());
Example 2: Monthly Data Archival
global class MonthlyDataArchiver implements Schedulable {
global void execute(SchedulableContext context) {
List<Opportunity> closed = [SELECT Id FROM Opportunity WHERE StageName = 'Closed Won' AND ClosedDate < LAST_N_DAYS:90];
// Archive logic here
}
}
// Schedule on the 1st day of every month at 3 AM
String cron = '0 0 3 1 * ?';
System.schedule('Monthly Data Archival', cron, new MonthlyDataArchiver());
Example 3: Daily Email Reminder to Sales Team
global class DailyEmailReminder implements Schedulable {
global void execute(SchedulableContext context) {
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(new String[]{'sales@example.com'});
mail.setSubject('Daily Follow-up Reminder');
mail.setPlainTextBody('Don’t forget to follow up with your leads today!');
Messaging.sendEmail(new Messaging.SingleEmailMessage[]{mail});
}
}
// Schedule every day at 8 AM
System.schedule('Daily Sales Email', '0 0 8 * * ?', new DailyEmailReminder());
Best Practices
- Use Scheduled Apex for time-based automation or recurring logic
- Scheduled jobs count toward the limit of 100 active scheduled jobs per org
- Use
System.abortJob(jobId)to stop a job if needed - Combine with Batch Apex for processing large datasets
- Schedule jobs during off-peak hours to minimize performance impact
- Always test in a sandbox with istic data before scheduling in production
Common Use Cases
- Weekly or monthly data cleanup tasks
- Nightly or morning job scheduling for reporting and analytics
- Automated follow-up or reminder emails
- Regular synchronization with external services
- Triggering chained jobs like Queueables or Batches after certain intervals
Scheduled Apex acts as your automated assistant, reliably executing background jobs. When combined with Batch or Queueable Apex, it becomes a powerhouse for handling large volumes and multi-step processing—keeping your Salesforce org clean, efficient, and smartly automated.
Test Classes
Apex test classes are essential for validating your business logic and ensuring code stability across deployments. Salesforce enforces that at least 75% of your Apex code must be covered by tests before you can deploy it to production.
What Is a Test Class?
A test class is a special Apex class annotated with @isTest. It simulates scenarios in a controlled environment. Tests are isolated and do not affect your org’s actual data. They ensure your code behaves as expected and is resilient against edge cases and changes.
Test Class Structure
@isTest
private class AccountProcessorTest {
@testSetup
static void setup() {
Account testAcc = new Account(Name = 'Test Account');
insert testAcc;
}
@isTest
static void testContactCounting() {
Account testAcc = [SELECT Id FROM Account LIMIT 1];
List<Contact> testContacts = new List<Contact>();
for (Integer i = 0; i < 5; i++) {
testContacts.add(new Contact(
FirstName = 'Test',
LastName = 'User ' + i,
AccountId = testAcc.Id
));
}
insert testContacts;
Test.startTest();
AccountProcessor.countContacts(new List<Id>{testAcc.Id});
Test.stopTest();
Account updatedAcc = [SELECT Number_of_Contacts__c FROM Account WHERE Id = :testAcc.Id];
System.assertEquals(5, updatedAcc.Number_of_Contacts__c, 'Contact count should be 5');
}
}
Best Practices for Writing Test Classes
-
1. Use
@testSetupfor Common Data:
Pre-load reusable data once for all test methods.@testSetup static void setupTestData() { insert new Account(Name = 'Shared Test Account'); } -
2. Wrap Logic in
Test.startTest()andTest.stopTest():
Ensures async behavior and limits are measured properly.Test.startTest(); System.enqueueJob(new MyQueueableJob()); Test.stopTest(); -
3. Use a Test Data Factory:
Abstract test data creation to improve readability.@isTest public class TestDataFactory { public static Account createAccount(String name, Boolean doInsert) { Account acc = new Account(Name = name); if(doInsert) insert acc; return acc; } } -
4. Use
System.runAs()for Role/Profile Testing:
Simulate different user profiles or permission sets.User testUser = new User( Alias = 'tuser', Email='testuser@example.com', Username='testuser@example.com', ProfileId = [SELECT Id FROM Profile WHERE Name='Standard User' LIMIT 1].Id, TimeZoneSidKey='Asia/Kolkata', LocaleSidKey='en_US', EmailEncodingKey='UTF-8', LanguageLocaleKey='en_US' ); insert testUser; System.runAs(testUser) { MyLogic.runSomething(); } -
5. Always Use
System.assert()Statements:
Verify expected results and edge cases.System.assertEquals('Expected', actualValue, 'Mismatch in expected result'); -
6. Test Bulk and Governor Limit Scenarios:
Validate performance and behavior at scale.List<Account> accounts = new List<Account>(); for (Integer i = 0; i < 200; i++) { accounts.add(new Account(Name = 'Bulk ' + i)); } insert accounts; Test.startTest(); MyTriggerHandler.handleBulkInsert(accounts); Test.stopTest(); - 7. Isolate Test Data from Org Data:
Don’t depend on existing org records; generate fresh test data every time. -
8. Test Error and Edge Cases:
Test both success and failure paths.try { MyLogic.divide(10, 0); System.assert(false, 'Exception expected'); } catch (Exception e) { System.assertEquals('Cannot divide by zero', e.getMessage()); } - 9. Chain Test Scenarios Together:
Use multiple test methods to simulate workflows (e.g., insert → update → delete). -
10. Include Tests for Triggers and Queueables:
Make sure automation paths are fully covered.@isTest static void testTriggerExecution() { List<Lead> leads = new List<Lead>(); for (Integer i = 0; i < 5; i++) { leads.add(new Lead(FirstName='Test', LastName='Lead ' + i, Company='TestCo')); } insert leads; System.assertEquals(5, [SELECT COUNT() FROM Lead]); }
Final Thoughts
Writing quality test classes is more than just hitting 75% coverage — it’s about confidence. Confidence that your code works under all conditions, for all users, and at scale. Make your tests modular, descriptive, and comprehensive.
Debug Logs
Debug logs are your behind-the-scenes lens into Apex execution. They help you understand what your code is doing, step-by-step — from variable values and logic branches to errors and governor limits. Whether you're chasing down a bug or optimizing performance, debug logs are your best friend.
Working with Debug Logs
-
How to Generate Logs:
- Add
System.debug()statements in your Apex classes, triggers, or test methods - Execute the logic through UI, API calls, Developer Console, or anonymous window
- Test classes also auto-generate logs when executed
- Add
-
Where to View Logs:
- Setup > Debug Logs: Add your user if not already listed. Salesforce keeps the latest 20 logs per user.
- Developer Console > Debug > View Log: Run the code and open logs in time.
- VS Code (Salesforce Extensions): Use "SFDX: Get Apex Debug Logs" to pull logs via CLI.
-
Log Levels:
ERROR– Critical issues and unhandled exceptionsWARN– Potential issuesINFO– General application flow (good for light monitoring)DEBUG– Developer-focused logs (used most often)FINE,FINER,FINEST– Ultra-detailed logs (can be very noisy)
Debugging Example
Here's a example to show how debug logs capture execution details in a list-processing scenario:
public class DebugExample {
public static void processAccounts(List<Account> accounts) {
System.debug('Accounts to process: ' + accounts.size());
for (Account acc : accounts) {
System.debug('Processing Account: ' + acc.Name);
try {
acc.Description = 'Processed at ' + System.now();
} catch (Exception e) {
System.debug(LoggingLevel.ERROR, 'Error: ' + e.getMessage());
}
}
System.debug('Finished processing all accounts');
}
}
Best Practices for Using Debug Logs
- Don’t overuse
System.debug()in production: It can clutter logs and slightly affect performance. - Use meaningful debug messages: Instead of "inside loop", say "Looping through Account: " + acc.Name.
- Use
LoggingLevelfor structured outputs: It helps in filtering logs based on importance. - Leverage
Test.startTest()/stopTest()to isolate logs: Especially helpful when testing async jobs. - Download logs for analysis: Sometimes the Developer Console truncates large logs. Download for full insight.
Final Thoughts
Debug logs are more than just a developer tool — they’re a diagnostic microscope. Once you master reading them, even the trickiest bugs and bottlenecks become visible. Use them wisely to write cleaner, faster, and more reliable Apex.
Testing Best Practices
Writing tests isn’t just about hitting 75% code coverage — it’s about ensuring your code behaves correctly in every situation. A strong test suite builds confidence, prevents future bugs, and simplifies refactoring.
Key Testing Practices
-
✅ Test All Scenarios:
- Cover both positive (happy path) and negative (error or edge cases)
- Include boundary conditions (e.g., empty lists, null values)
- Simulate bulk operations (e.g., inserting 200+ records)
-
✅ Use a Test Data Factory:
- Create reusable utility classes to generate test data
- Reduces redundancy across multiple test classes
- Improves maintainability and test clarity
-
✅ Isolate Test Logic:
- Always use
SeeAllData=false(default) to avoid relying on org data - Make tests independent and repeatable
- Always use
-
✅ Use Assertions to Validate Outcomes:
- Use
System.assert(),System.assertEquals(), andSystem.assertNotEquals() - Assert actual vs expected results to ensure correctness
- Use
-
✅ Governor Limit Awareness:
- Test with istic and bulk data volumes
- Use
Limits.getDMLStatements(),Limits.getQueries()for limit testing - Ensure code doesn’t break under load
-
✅ Use
System.runAs():- Simulate running code as a specific user/profile
- Useful for testing security, FLS, sharing rules
-
✅ Test Exception Scenarios:
- Deliberately cause errors and ensure your code handles them
- Validate proper fallback or error messaging
Test Data Factory Example
@isTest
public class TestDataFactory {
public static Account createAccount(String name, Boolean doInsert) {
Account acc = new Account(
Name = name,
Industry = 'Software',
AnnualRevenue = 2000000
);
if (doInsert) insert acc;
return acc;
}
public static List<Contact> createContactsForAccount(Id accountId, Integer count, Boolean doInsert) {
List<Contact> contacts = new List<Contact>();
for (Integer i = 0; i < count; i++) {
contacts.add(new Contact(
FirstName = 'Test',
LastName = 'Contact ' + i,
Email = 'test' + i + '@mail.com',
AccountId = accountId
));
}
if (doInsert) insert contacts;
return contacts;
}
}
Final Thoughts
High-quality tests save time and headaches later. Use factories, simulate different users, test both good and bad inputs, and make sure your code gracefully handles edge cases. Testing is your safety net — make it strong.