This comprehensive guide covers essential Apex development interview questions ranging from basic concepts to advanced scenarios.
Recursion in Triggers: Recursion occurs when a trigger fires, performs DML operations that cause the same trigger to fire again, creating an infinite loop. This can lead to exceeding governor limits and runtime errors.
Common Causes:
Prevention Methods:
public class TriggerHandler {
public static Boolean isFirstRun = true;
}
trigger AccountTrigger on Account (after update) {
if (TriggerHandler.isFirstRun) {
TriggerHandler.isFirstRun = false;
// Your trigger logic here
}
}
public class TriggerHandler {
public static Set<Id> processedIds = new Set<Id>();
}
trigger AccountTrigger on Account (after update) {
for (Account acc : Trigger.new) {
if (!TriggerHandler.processedIds.contains(acc.Id)) {
TriggerHandler.processedIds.add(acc.Id);
// Your trigger logic here
}
}
}
public class TriggerHandler {
public static Boolean skipAccountTrigger = false;
public static Boolean skipContactTrigger = false;
}
Custom Exception: A custom exception is a user-defined exception class that extends the built-in Exception class. It allows developers to create specific exception types for their application’s unique error scenarios.
Creating Custom Exceptions:
public class MyCustomException extends Exception {
// Custom exception class
}
// Another example with more detail
public class InsufficientFundsException extends Exception {
private Decimal balance;
public InsufficientFundsException(Decimal currentBalance) {
this.balance = currentBalance;
}
public Decimal getBalance() {
return balance;
}
}
Using Custom Exceptions:
public class BankAccount {
public Decimal balance;
public void withdraw(Decimal amount) {
if (amount > balance) {
throw new InsufficientFundsException(balance);
}
balance -= amount;
}
}
// Catching custom exception
try {
BankAccount acc = new BankAccount();
acc.balance = 100;
acc.withdraw(150);
} catch (InsufficientFundsException e) {
System.debug('Insufficient funds. Current balance: ' + e.getBalance());
}
Benefits:
Test Classes: Test classes are special Apex classes annotated with @isTest that contain test methods to verify the functionality of your code. They simulate user interactions and validate that the code works as expected.
Importance:
Example Test Class:
@isTest
private class AccountTriggerTest {
@isTest
static void testAccountCreation() {
// Test data setup
Account testAccount = new Account(
Name = 'Test Account',
Industry = 'Technology'
);
// Test execution
Test.startTest();
insert testAccount;
Test.stopTest();
// Assertions
Account insertedAccount = [SELECT Id, Name, Industry FROM Account WHERE Id = :testAccount.Id];
System.assertEquals('Test Account', insertedAccount.Name);
System.assertEquals('Technology', insertedAccount.Industry);
}
}
Best Practices:
Test.startTest() and Test.stopTest() for governor limit reset@TestSetup@isTest(SeeAllData=true) unless absolutely necessary@TestSetup Annotation: The @TestSetup annotation is used to create test data once that can be reused by all test methods in a test class. This improves test performance by reducing the number of DML operations.
Key Features:
@TestSetup method allowed per test classExample:
@isTest
private class AccountServiceTest {
@TestSetup
static void setupTestData() {
// Create test data once for all test methods
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 10; i++) {
accounts.add(new Account(
Name = 'Test Account ' + i,
Industry = 'Technology'
));
}
insert accounts;
// Create related contacts
List<Contact> contacts = new List<Contact>();
for (Account acc : accounts) {
contacts.add(new Contact(
LastName = 'Test Contact',
AccountId = acc.Id
));
}
insert contacts;
}
@isTest
static void testMethod1() {
// Query the test data created in @TestSetup
List<Account> accounts = [SELECT Id, Name FROM Account];
System.assertEquals(10, accounts.size());
// Test logic here
}
@isTest
static void testMethod2() {
// Same test data is available here
List<Contact> contacts = [SELECT Id, LastName FROM Contact];
System.assertEquals(10, contacts.size());
// Different test logic here
}
}
Benefits:
Important Notes:
@TestSetup is committed and visible to all test methods@TestSetup method itself is not a test methodScheduled Apex: A scheduled Apex class allows you to run Apex code at specified times using the Salesforce scheduler. It implements the Schedulable interface and is useful for performing regular batch operations.
Implementation:
global class ScheduledAccountUpdate implements Schedulable {
global void execute(SchedulableContext sc) {
// Code to be executed on schedule
List<Account> accounts = [SELECT Id, Name, Status__c
FROM Account
WHERE Status__c = 'Pending'
LIMIT 200];
for (Account acc : accounts) {
acc.Status__c = 'Active';
}
update accounts;
}
}
Scheduling the Class:
// Schedule to run daily at 2 AM String cronExpression = '0 0 2 * * ?'; String jobName = 'Daily Account Update'; ScheduledAccountUpdate scheduleJob = new ScheduledAccountUpdate(); System.schedule(jobName, cronExpression, scheduleJob);
CRON Expression Format:
Seconds Minutes Hours Day_of_month Month Day_of_week Optional_year 0 0 2 * * ? * Examples: '0 0 2 * * ?' - Daily at 2 AM '0 0 12 ? * MON-FRI' - Weekdays at noon '0 0 0 1 * ?' - First day of every month at midnight '0 0 8,20 * * ?' - Twice daily at 8 AM and 8 PM
Limitations:
Best Practices:
These are access modifiers that control the visibility and accessibility of classes, methods, and variables in Apex.
public class MyClass {
private String secretData = 'Hidden';
private void privateMethod() {
// Only accessible within MyClass
}
}
public class MyClass {
public String publicData = 'Visible in namespace';
public void publicMethod() {
// Accessible within the same namespace
}
}
global class GlobalWebService {
global String globalData = 'Accessible everywhere';
webservice static String getInfo() {
// Web service method must be global
return 'Information';
}
}
public class ParentClass {
protected String protectedData = 'Visible to children';
protected void protectedMethod() {
// Accessible in this class and subclasses
}
}
public class ChildClass extends ParentClass {
public void accessProtected() {
// Can access protectedData and protectedMethod
System.debug(protectedData);
protectedMethod();
}
}
Comparison Table:
| Modifier | Same Class | Same Namespace | Different Namespace | Subclass |
|---|---|---|---|---|
| Private | ✓ | ✗ | ✗ | ✗ |
| Protected | ✓ | ✗ | ✗ | ✓ |
| Public | ✓ | ✓ | ✗ | ✓ |
| Global | ✓ | ✓ | ✓ | ✓ |
Example Combining All:
global class AccessModifierExample {
// Different access levels
private String privateVar = 'Private';
protected String protectedVar = 'Protected';
public String publicVar = 'Public';
global String globalVar = 'Global';
// Methods with different access
private void privateMethod() {
System.debug('Private method');
}
protected void protectedMethod() {
System.debug('Protected method');
}
public void publicMethod() {
System.debug('Public method');
}
global void globalMethod() {
System.debug('Global method');
}
}
Best Practices:
Dynamic Apex: Dynamic Apex enables developers to write code that can adapt to changes in an organization’s schema at runtime. It allows you to access sObject and field metadata dynamically, without hardcoding API names.
Key Components:
Provides methods to describe sObjects and fields.
Execute queries built as strings at runtime.
Perform DML operations on dynamically determined objects.
Use Cases and Examples:
// Get field value dynamically
Account acc = new Account(Name = 'Test');
String fieldName = 'Name';
String fieldValue = (String)acc.get(fieldName);
System.debug('Field Value: ' + fieldValue);
// Set field value dynamically
acc.put('Industry', 'Technology');
// Build query dynamically
String objectName = 'Account';
String fieldName = 'Name';
String whereCondition = 'Industry = \'Technology\'';
String query = 'SELECT Id, ' + fieldName +
' FROM ' + objectName +
' WHERE ' + whereCondition;
List<sObject> records = Database.query(query);
for (sObject record : records) {
System.debug('Record: ' + record.get(fieldName));
}
// Create sObject dynamically
String objectName = 'Account';
Schema.SObjectType objectType = Schema.getGlobalDescribe().get(objectName);
sObject newRecord = objectType.newSObject();
newRecord.put('Name', 'Dynamic Account');
newRecord.put('Industry', 'Technology');
insert newRecord;
// Get all objects in the org
Map<String, Schema.SObjectType> globalDescribe = Schema.getGlobalDescribe();
for (String objectName : globalDescribe.keySet()) {
System.debug('Object: ' + objectName);
}
// Describe a specific object
Schema.SObjectType accountType = Schema.getGlobalDescribe().get('Account');
Schema.DescribeSObjectResult accountDescribe = accountType.getDescribe();
System.debug('Label: ' + accountDescribe.getLabel());
System.debug('Plural Label: ' + accountDescribe.getLabelPlural());
System.debug('Is Custom: ' + accountDescribe.isCustom());
// Get all fields of an object
Map<String, Schema.SObjectField> fieldMap = accountDescribe.fields.getMap();
for (String fieldName : fieldMap.keySet()) {
Schema.DescribeFieldResult fieldDescribe = fieldMap.get(fieldName).getDescribe();
System.debug('Field: ' + fieldDescribe.getLabel() +
', Type: ' + fieldDescribe.getType());
}
// Get picklist values dynamically
public List<String> getPicklistValues(String objectName, String fieldName) {
List<String> picklistValues = new List<String>();
Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
Schema.DescribeSObjectResult objDescribe = objType.getDescribe();
Schema.SObjectField field = objDescribe.fields.getMap().get(fieldName);
Schema.DescribeFieldResult fieldDescribe = field.getDescribe();
for (Schema.PicklistEntry entry : fieldDescribe.getPicklistValues()) {
if (entry.isActive()) {
picklistValues.add(entry.getValue());
}
}
return picklistValues;
}
// Get RecordType Id dynamically
public Id getRecordTypeId(String objectName, String recordTypeName) {
Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
Schema.DescribeSObjectResult objDescribe = objType.getDescribe();
Map<String, Schema.RecordTypeInfo> recordTypeMap = objDescribe.getRecordTypeInfosByName();
if (recordTypeMap.containsKey(recordTypeName)) {
return recordTypeMap.get(recordTypeName).getRecordTypeId();
}
return null;
}
public void updateField(sObject record, String fieldName, Object fieldValue) {
record.put(fieldName, fieldValue);
update record;
}
// Usage
Account acc = [SELECT Id FROM Account LIMIT 1];
updateField(acc, 'Industry', 'Healthcare');
Benefits:
Considerations:
Schema Class: The Schema class provides methods to access and describe your organization’s data model (schema) at runtime. It’s a key component of Dynamic Apex that allows you to work with sObjects, fields, and their metadata programmatically.
Main Components:
Returns a map of all sObject types in the organization.
Map<String, Schema.SObjectType> globalDescribe = Schema.getGlobalDescribe();
// Iterate through all objects
for (String objectName : globalDescribe.keySet()) {
System.debug('Object API Name: ' + objectName);
}
// Get a specific object
Schema.SObjectType accountType = globalDescribe.get('Account');
Represents an sObject type.
Schema.SObjectType accountType = Account.SObjectType;
// or
Schema.SObjectType accountType = Schema.getGlobalDescribe().get('Account');
// Get sObject describe result
Schema.DescribeSObjectResult accountDescribe = accountType.getDescribe();
Contains metadata about an sObject.
Schema.DescribeSObjectResult accountDescribe = Account.SObjectType.getDescribe(); // Common properties String label = accountDescribe.getLabel(); // 'Account' String pluralLabel = accountDescribe.getLabelPlural(); // 'Accounts' String apiName = accountDescribe.getName(); // 'Account' Boolean isCustom = accountDescribe.isCustom(); // false Boolean isCreateable = accountDescribe.isCreateable(); Boolean isUpdateable = accountDescribe.isUpdateable(); Boolean isDeletable = accountDescribe.isDeletable(); // Get all fields Map<String, Schema.SObjectField> fieldMap = accountDescribe.fields.getMap();
Represents a field on an sObject.
Schema.SObjectField nameField = Account.Name;
// or
Schema.SObjectField nameField = Account.SObjectType.getDescribe()
.fields.getMap().get('Name');
Contains metadata about a field.
Schema.DescribeFieldResult fieldDescribe = Account.Name.getDescribe(); // Common properties String fieldLabel = fieldDescribe.getLabel(); // 'Account Name' String fieldApiName = fieldDescribe.getName(); // 'Name' Schema.DisplayType fieldType = fieldDescribe.getType(); // STRING Integer fieldLength = fieldDescribe.getLength(); // 255 Boolean isRequired = !fieldDescribe.isNillable(); Boolean isCustomField = fieldDescribe.isCustom(); Boolean isUnique = fieldDescribe.isUnique();
Practical Examples:
public class PermissionChecker {
public static Boolean canCreateAccount() {
return Schema.sObjectType.Account.isCreateable();
}
public static Boolean canUpdateAccount() {
return Schema.sObjectType.Account.isUpdateable();
}
public static Boolean canDeleteAccount() {
return Schema.sObjectType.Account.isDeletable();
}
public static Boolean canAccessAccountName() {
return Schema.sObjectType.Account.fields.Name.isAccessible();
}
public static Boolean canUpdateAccountName() {
return Schema.sObjectType.Account.fields.Name.isUpdateable();
}
}
public List<String> getUpdateableFields(String objectName) {
List<String> updateableFields = new List<String>();
Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
Map<String, Schema.SObjectField> fieldMap = objType.getDescribe().fields.getMap();
for (String fieldName : fieldMap.keySet()) {
Schema.DescribeFieldResult fieldDescribe = fieldMap.get(fieldName).getDescribe();
if (fieldDescribe.isUpdateable()) {
updateableFields.add(fieldName);
}
}
return updateableFields;
}
public Boolean isFieldType(String objectName, String fieldName, Schema.DisplayType expectedType) {
Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
Schema.SObjectField field = objType.getDescribe().fields.getMap().get(fieldName);
Schema.DescribeFieldResult fieldDescribe = field.getDescribe();
return fieldDescribe.getType() == expectedType;
}
// Usage
Boolean isPhoneField = isFieldType('Account', 'Phone', Schema.DisplayType.PHONE);
public Map<String, List<String>> getDependentPicklistValues(
String objectName,
String controllingField,
String dependentField) {
Map<String, List<String>> dependencyMap = new Map<String, List<String>>();
Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
Schema.SObjectField depField = objType.getDescribe().fields.getMap().get(dependentField);
Schema.DescribeFieldResult fieldDescribe = depField.getDescribe();
// Get controlling field values
Schema.SObjectField ctrlField = objType.getDescribe().fields.getMap().get(controllingField);
List<Schema.PicklistEntry> ctrlValues = ctrlField.getDescribe().getPicklistValues();
// Map dependent values to controlling values
for (Schema.PicklistEntry entry : fieldDescribe.getPicklistValues()) {
if (entry.isActive()) {
// Logic to map dependent to controlling values
// This requires additional processing based on validFor property
}
}
return dependencyMap;
}
public String buildSelectAllQuery(String objectName) {
Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
Map<String, Schema.SObjectField> fieldMap = objType.getDescribe().fields.getMap();
List<String> fields = new List<String>();
for (String fieldName : fieldMap.keySet()) {
if (fieldMap.get(fieldName).getDescribe().isAccessible()) {
fields.add(fieldName);
}
}
return 'SELECT ' + String.join(fields, ',') + ' FROM ' + objectName;
}
public List<String> getChildRelationships(String objectName) {
List<String> relationships = new List<String>();
Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
Schema.DescribeSObjectResult objDescribe = objType.getDescribe();
for (Schema.ChildRelationship rel : objDescribe.getChildRelationships()) {
if (rel.getRelationshipName() != null) {
relationships.add(rel.getRelationshipName());
}
}
return relationships;
}
Common Schema Methods Summary:
| Method | Purpose |
|---|---|
Schema.getGlobalDescribe() | Get all sObjects in org |
sObject.SObjectType.getDescribe() | Get sObject metadata |
field.getDescribe() | Get field metadata |
getLabel() | Get user-friendly label |
getName() | Get API name |
isCustom() | Check if custom |
isAccessible() | Check read permission |
isCreateable() | Check create permission |
isUpdateable() | Check update permission |
isDeletable() | Check delete permission |
Benefits:
Triggers are powerful but can lead to performance issues and maintenance challenges if not written properly. Here are the best practices:
trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
AccountTriggerHandler.handleTrigger();
}
// Trigger
trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
new AccountTriggerHandler().run();
}
// Handler
public class AccountTriggerHandler {
public void run() {
if (Trigger.isBefore) {
if (Trigger.isInsert) {
beforeInsert(Trigger.new);
} else if (Trigger.isUpdate) {
beforeUpdate(Trigger.new, Trigger.oldMap);
}
} else if (Trigger.isAfter) {
if (Trigger.isInsert) {
afterInsert(Trigger.new);
} else if (Trigger.isUpdate) {
afterUpdate(Trigger.new, Trigger.oldMap);
}
}
}
private void beforeInsert(List<Account> newAccounts) {
// Before insert logic
}
private void beforeUpdate(List<Account> newAccounts, Map<Id, Account> oldMap) {
// Before update logic
}
private void afterInsert(List<Account> newAccounts) {
// After insert logic
}
private void afterUpdate(List<Account> newAccounts, Map<Id, Account> oldMap) {
// After update logic
}
}
// Bad Practice
trigger AccountTrigger on Account (after insert) {
for (Account acc : Trigger.new) {
Contact con = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
con.Description = 'Updated';
update con;
}
}
// Good Practice
trigger AccountTrigger on Account (after insert) {
Set<Id> accountIds = new Set<Id>();
for (Account acc : Trigger.new) {
accountIds.add(acc.Id);
}
List<Contact> contactsToUpdate = [SELECT Id, AccountId FROM Contact
WHERE AccountId IN :accountIds];
for (Contact con : contactsToUpdate) {
con.Description = 'Updated';
}
update contactsToUpdate;
}
public class TriggerHelper {
public static Boolean isFirstRun = true;
public static Set<Id> processedIds = new Set<Id>();
}
trigger AccountTrigger on Account (after update) {
if (TriggerHelper.isFirstRun) {
TriggerHelper.isFirstRun = false;
// Your trigger logic
}
}
Trigger.new – List of new records (insert, update, undelete)Trigger.old – List of old records (update, delete)Trigger.newMap – Map of new records (before update, after insert, after update, after undelete)Trigger.oldMap – Map of old records (update, delete)trigger AccountTrigger on Account (before update) {
for (Account acc : Trigger.new) {
Account oldAccount = Trigger.oldMap.get(acc.Id);
if (acc.Industry != oldAccount.Industry) {
// Industry changed
acc.Description = 'Industry changed from ' + oldAccount.Industry;
}
}
}
Before Triggers:
After Triggers:
trigger AccountTrigger on Account (before insert, after insert) {
if (Trigger.isBefore && Trigger.isInsert) {
// Set default values
for (Account acc : Trigger.new) {
if (acc.Type == null) {
acc.Type = 'Prospect';
}
}
}
if (Trigger.isAfter && Trigger.isInsert) {
// Create related records
List<Contact> contacts = new List<Contact>();
for (Account acc : Trigger.new) {
contacts.add(new Contact(
LastName = 'Default Contact',
AccountId = acc.Id
));
}
insert contacts;
}
}
trigger AccountTrigger on Account (before insert) {
try {
// Your logic here
AccountTriggerHandler.validateAccounts(Trigger.new);
} catch (Exception e) {
System.debug('Error in AccountTrigger: ' + e.getMessage());
Trigger.new[0].addError('An error occurred: ' + e.getMessage());
}
}
// Efficient approach
Map<Id, Account> accountMap = new Map<Id, Account>(
[SELECT Id, Name FROM Account WHERE Id IN :accountIds]
);
for (Contact con : contactList) {
Account relatedAccount = accountMap.get(con.AccountId);
// Process
}
@isTest
private class AccountTriggerTest {
@isTest
static void testBulkInsert() {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 200; i++) {
accounts.add(new Account(Name = 'Test Account ' + i));
}
Test.startTest();
insert accounts;
Test.stopTest();
// Assertions
List<Account> insertedAccounts = [SELECT Id FROM Account];
System.assertEquals(200, insertedAccounts.size());
}
}
trigger AccountTrigger on Account (after insert) {
if (!System.isBatch() && !System.isFuture()) {
System.enqueueJob(new AccountQueueable(Trigger.newMap.keySet()));
}
}
/**
* AccountTrigger
* Purpose: Handles all DML events for Account object
* Created: 2025-01-15
* Modified: 2025-01-20
*/
trigger AccountTrigger on Account (before insert, after insert) {
// Delegate to handler
new AccountTriggerHandler().run();
}
// Using Custom Settings
Trigger_Settings__c settings = Trigger_Settings__c.getInstance();
if (settings.Enable_Account_Trigger__c) {
// Execute trigger logic
}
Understand the Salesforce order of execution:
if (Schema.sObjectType.Account.fields.Industry.isUpdateable()) {
acc.Industry = 'Technology';
}
Summary Checklist:
Following these best practices will result in maintainable, scalable, and performant triggers.
Platform Events: Platform Events are a part of Salesforce’s event-driven architecture that enables you to deliver secure, scalable, and customizable event notifications. They use a publish-subscribe model where publishers send event messages that subscribers receive in near real-time.
Key Characteristics:
Types of Platform Events:
Pre-built events provided by Salesforce (e.g., Real-Time Event Monitoring)
User-defined events for custom use cases
Creating a Platform Event:
Setup → Platform Events → New Platform Event
API Name: Order_Event__e Fields: - Order_Number__c (Text) - Order_Amount__c (Number) - Status__c (Text)
Publishing Platform Events:
public class OrderEventPublisher {
public static void publishOrderEvent(String orderNumber, Decimal amount, String status) {
// Create event instance
Order_Event__e orderEvent = new Order_Event__e(
Order_Number__c = orderNumber,
Order_Amount__c = amount,
Status__c = status
);
// Publish event
Database.SaveResult result = EventBus.publish(orderEvent);
// Check if event was published successfully
if (result.isSuccess()) {
System.debug('Order event published successfully');
} else {
for (Database.Error error : result.getErrors()) {
System.debug('Error publishing event: ' + error.getMessage());
}
}
}
// Bulk publishing
public static void publishMultipleEvents(List<Order_Event__e> events) {
List<Database.SaveResult> results = EventBus.publish(events);
for (Database.SaveResult result : results) {
if (!result.isSuccess()) {
for (Database.Error error : result.getErrors()) {
System.debug('Error: ' + error.getMessage());
}
}
}
}
}
Subscribing to Platform Events:
trigger OrderEventTrigger on Order_Event__e (after insert) {
for (Order_Event__e event : Trigger.new) {
System.debug('Order Number: ' + event.Order_Number__c);
System.debug('Order Amount: ' + event.Order_Amount__c);
System.debug('Status: ' + event.Status__c);
// Process the event
if (event.Status__c == 'Completed') {
// Create notification, update records, etc.
OrderEventHandler.handleCompletedOrder(event);
}
}
}
public class OrderEventHandler {
public static void handleCompletedOrder(Order_Event__e event) {
// Create a task for sales team
Task followUpTask = new Task(
Subject = 'Follow up on Order: ' + event.Order_Number__c,
Description = 'Order amount: ' + event.Order_Amount__c,
Status = 'Not Started',
Priority = 'High'
);
insert followUpTask;
}
}
Subscribing via Flow:
Use Cases:
Advanced Example – Integration with External System:
public class ExternalSystemIntegration {
@future(callout=true)
public static void notifyExternalSystem(String orderNumber, Decimal amount) {
// First publish platform event
Order_Event__e orderEvent = new Order_Event__e(
Order_Number__c = orderNumber,
Order_Amount__c = amount,
Status__c = 'Pending External Processing'
);
EventBus.publish(orderEvent);
// Then make callout
HttpRequest req = new HttpRequest();
req.setEndpoint('https://external-system.com/api/orders');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(new Map<String, Object>{
'orderNumber' => orderNumber,
'amount' => amount
}));
Http http = new Http();
HttpResponse res = http.send(req);
// Publish result event
if (res.getStatusCode() == 200) {
Order_Event__e successEvent = new Order_Event__e(
Order_Number__c = orderNumber,
Status__c = 'External System Notified'
);
EventBus.publish(successEvent);
}
}
}
Benefits:
Limitations:
Both REST and SOAP are protocols for exposing Apex methods as web services, but they have different characteristics and use cases.
Characteristics:
Creating REST Web Service:
@RestResource(urlMapping='/accounts/*')
global class AccountRestService {
@HttpGet
global static Account getAccount() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
String accountId = req.requestURI.substring(req.requestURI.lastIndexOf('/')+1);
Account acc = [SELECT Id, Name, Industry, AnnualRevenue
FROM Account
WHERE Id = :accountId];
return acc;
}
@HttpPost
global static String createAccount(String name, String industry) {
Account acc = new Account(
Name = name,
Industry = industry
);
insert acc;
return acc.Id;
}
@HttpPut
global static String updateAccount(String id, String name, String industry) {
Account acc = [SELECT Id FROM Account WHERE Id = :id];
acc.Name = name;
acc.Industry = industry;
update acc;
return 'Account updated successfully';
}
@HttpDelete
global static String deleteAccount() {
RestRequest req = RestContext.request;
String accountId = req.requestURI.substring(req.requestURI.lastIndexOf('/')+1);
Account acc = [SELECT Id FROM Account WHERE Id = :accountId];
delete acc;
return 'Account deleted successfully';
}
@HttpPatch
global static String patchAccount() {
RestRequest req = RestContext.request;
String accountId = req.requestURI.substring(req.requestURI.lastIndexOf('/')+1);
// Parse JSON from request body
Map<String, Object> params = (Map<String, Object>)JSON.deserializeUntyped(req.requestBody.toString());
Account acc = [SELECT Id FROM Account WHERE Id = :accountId];
if (params.containsKey('name')) {
acc.Name = (String)params.get('name');
}
if (params.containsKey('industry')) {
acc.Industry = (String)params.get('industry');
}
update acc;
return 'Account patched successfully';
}
}
Advanced REST Example with Custom Response:
@RestResource(urlMapping='/api/v1/customers/*')
global class CustomerRestAPI {
global class CustomerResponse {
global String status;
global String message;
global Object data;
global List<String> errors;
}
@HttpGet
global static CustomerResponse getCustomer() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
CustomerResponse response = new CustomerResponse();
try {
String customerId = req.requestURI.substring(req.requestURI.lastIndexOf('/')+1);
Account customer = [SELECT Id, Name, Industry, AnnualRevenue,
(SELECT Id, FirstName, LastName, Email FROM Contacts)
FROM Account
WHERE Id = :customerId];
response.status = 'success';
response.message = 'Customer retrieved successfully';
response.data = customer;
res.statusCode = 200;
} catch (Exception e) {
response.status = 'error';
response.message = 'Failed to retrieve customer';
response.errors = new List<String>{e.getMessage()};
res.statusCode = 500;
}
return response;
}
@HttpPost
global static CustomerResponse createCustomer(String name, String industry, Decimal revenue) {
CustomerResponse response = new CustomerResponse();
try {
Account newCustomer = new Account(
Name = name,
Industry = industry,
AnnualRevenue = revenue
);
insert newCustomer;
response.status = 'success';
response.message = 'Customer created successfully';
response.data = newCustomer;
RestContext.response.statusCode = 201;
} catch (Exception e) {
response.status = 'error';
response.message = 'Failed to create customer';
response.errors = new List<String>{e.getMessage()};
RestContext.response.statusCode = 400;
}
return response;
}
}
Calling REST Endpoint:
Endpoint: https://yourinstance.salesforce.com/services/apexrest/accounts/001XXXXXXXXXXXXXXX
Method: GET
Headers:
Authorization: Bearer {access_token}
Content-Type: application/json
Characteristics:
Creating SOAP Web Service:
global class AccountSoapService {
webservice static Account getAccount(String accountId) {
Account acc = [SELECT Id, Name, Industry, AnnualRevenue
FROM Account
WHERE Id = :accountId];
return acc;
}
webservice static String createAccount(String name, String industry) {
Account acc = new Account(
Name = name,
Industry = industry
);
insert acc;
return acc.Id;
}
webservice static Boolean updateAccount(String accountId, String name, String industry) {
try {
Account acc = [SELECT Id FROM Account WHERE Id = :accountId];
acc.Name = name;
acc.Industry = industry;
update acc;
return true;
} catch (Exception e) {
return false;
}
}
webservice static Boolean deleteAccount(String accountId) {
try {
Account acc = [SELECT Id FROM Account WHERE Id = :accountId];
delete acc;
return true;
} catch (Exception e) {
return false;
}
}
}
Advanced SOAP Example with Custom Types:
global class OrderSoapService {
// Custom class for request
global class OrderRequest {
webservice String customerName;
webservice String productName;
webservice Decimal quantity;
webservice Decimal unitPrice;
}
// Custom class for response
global class OrderResponse {
webservice String orderId;
webservice String status;
webservice Decimal totalAmount;
webservice String message;
}
webservice static OrderResponse createOrder(OrderRequest request) {
OrderResponse response = new OrderResponse();
try {
// Create Account
Account acc = new Account(Name = request.customerName);
insert acc;
// Create Order
Order ord = new Order(
AccountId = acc.Id,
Status = 'Draft',
EffectiveDate = Date.today()
);
insert ord;
// Calculate total
Decimal total = request.quantity * request.unitPrice;
response.orderId = ord.Id;
response.status = 'Success';
response.totalAmount = total;
response.message = 'Order created successfully';
} catch (Exception e) {
response.status = 'Error';
response.message = e.getMessage();
}
return response;
}
webservice static List<Account> getAccountsByIndustry(String industry) {
return [SELECT Id, Name, Industry, AnnualRevenue
FROM Account
WHERE Industry = :industry
LIMIT 100];
}
}
Accessing WSDL:
Setup → Apex Classes → Select Class → Generate WSDL
| Feature | REST | SOAP |
|---|---|---|
| Protocol | Architectural style | Protocol |
| Format | JSON, XML | XML only |
| Performance | Faster, lightweight | Slower, more overhead |
| Security | HTTPS, OAuth | WS-Security, SSL |
| State | Stateless | Can be stateful |
| Caching | Can leverage HTTP caching | No caching |
| Learning Curve | Easier | Steeper |
| Error Handling | HTTP status codes | SOAP faults |
| Bandwidth | Less | More |
| Use Case | Modern web/mobile apps | Enterprise integrations |
When to Use REST:
When to Use SOAP:
Example: Making a REST Callout from Apex:
public class RestCalloutExample {
@future(callout=true)
public static void callExternalService() {
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/data');
req.setMethod('GET');
req.setHeader('Content-Type', 'application/json');
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
Map<String, Object> results = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
System.debug('Response: ' + results);
}
}
}
Integration Patterns: Integration patterns are proven solutions for common integration scenarios between Salesforce and external systems. They define how systems communicate and exchange data.
Common Integration Patterns:
Scenario: Salesforce invokes an external service and waits for a response synchronously.
Implementation:
public class RemoteProcessInvocation {
public static String validateAddress(String street, String city, String zipCode) {
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.addressvalidation.com/validate');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
Map<String, String> addressData = new Map<String, String>{
'street' => street,
'city' => city,
'zipCode' => zipCode
};
req.setBody(JSON.serialize(addressData));
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
Map<String, Object> result = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
return (String)result.get('validatedAddress');
}
return null;
}
}
// Usage in trigger
trigger AccountTrigger on Account (before insert) {
for (Account acc : Trigger.new) {
if (acc.BillingStreet != null) {
String validated = RemoteProcessInvocation.validateAddress(
acc.BillingStreet,
acc.BillingCity,
acc.BillingPostalCode
);
acc.BillingStreet = validated;
}
}
}
Characteristics:
Scenario: Salesforce invokes an external service but doesn’t wait for a response.
Implementation:
public class FireAndForgetIntegration {
@future(callout=true)
public static void notifyExternalSystem(String accountId, String accountName) {
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.external.com/notifications');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setTimeout(10000); // 10 seconds timeout
Map<String, String> data = new Map<String, String>{
'accountId' => accountId,
'accountName' => accountName,
'timestamp' => String.valueOf(DateTime.now())
};
req.setBody(JSON.serialize(data));
try {
Http http = new Http();
HttpResponse res = http.send(req);
// Don't process response, just log
System.debug('Notification sent, Status: ' + res.getStatusCode());
} catch (Exception e) {
System.debug('Error sending notification: ' + e.getMessage());
// Optionally log to custom object
}
}
}
// Usage
trigger AccountTrigger on Account (after insert) {
for (Account acc : Trigger.new) {
FireAndForgetIntegration.notifyExternalSystem(acc.Id, acc.Name);
}
}
Characteristics:
Scenario: Synchronize large volumes of data between Salesforce and external systems on a scheduled basis.
Implementation:
global class BatchDataSync implements Database.Batchable<sObject>, Database.AllowsCallouts {
global Database.QueryLocator start(Database.BatchableContext bc) {
// Get records modified in last 24 hours
DateTime yesterday = DateTime.now().addDays(-1);
return Database.getQueryLocator(
'SELECT Id, Name, Industry, LastModifiedDate FROM Account WHERE LastModifiedDate > :yesterday'
);
}
global void execute(Database.BatchableContext bc, List<Account> scope) {
// Prepare data for external system
List<Map<String, Object>> accountData = new List<Map<String, Object>>();
for (Account acc : scope) {
accountData.add(new Map<String, Object>{
'id' => acc.Id,
'name' => acc.Name,
'industry' => acc.Industry
});
}
// Make callout
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.external.com/sync');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(accountData));
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
System.debug('Batch sync successful');
} else {
System.debug('Batch sync failed: ' + res.getBody());
}
}
global void finish(Database.BatchableContext bc) {
System.debug('Batch job completed');
}
}
// Schedule the batch
global class ScheduleBatchDataSync implements Schedulable {
global void execute(SchedulableContext sc) {
Database.executeBatch(new BatchDataSync(), 200);
}
}
Characteristics:
Scenario: External system calls Salesforce to create, update, or query data.
Implementation:
@RestResource(urlMapping='/api/orders/*')
global class InboundOrderAPI {
global class OrderRequest {
global String customerName;
global String productName;
global Decimal quantity;
global Decimal price;
}
global class OrderResponse {
global Boolean success;
global String orderId;
global String message;
}
@HttpPost
global static OrderResponse createOrder(OrderRequest request) {
OrderResponse response = new OrderResponse();
try {
// Find or create account
List<Account> accounts = [SELECT Id FROM Account WHERE Name = :request.customerName LIMIT 1];
Account acc;
if (accounts.isEmpty()) {
acc = new Account(Name = request.customerName);
insert acc;
} else {
acc = accounts[0];
}
// Create order
Order__c order = new Order__c(
Account__c = acc.Id,
Product_Name__c = request.productName,
Quantity__c = request.quantity,
Unit_Price__c = request.price,
Total_Amount__c = request.quantity * request.price
);
insert order;
response.success = true;
response.orderId = order.Id;
response.message = 'Order created successfully';
} catch (Exception e) {
response.success = false;
response.message = e.getMessage();
}
return response;
}
@HttpGet
global static List<Order__c> getOrders() {
String accountName = RestContext.request.params.get('accountName');
if (String.isNotBlank(accountName)) {
return [SELECT Id, Product_Name__c, Quantity__c, Total_Amount__c
FROM Order__c
WHERE Account__r.Name = :accountName];
}
return [SELECT Id, Product_Name__c, Quantity__c, Total_Amount__c
FROM Order__c
LIMIT 100];
}
}
Characteristics:
Scenario: Update Salesforce UI when external data changes (using Platform Events or Streaming API).
Implementation:
// External system publishes platform event
public class ExternalDataChangePublisher {
public static void publishDataChange(String entityType, String entityId, String changeType) {
External_Data_Change__e event = new External_Data_Change__e(
Entity_Type__c = entityType,
Entity_Id__c = entityId,
Change_Type__c = changeType,
Timestamp__c = DateTime.now()
);
EventBus.publish(event);
}
}
// Trigger to handle the event
trigger ExternalDataChangeTrigger on External_Data_Change__e (after insert) {
for (External_Data_Change__e event : Trigger.new) {
if (event.Entity_Type__c == 'Account' && event.Change_Type__c == 'Update') {
// Refresh account data
ExternalDataChangeHandler.refreshAccountData(event.Entity_Id__c);
}
}
}
public class ExternalDataChangeHandler {
@future(callout=true)
public static void refreshAccountData(String accountId) {
// Call external system to get latest data
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.external.com/accounts/' + accountId);
req.setMethod('GET');
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
Map<String, Object> externalData = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
// Update Salesforce record
Account acc = [SELECT Id FROM Account WHERE External_Id__c = :accountId];
acc.Name = (String)externalData.get('name');
acc.Industry = (String)externalData.get('industry');
update acc;
}
}
}
Scenario: Access external data in Salesforce without storing it locally.
Characteristics:
Best Practices for Integration Patterns:
public class IntegrationErrorHandler {
public static void logIntegrationError(String systemName, String operation, String errorMessage) {
Integration_Log__c log = new Integration_Log__c(
System_Name__c = systemName,
Operation__c = operation,
Error_Message__c = errorMessage,
Timestamp__c = DateTime.now()
);
insert log;
}
}
public class RetryableCallout {
public static HttpResponse makeCalloutWithRetry(HttpRequest req, Integer maxRetries) {
Integer attempts = 0;
HttpResponse res;
while (attempts < maxRetries) {
try {
Http http = new Http();
res = http.send(req);
if (res.getStatusCode() == 200) {
return res;
}
attempts++;
} catch (Exception e) {
attempts++;
if (attempts >= maxRetries) {
throw e;
}
}
}
return res;
}
}
public class CircuitBreaker {
private static Integer failureCount = 0;
private static final Integer FAILURE_THRESHOLD = 5;
private static DateTime lastFailureTime;
private static final Integer TIMEOUT_MINUTES = 5;
public static Boolean isOpen() {
if (failureCount >= FAILURE_THRESHOLD) {
if (lastFailureTime.addMinutes(TIMEOUT_MINUTES) > DateTime.now()) {
return true; // Circuit is open
} else {
failureCount = 0; // Reset after timeout
}
}
return false;
}
public static void recordFailure() {
failureCount++;
lastFailureTime = DateTime.now();
}
public static void recordSuccess() {
failureCount = 0;
}
}
Sharing Keywords: These keywords control whether the current user’s sharing rules are enforced when executing Apex code.
Definition: Enforces the sharing rules of the current user. Only records accessible to the user are returned or modified.
public with sharing class AccountController {
public List<Account> getAccounts() {
// Returns only accounts the current user has access to
return [SELECT Id, Name, Industry FROM Account];
}
public void updateAccount(String accountId, String newName) {
Account acc = [SELECT Id, Name FROM Account WHERE Id = :accountId];
// Will throw exception if user doesn't have edit access
acc.Name = newName;
update acc;
}
}
Definition: Executes code without enforcing the current user’s sharing rules. All records are accessible regardless of user permissions.
public without sharing class AccountUtility {
public List<Account> getAllAccounts() {
// Returns ALL accounts in the system
return [SELECT Id, Name, Industry FROM Account];
}
public void updateAccountAsSystem(String accountId, String newName) {
Account acc = [SELECT Id, Name FROM Account WHERE Id = :accountId];
// Updates regardless of user's access
acc.Name = newName;
update acc;
}
}
Definition: Inherits the sharing setting from the calling class. If called from a context with sharing, it uses sharing rules; otherwise, it runs without sharing.
public inherited sharing class FlexibleController {
public List<Account> getAccounts() {
// Inherits sharing context from caller
return [SELECT Id, Name FROM Account];
}
}
// Example usage
public with sharing class CallerWithSharing {
public void doSomething() {
FlexibleController controller = new FlexibleController();
// FlexibleController will run WITH sharing
List<Account> accounts = controller.getAccounts();
}
}
public without sharing class CallerWithoutSharing {
public void doSomething() {
FlexibleController controller = new FlexibleController();
// FlexibleController will run WITHOUT sharing
List<Account> accounts = controller.getAccounts();
}
}
Behavior:
inherited sharing for inner classes, without sharing for top-level classespublic class NoSharingKeyword {
// Behaves as WITHOUT SHARING for top-level class
public List<Account> getAccounts() {
return [SELECT Id, Name FROM Account];
}
}
Example 1: Security-Conscious Design
// Public facing controller - respects user permissions
public with sharing class AccountPublicController {
public List<Account> getUserAccessibleAccounts() {
return [SELECT Id, Name, Industry FROM Account];
}
public void createAccount(String name) {
Account acc = new Account(Name = name);
insert acc; // Will fail if user doesn't have create permission
}
}
// Internal utility - system context needed
public without sharing class AccountSystemUtility {
public static void createSystemAccount(String name) {
Account acc = new Account(
Name = name,
RecordTypeId = getSystemRecordTypeId()
);
insert acc; // Always succeeds regardless of user permissions
}
private static Id getSystemRecordTypeId() {
return Schema.SObjectType.Account.getRecordTypeInfosByName()
.get('System Account').getRecordTypeId();
}
}
Example 2: Mixing Sharing Contexts
public with sharing class OpportunityController {
public List<Opportunity> getUserOpportunities() {
// Respects sharing rules
return [SELECT Id, Name, Amount FROM Opportunity];
}
public void createOpportunityWithSystemDefaults(String name, Decimal amount) {
// Need to access system data without sharing
Map<String, String> systemDefaults = OpportunitySystemDefaults.getDefaults();
Opportunity opp = new Opportunity(
Name = name,
Amount = amount,
StageName = systemDefaults.get('defaultStage'),
CloseDate = Date.today().addDays(30)
);
insert opp; // Respects sharing rules for insert
}
}
public without sharing class OpportunitySystemDefaults {
public static Map<String, String> getDefaults() {
// Access system configuration that user might not have access to
System_Config__c config = [SELECT Default_Stage__c, Default_Probability__c
FROM System_Config__c
LIMIT 1];
return new Map<String, String>{
'defaultStage' => config.Default_Stage__c,
'defaultProbability' => String.valueOf(config.Default_Probability__c)
};
}
}
Example 3: Elevated Privileges for Specific Operations
public with sharing class CaseController {
public List<Case> getMyCases() {
// User sees only their cases
return [SELECT Id, CaseNumber, Subject, Status FROM Case];
}
public void escalateCase(String caseId) {
// Escalation requires system privileges
CaseEscalationUtility.performEscalation(caseId);
}
}
public without sharing class CaseEscalationUtility {
public static void performEscalation(String caseId) {
Case c = [SELECT Id, Status, Priority, OwnerId FROM Case WHERE Id = :caseId];
// System can access queue even if user can't
Group escalationQueue = [SELECT Id FROM Group
WHERE Type = 'Queue'
AND DeveloperName = 'Escalation_Queue'
LIMIT 1];
c.Status = 'Escalated';
c.Priority = 'High';
c.OwnerId = escalationQueue.Id;
update c; // Updates as system
// Send notification to queue members
notifyQueueMembers(escalationQueue.Id, c.Id);
}
private static void notifyQueueMembers(Id queueId, Id caseId) {
List<GroupMember> members = [SELECT UserOrGroupId FROM GroupMember WHERE GroupId = :queueId];
// Notification logic
}
}
Example 4: Report Generation
public with sharing class UserReportController {
public String generateMyReport() {
// User sees their own data
List<Opportunity> myOpps = [SELECT Id, Amount FROM Opportunity];
return ReportGenerator.generateReport(myOpps);
}
}
public without sharing class AdminReportController {
public String generateOrgWideReport() {
// Admin sees all data
List<Opportunity> allOpps = [SELECT Id, Amount, Owner.Name FROM Opportunity];
return ReportGenerator.generateReport(allOpps);
}
}
public inherited sharing class ReportGenerator {
public static String generateReport(List<Opportunity> opportunities) {
// Inherits sharing from caller
// Process and format opportunities
Decimal total = 0;
for (Opportunity opp : opportunities) {
total += opp.Amount != null ? opp.Amount : 0;
}
return 'Total Opportunities: ' + opportunities.size() + ', Total Amount: ' + total;
}
}
| Keyword | Sharing Rules | Use Case | Access Level |
|---|---|---|---|
| with sharing | Enforced | User-facing features | User’s access |
| without sharing | Not enforced | System operations | All records |
| inherited sharing | Inherited from caller | Utility classes | Depends on caller |
| Omitted | Not enforced (top-level) | Legacy code | All records |
public with sharing class SecureController {
public void createAccount(String name) {
// Check CRUD permission
if (!Schema.sObjectType.Account.isCreateable()) {
throw new SecurityException('User does not have permission to create Accounts');
}
// Check FLS for Name field
if (!Schema.sObjectType.Account.fields.Name.isCreateable()) {
throw new SecurityException('User does not have permission to set Account Name');
}
Account acc = new Account(Name = name);
insert acc;
}
}
Both Custom Metadata Types and Custom Settings store application configuration data, but they have different characteristics and use cases.
Definition: Custom Metadata Types are customizable, deployable, packageable application metadata that you can use to build customizable functionality.
Creating Custom Metadata Type:
Setup → Custom Metadata Types → New Custom Metadata Type
Label: API Configuration Plural Label: API Configurations Object Name: API_Configuration__mdt Fields: - Endpoint__c (Text) - Timeout__c (Number) - API_Key__c (Text Encrypted) - Is_Active__c (Checkbox)
Accessing Custom Metadata in Apex:
public class APIIntegration {
// Query Custom Metadata Type
public static String getEndpoint(String configName) {
API_Configuration__mdt config = [SELECT Endpoint__c, Timeout__c, API_Key__c
FROM API_Configuration__mdt
WHERE DeveloperName = :configName
LIMIT 1];
return config.Endpoint__c;
}
// Get all configurations
public static List<API_Configuration__mdt> getAllConfigs() {
return [SELECT DeveloperName, Endpoint__c, Timeout__c, Is_Active__c
FROM API_Configuration__mdt
WHERE Is_Active__c = true];
}
// Use in integration
public static void makeAPICall(String configName) {
API_Configuration__mdt config = API_Configuration__mdt.getInstance(configName);
if (config != null && config.Is_Active__c) {
HttpRequest req = new HttpRequest();
req.setEndpoint(config.Endpoint__c);
req.setMethod('GET');
req.setTimeout(Integer.valueOf(config.Timeout__c));
req.setHeader('Authorization', 'Bearer ' + config.API_Key__c);
Http http = new Http();
HttpResponse res = http.send(req);
System.debug('Response: ' + res.getBody());
}
}
}
Advanced Example – Business Rules Engine:
// Custom Metadata Type: Validation_Rule__mdt
// Fields: Object_Name__c, Field_Name__c, Rule_Type__c, Error_Message__c, Is_Active__c
public class ValidationEngine {
private static Map<String, List<Validation_Rule__mdt>> rulesCache;
// Load all validation rules
static {
rulesCache = new Map<String, List<Validation_Rule__mdt>>();
for (Validation_Rule__mdt rule : [SELECT Object_Name__c, Field_Name__c,
Rule_Type__c, Error_Message__c,
Validation_Logic__c
FROM Validation_Rule__mdt
WHERE Is_Active__c = true]) {
if (!rulesCache.containsKey(rule.Object_Name__c)) {
rulesCache.put(rule.Object_Name__c, new List<Validation_Rule__mdt>());
}
rulesCache.get(rule.Object_Name__c).add(rule);
}
}
public static Boolean validateRecord(sObject record, String objectName) {
if (!rulesCache.containsKey(objectName)) {
return true;
}
for (Validation_Rule__mdt rule : rulesCache.get(objectName)) {
Object fieldValue = record.get(rule.Field_Name__c);
if (rule.Rule_Type__c == 'Required' && fieldValue == null) {
record.addError(rule.Error_Message__c);
return false;
}
if (rule.Rule_Type__c == 'MaxLength') {
String strValue = String.valueOf(fieldValue);
if (strValue != null && strValue.length() > Integer.valueOf(rule.Validation_Logic__c)) {
record.addError(rule.Error_Message__c);
return false;
}
}
}
return true;
}
}
// Usage in trigger
trigger AccountTrigger on Account (before insert, before update) {
for (Account acc : Trigger.new) {
ValidationEngine.validateRecord(acc, 'Account');
}
}
Definition: Custom Settings are similar to custom objects but provide a cached storage mechanism for configuration data.
Types of Custom Settings:
Creating Custom Setting:
Setup → Custom Settings → New
Label: Integration Settings Object Name: Integration_Settings__c Setting Type: Hierarchy Fields: - API_Endpoint__c (Text) - Retry_Count__c (Number) - Enable_Logging__c (Checkbox)
Accessing Custom Settings in Apex:
Hierarchy Custom Settings:
public class IntegrationManager {
// Get settings for current user/profile/org
public static Integration_Settings__c getSettings() {
// Returns setting for current user, or profile, or org default
return Integration_Settings__c.getInstance();
}
// Get settings for specific user
public static Integration_Settings__c getUserSettings(Id userId) {
return Integration_Settings__c.getInstance(userId);
}
// Get settings for specific profile
public static Integration_Settings__c getProfileSettings(Id profileId) {
return Integration_Settings__c.getInstance(profileId);
}
// Get org-wide default settings
public static Integration_Settings__c getOrgDefaults() {
return Integration_Settings__c.getOrgDefaults();
}
// Usage
public static void performIntegration() {
Integration_Settings__c settings = Integration_Settings__c.getInstance();
if (settings.Enable_Logging__c) {
System.debug('Integration started');
}
HttpRequest req = new HttpRequest();
req.setEndpoint(settings.API_Endpoint__c);
req.setMethod('GET');
// Retry logic based on settings
Integer retryCount = Integer.valueOf(settings.Retry_Count__c);
// Implementation...
}
}
List Custom Settings:
// Custom Setting: Country_Code__c (List type)
// Fields: Country_Name__c, Code__c, Currency__c
public class CountryManager {
// Get all list custom settings
public static Map<String, Country_Code__c> getAllCountries() {
return Country_Code__c.getAll();
}
// Get specific setting by name
public static Country_Code__c getCountry(String countryName) {
return Country_Code__c.getInstance(countryName);
}
// Get country code
public static String getCountryCode(String countryName) {
Country_Code__c country = Country_Code__c.getInstance(countryName);
return country != null ? country.Code__c : null;
}
// Usage in validation
public static Boolean isValidCountry(String countryName) {
return Country_Code__c.getInstance(countryName) != null;
}
}
// Usage
trigger AccountTrigger on Account (before insert) {
for (Account acc : Trigger.new) {
if (acc.BillingCountry != null) {
if (!CountryManager.isValidCountry(acc.BillingCountry)) {
acc.BillingCountry.addError('Invalid country name');
}
}
}
}
| Feature | Custom Metadata Types | Custom Settings (Hierarchy) | Custom Settings (List) |
|---|---|---|---|
| Deployable | ✓ (via changesets/metadata API) | ✗ (requires data migration) | ✗ (requires data migration) |
| Packageable | ✓ | ✓ | ✓ |
| Records in Package | ✓ | ✗ | ✗ |
| Accessible via SOQL | ✓ | ✗ (use getInstance) | ✗ (use getInstance/getAll) |
| DML Operations | ✗ (only via Metadata API) | ✓ | ✓ |
| Cached | ✓ | ✓ | ✓ |
| Counts Against Limits | ✗ (metadata) | ✗ (not counted) | ✗ (not counted) |
| Supports Relationships | ✓ | ✗ | ✗ |
| Hierarchy Support | ✗ | ✓ (User/Profile/Org) | ✗ |
| Visibility | Public/Protected | Public only | Public only |
| Data Types Supported | All standard types | All standard types | All standard types |
| Record Trigger Support | ✗ | ✗ | ✗ |
| Subscriber Override | ✓ | Limited | Limited |
Use Custom Metadata Types when:
Use Hierarchy Custom Settings when:
Use List Custom Settings when:
Mixed DML Operation Error: This error occurs when you try to perform DML operations on setup objects (like User, Profile, PermissionSet) and non-setup objects (like Account, Contact) in the same transaction.
Error Message:
"MIXED_DML_OPERATION, DML operation on setup object is not permitted after you have updated a non-setup object (or vice versa)"
Setup Objects Include:
Non-Setup Objects:
public class MixedDMLExample {
public static void createAccountAndUser() {
// This will cause Mixed DML error
// DML on non-setup object
Account acc = new Account(Name = 'Test Account');
insert acc;
UserRole uRole = [SELECT Id FROM UserRole WHERE Name = 'Sales User' LIMIT 1];
// DML on setup object in same transaction - ERROR!
User newUser = new User(
FirstName = 'Test',
LastName = 'User',
Email = '[email protected]',
Username = '[email protected]',
Alias = 'tuser',
TimeZoneSidKey = 'America/Los_Angeles',
LocaleSidKey = 'en_US',
EmailEncodingKey = 'UTF-8',
LanguageLocaleKey = 'en_US',
UserRoleId = uRole.Id,
ProfileId = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1].Id
);
insert newUser; // MIXED_DML_OPERATION error here!
}
}
public class MixedDMLSolution1 {
public static void createAccountAndUser() {
// DML on non-setup object first
Account acc = new Account(Name = 'Test Account');
insert acc;
// DML on setup object in separate transaction
createUserAsync('testuser');
}
@future
public static void createUserAsync(String userName) {
User newUser = new User(
FirstName = 'Test',
LastName = userName,
Email = userName + '@test.com',
Username = userName + '@test.com.test',
Alias = userName.substring(0, 5),
TimeZoneSidKey = 'America/Los_Angeles',
LocaleSidKey = 'en_US',
EmailEncodingKey = 'UTF-8',
LanguageLocaleKey = 'en_US',
ProfileId = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1].Id
);
insert newUser;
}
}
public class MixedDMLSolution2 {
public static void createAccountAndUser() {
// DML on non-setup object
Account acc = new Account(Name = 'Test Account');
insert acc;
// Queue setup object DML
System.enqueueJob(new UserCreationQueueable('testuser'));
}
}
public class UserCreationQueueable implements Queueable {
private String userName;
public UserCreationQueueable(String userName) {
this.userName = userName;
}
public void execute(QueueableContext context) {
// Get the Profile
Profile prof = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1];
// Get the User Role
UserRole uRole = [SELECT Id FROM UserRole WHERE Name = 'Sales User' LIMIT 1];
User newUser = new User(
FirstName = 'Test',
LastName = userName,
Email = userName + '@test.com',
Username = userName + '@test.com.test',
Alias = userName.substring(0, 5),
TimeZoneSidKey = 'America/Los_Angeles',
LocaleSidKey = 'en_US',
EmailEncodingKey = 'UTF-8',
LanguageLocaleKey = 'en_US',
ProfileId = prof.Id,
UserRoleId = uRole.Id
);
insert newUser;
}
}
Database.Stateful Interface: The Database.Stateful interface is used with Batch Apex to maintain state across batch transactions. By default, batch apex doesn’t retain instance variable values between execute method calls, but implementing this interface changes that behavior.
Default Behavior (Without Stateful):
global class BatchWithoutStateful implements Database.Batchable<sObject> {
global Integer recordCount = 0; // This will NOT persist across batches
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id, Name FROM Account');
}
global void execute(Database.BatchableContext bc, List<Account> scope) {
recordCount += scope.size();
System.debug('Current count: ' + recordCount);
// recordCount resets to 0 for each batch!
}
global void finish(Database.BatchableContext bc) {
System.debug('Total records: ' + recordCount); // Will be 0 or last batch size
}
}
With Database.Stateful:
global class BatchWithStateful implements Database.Batchable<sObject>, Database.Stateful {
global Integer recordCount = 0; // This WILL persist across batches
global List<String> errorMessages = new List<String>();
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id, Name, Industry FROM Account');
}
global void execute(Database.BatchableContext bc, List<Account> scope) {
recordCount += scope.size();
List<Account> accountsToUpdate = new List<Account>();
for (Account acc : scope) {
if (String.isBlank(acc.Industry)) {
acc.Industry = 'Unknown';
accountsToUpdate.add(acc);
}
}
try {
update accountsToUpdate;
} catch (Exception e) {
errorMessages.add('Batch error: ' + e.getMessage());
}
System.debug('Processed so far: ' + recordCount);
}
global void finish(Database.BatchableContext bc) {
System.debug('Total records processed: ' + recordCount);
if (!errorMessages.isEmpty()) {
// Send email notification with all errors
sendErrorNotification(errorMessages);
}
}
private void sendErrorNotification(List<String> errors) {
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setToAddresses(new String[]{'[email protected]'});
email.setSubject('Batch Job Errors');
email.setPlainTextBody('Errors:\n' + String.join(errors, '\n'));
Messaging.sendEmail(new Messaging.SingleEmailMessage[]{email});
}
}
Use Case 1: Counting and Aggregating Results
global class SalesReportBatch implements Database.Batchable<sObject>, Database.Stateful {
global Decimal totalRevenue = 0;
global Integer opportunitiesProcessed = 0;
global Integer wonOpportunities = 0;
global Map<String, Decimal> revenueByIndustry = new Map<String, Decimal>();
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, Amount, StageName, Account.Industry FROM Opportunity WHERE CloseDate = THIS_YEAR'
);
}
global void execute(Database.BatchableContext bc, List<Opportunity> scope) {
for (Opportunity opp : scope) {
opportunitiesProcessed++;
if (opp.StageName == 'Closed Won' && opp.Amount != null) {
wonOpportunities++;
totalRevenue += opp.Amount;
// Track revenue by industry
String industry = opp.Account.Industry != null ? opp.Account.Industry : 'Unknown';
if (!revenueByIndustry.containsKey(industry)) {
revenueByIndustry.put(industry, 0);
}
revenueByIndustry.put(industry, revenueByIndustry.get(industry) + opp.Amount);
}
}
}
global void finish(Database.BatchableContext bc) {
// Create summary report
String report = 'Sales Report Summary\n';
report += '====================\n';
report += 'Opportunities Processed: ' + opportunitiesProcessed + '\n';
report += 'Won Opportunities: ' + wonOpportunities + '\n';
report += 'Total Revenue: $' + totalRevenue.format() + '\n\n';
report += 'Revenue by Industry:\n';
for (String industry : revenueByIndustry.keySet()) {
report += industry + ': $' + revenueByIndustry.get(industry).format() + '\n';
}
// Save report or send email
System.debug(report);
// Create a report record
Sales_Report__c salesReport = new Sales_Report__c(
Report_Date__c = Date.today(),
Total_Revenue__c = totalRevenue,
Opportunities_Count__c = wonOpportunities,
Report_Details__c = report
);
insert salesReport;
}
}
Use Case 2: Error Logging and Tracking
global class DataCleanupBatch implements Database.Batchable<sObject>, Database.Stateful {
global Integer successCount = 0;
global Integer errorCount = 0;
global List<String> errorRecordIds = new List<String>();
global Map<String, Integer> errorTypeCount = new Map<String, Integer>();
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, Name, Phone, Email FROM Contact WHERE Email != null'
);
}
global void execute(Database.BatchableContext bc, List<Contact> scope) {
List<Contact> contactsToUpdate = new List<Contact>();
for (Contact con : scope) {
// Clean up phone numbers
if (con.Phone != null) {
con.Phone = cleanPhoneNumber(con.Phone);
}
// Validate email
if (!isValidEmail(con.Email)) {
con.Email = null;
}
contactsToUpdate.add(con);
}
// Update with partial success
Database.SaveResult[] results = Database.update(contactsToUpdate, false);
for (Integer i = 0; i < results.size(); i++) {
if (results[i].isSuccess()) {
successCount++;
} else {
errorCount++;
errorRecordIds.add(contactsToUpdate[i].Id);
for (Database.Error error : results[i].getErrors()) {
String errorType = error.getStatusCode().name();
if (!errorTypeCount.containsKey(errorType)) {
errorTypeCount.put(errorType, 0);
}
errorTypeCount.put(errorType, errorTypeCount.get(errorType) + 1);
}
}
}
}
global void finish(Database.BatchableContext bc) {
// Create comprehensive error report
Batch_Execution_Log__c log = new Batch_Execution_Log__c(
Batch_Name__c = 'DataCleanupBatch',
Execution_Date__c = DateTime.now(),
Success_Count__c = successCount,
Error_Count__c = errorCount,
Total_Records__c = successCount + errorCount
);
insert log;
// Log each error type
for (String errorType : errorTypeCount.keySet()) {
Batch_Error_Detail__c errorDetail = new Batch_Error_Detail__c(
Batch_Log__c = log.Id,
Error_Type__c = errorType,
Error_Count__c = errorTypeCount.get(errorType)
);
insert errorDetail;
}
// Send notification if there were errors
if (errorCount > 0) {
sendErrorNotification(log.Id, errorCount, errorTypeCount);
}
}
private String cleanPhoneNumber(String phone) {
return phone.replaceAll('[^0-9]', '');
}
private Boolean isValidEmail(String email) {
String emailRegex = '^[a-zA-Z0-9._|\\\\%#~`=?&/$^*!}{+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$';
Pattern pattern = Pattern.compile(emailRegex);
Matcher matcher = pattern.matcher(email);
return matcher.matches();
}
private void sendErrorNotification(Id logId, Integer errors, Map<String, Integer> errorTypes) {
// Email notification logic
}
}
Use Case 3: Multi-Step Processing
global class OrderProcessingBatch implements Database.Batchable<sObject>, Database.Stateful {
global Integer ordersProcessed = 0;
global Decimal totalOrderValue = 0;
global Set<Id> accountsToUpdate = new Set<Id>();
global Map<Id, Decimal> accountRevenue = new Map<Id, Decimal>();
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, AccountId, TotalAmount, Status FROM Order WHERE Status = \'Pending\''
);
}
global void execute(Database.BatchableContext bc, List<Order> scope) {
List<Order> ordersToUpdate = new List<Order>();
for (Order ord : scope) {
// Process order
ord.Status = 'Processed';
ordersToUpdate.add(ord);
// Track statistics
ordersProcessed++;
totalOrderValue += ord.TotalAmount != null ? ord.TotalAmount : 0;
// Track accounts that need updating
accountsToUpdate.add(ord.AccountId);
// Calculate revenue by account
if (!accountRevenue.containsKey(ord.AccountId)) {
accountRevenue.put(ord.AccountId, 0);
}
accountRevenue.put(ord.AccountId,
accountRevenue.get(ord.AccountId) + (ord.TotalAmount != null ? ord.TotalAmount : 0));
}
update ordersToUpdate;
}
global void finish(Database.BatchableContext bc) {
// Update accounts with total revenue
List<Account> accountsToUpdateList = [SELECT Id, Total_Revenue__c
FROM Account
WHERE Id IN :accountsToUpdate];
for (Account acc : accountsToUpdateList) {
if (accountRevenue.containsKey(acc.Id)) {
acc.Total_Revenue__c = (acc.Total_Revenue__c != null ? acc.Total_Revenue__c : 0)
+ accountRevenue.get(acc.Id);
}
}
update accountsToUpdateList;
// Create summary record
Order_Processing_Summary__c summary = new Order_Processing_Summary__c(
Processing_Date__c = Date.today(),
Orders_Processed__c = ordersProcessed,
Total_Order_Value__c = totalOrderValue,
Accounts_Updated__c = accountsToUpdate.size()
);
insert summary;
System.debug('Batch completed: ' + ordersProcessed + ' orders processed');
}
}
Use Case 4: Collecting Data for Post-Processing
global class DuplicateDetectionBatch implements Database.Batchable<sObject>, Database.Stateful {
global Map<String, List<Id>> emailToAccountIds = new Map<String, List<Id>>();
global List<Id> duplicateAccountIds = new List<Id>();
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, Email__c FROM Account WHERE Email__c != null ORDER BY Email__c'
);
}
global void execute(Database.BatchableContext bc, List<Account> scope) {
for (Account acc : scope) {
String email = acc.Email__c.toLowerCase();
if (!emailToAccountIds.containsKey(email)) {
emailToAccountIds.put(email, new List<Id>());
}
emailToAccountIds.get(email).add(acc.Id);
}
}
global void finish(Database.BatchableContext bc) {
// Find duplicates
List<Duplicate_Record__c> duplicatesToInsert = new List<Duplicate_Record__c>();
for (String email : emailToAccountIds.keySet()) {
List<Id> accountIds = emailToAccountIds.get(email);
if (accountIds.size() > 1) {
// Found duplicates
for (Id accId : accountIds) {
duplicatesToInsert.add(new Duplicate_Record__c(
Account__c = accId,
Duplicate_Email__c = email,
Duplicate_Count__c = accountIds.size(),
Detected_Date__c = Date.today()
));
duplicateAccountIds.add(accId);
}
}
}
if (!duplicatesToInsert.isEmpty()) {
insert duplicatesToInsert;
// Notify admin
System.debug('Found ' + duplicateAccountIds.size() + ' duplicate accounts');
}
}
}
1. Heap Size Limits:
global class MemoryEfficientBatch implements Database.Batchable<sObject>, Database.Stateful {
// Good: primitive types
global Integer recordCount = 0;
global Decimal totalAmount = 0;
// Risky: large collections
global List<Id> allProcessedIds = new List<Id>(); // Could grow very large!
// Better: use summary data
global Map<String, Integer> summaryByType = new Map<String, Integer>(); // Limited size
}
2. Testing Stateful Batches:
@isTest
private class BatchWithStatefulTest {
@testSetup
static void setup() {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 250; i++) {
accounts.add(new Account(
Name = 'Test Account ' + i,
Industry = i < 100 ? 'Technology' : 'Finance'
));
}
insert accounts;
}
@isTest
static void testStatefulBatch() {
Test.startTest();
SalesReportBatch batch = new SalesReportBatch();
Database.executeBatch(batch, 200);
Test.stopTest();
// Verify stateful data persisted
List<Sales_Report__c> reports = [SELECT Total_Revenue__c FROM Sales_Report__c];
System.assertEquals(1, reports.size(), 'Report should be created');
}
}
| Feature | Without Stateful | With Stateful |
|---|---|---|
| Instance Variables | Reset each batch | Persist across batches |
| Use Case | Independent batch processing | Accumulating data, reporting |
| Memory | Lower | Higher (be careful) |
| Complexity | Simpler | More complex |
| Best For | Standard updates | Aggregation, counting, tracking |
Best Practices:
Aggregate Functions: Aggregate functions allow you to perform calculations on a set of records without retrieving all the individual records. They help you group, count, sum, and compute values efficiently.
Available Aggregate Functions:
COUNT() – Counting Records:
public class AggregateExamples {
// Count all accounts
public static Integer getTotalAccounts() {
AggregateResult result = [SELECT COUNT() totalAccounts FROM Account];
return (Integer)result.get('totalAccounts');
}
// Count accounts with specific criteria
public static Integer getActiveAccounts() {
AggregateResult result = [SELECT COUNT() total
FROM Account
WHERE IsActive__c = true];
return (Integer)result.get('total');
}
// Count with alias
public static void countByIndustry() {
List<AggregateResult> results = [SELECT Industry, COUNT(Id) accountCount
FROM Account
GROUP BY Industry];
for (AggregateResult ar : results) {
String industry = (String)ar.get('Industry');
Integer count = (Integer)ar.get('accountCount');
System.debug(industry + ': ' + count);
}
}
}
COUNT_DISTINCT() – Unique Values:
public class UniqueValueCounter {
// Count unique industries
public static Integer getUniqueIndustries() {
AggregateResult result = [SELECT COUNT_DISTINCT(Industry) uniqueIndustries
FROM Account];
return (Integer)result.get('uniqueIndustries');
}
// Count unique owners
public static Integer getUniqueAccountOwners() {
AggregateResult result = [SELECT COUNT_DISTINCT(OwnerId) uniqueOwners
FROM Account];
return (Integer)result.get('uniqueOwners');
}
}
SUM() – Total Values:
public class RevenueCalculator {
// Total annual revenue
public static Decimal getTotalRevenue() {
AggregateResult result = [SELECT SUM(AnnualRevenue) totalRevenue
FROM Account];
return (Decimal)result.get('totalRevenue');
}
// Sum by industry
public static Map<String, Decimal> getRevenueByIndustry() {
Map<String, Decimal> revenueMap = new Map<String, Decimal>();
List<AggregateResult> results = [SELECT Industry, SUM(AnnualRevenue) revenue
FROM Account
GROUP BY Industry];
for (AggregateResult ar : results) {
String industry = (String)ar.get('Industry');
Decimal revenue = (Decimal)ar.get('revenue');
revenueMap.put(industry, revenue);
}
return revenueMap;
}
// Total opportunity amount by stage
public static void getOpportunityTotalsByStage() {
List<AggregateResult> results = [SELECT StageName, SUM(Amount) total
FROM Opportunity
GROUP BY StageName];
for (AggregateResult ar : results) {
System.debug(ar.get('StageName') + ': $' + ar.get('total'));
}
}
}
AVG() – Average Values:
public class AverageCalculator {
// Average annual revenue
public static Decimal getAverageRevenue() {
AggregateResult result = [SELECT AVG(AnnualRevenue) avgRevenue
FROM Account];
return (Decimal)result.get('avgRevenue');
}
// Average by industry
public static Map<String, Decimal> getAverageRevenueByIndustry() {
Map<String, Decimal> avgMap = new Map<String, Decimal>();
List<AggregateResult> results = [SELECT Industry, AVG(AnnualRevenue) average
FROM Account
GROUP BY Industry];
for (AggregateResult ar : results) {
avgMap.put((String)ar.get('Industry'), (Decimal)ar.get('average'));
}
return avgMap;
}
// Average opportunity amount
public static Decimal getAverageOpportunityAmount() {
AggregateResult result = [SELECT AVG(Amount) avgAmount
FROM Opportunity
WHERE StageName = 'Closed Won'];
return (Decimal)result.get('avgAmount');
}
}
MIN() and MAX():
public class MinMaxCalculator {
// Find date range of opportunities
public static Map<String, Date> getOpportunityDateRange() {
AggregateResult result = [SELECT MIN(CloseDate) minDate, MAX(CloseDate) maxDate
FROM Opportunity];
return new Map<String, Date>{
'earliest' => (Date)result.get('minDate'),
'latest' => (Date)result.get('maxDate')
};
}
// Get revenue range by industry
public static void getRevenueRangeByIndustry() {
List<AggregateResult> results = [SELECT Industry,
MIN(AnnualRevenue) minRevenue,
MAX(AnnualRevenue) maxRevenue
FROM Account
GROUP BY Industry];
for (AggregateResult ar : results) {
System.debug('Industry: ' + ar.get('Industry'));
System.debug('Min: $' + ar.get('minRevenue'));
System.debug('Max: $' + ar.get('maxRevenue'));
}
}
}
Multiple GROUP BY:
public class AdvancedGrouping {
// Group by multiple fields
public static void getOpportunitiesByStageAndOwner() {
List<AggregateResult> results = [SELECT StageName, Owner.Name,
COUNT(Id) oppCount,
SUM(Amount) totalAmount
FROM Opportunity
GROUP BY StageName, Owner.Name
ORDER BY StageName, Owner.Name];
for (AggregateResult ar : results) {
System.debug('Stage: ' + ar.get('StageName'));
System.debug('Owner: ' + ar.get('Name'));
System.debug('Count: ' + ar.get('oppCount'));
System.debug('Total: $' + ar.get('totalAmount'));
}
}
// Group by date functions
public static void getOpportunitiesByMonthAndYear() {
List<AggregateResult> results = [SELECT CALENDAR_YEAR(CloseDate) year,
CALENDAR_MONTH(CloseDate) month,
COUNT(Id) count,
SUM(Amount) total
FROM Opportunity
GROUP BY CALENDAR_YEAR(CloseDate),
CALENDAR_MONTH(CloseDate)
ORDER BY CALENDAR_YEAR(CloseDate),
CALENDAR_MONTH(CloseDate)];
for (AggregateResult ar : results) {
System.debug('Year: ' + ar.get('year') + ', Month: ' + ar.get('month'));
System.debug('Count: ' + ar.get('count') + ', Total: $' + ar.get('total'));
}
}
}
HAVING Clause:
public class HavingClauseExamples {
// Find industries with more than 10 accounts
public static List<String> getLargeIndustries() {
List<String> industries = new List<String>();
List<AggregateResult> results = [SELECT Industry, COUNT(Id) accountCount
FROM Account
GROUP BY Industry
HAVING COUNT(Id) > 10];
for (AggregateResult ar : results) {
industries.add((String)ar.get('Industry'));
}
return industries;
}
// Find accounts with high total opportunity value
public static List<Id> getHighValueAccounts(Decimal minTotalValue) {
List<Id> accountIds = new List<Id>();
List<AggregateResult> results = [SELECT AccountId, SUM(Amount) total
FROM Opportunity
WHERE AccountId != null
GROUP BY AccountId
HAVING SUM(Amount) > :minTotalValue];
for (AggregateResult ar : results) {
accountIds.add((Id)ar.get('AccountId'));
}
return accountIds;
}
// Find owners with average deal size over threshold
public static List<Id> getTopPerformingOwners(Decimal avgThreshold) {
List<Id> ownerIds = new List<Id>();
List<AggregateResult> results = [SELECT OwnerId, AVG(Amount) avgAmount
FROM Opportunity
WHERE StageName = 'Closed Won'
GROUP BY OwnerId
HAVING AVG(Amount) > :avgThreshold];
for (AggregateResult ar : results) {
ownerIds.add((Id)ar.get('OwnerId'));
}
return ownerIds;
}
}
Date Functions with Aggregates:
public class DateAggregation {
// Get opportunities by fiscal quarter
public static void getOpportunitiesByFiscalQuarter() {
List<AggregateResult> results = [SELECT FISCAL_QUARTER(CloseDate) quarter,
FISCAL_YEAR(CloseDate) year,
COUNT(Id) count,
SUM(Amount) total
FROM Opportunity
GROUP BY FISCAL_QUARTER(CloseDate),
FISCAL_YEAR(CloseDate)];
for (AggregateResult ar : results) {
System.debug('FY' + ar.get('year') + ' Q' + ar.get('quarter'));
System.debug('Count: ' + ar.get('count') + ', Total: $' + ar.get('total'));
}
}
// Get cases created per day
public static void getCasesByDay() {
List<AggregateResult> results = [SELECT DAY_ONLY(CreatedDate) day,
COUNT(Id) caseCount
FROM Case
WHERE CreatedDate = LAST_N_DAYS:30
GROUP BY DAY_ONLY(CreatedDate)
ORDER BY DAY_ONLY(CreatedDate)];
for (AggregateResult ar : results) {
System.debug('Date: ' + ar.get('day') + ', Cases: ' + ar.get('caseCount'));
}
}
}
Complex Business Analytics:
public class SalesAnalytics {
// Comprehensive sales dashboard data
public class DashboardData {
public Decimal totalRevenue;
public Decimal averageDealSize;
public Integer totalDeals;
public Integer wonDeals;
public Decimal winRate;
public Map<String, Decimal> revenueByStage;
public Map<String, Integer> dealsByIndustry;
}
public static DashboardData getDashboardData() {
DashboardData data = new DashboardData();
// Get overall statistics
AggregateResult overall = [SELECT COUNT(Id) total,
SUM(Amount) revenue,
AVG(Amount) avgDeal
FROM Opportunity
WHERE CloseDate = THIS_YEAR][0];
data.totalDeals = (Integer)overall.get('total');
data.totalRevenue = (Decimal)overall.get('revenue');
data.averageDealSize = (Decimal)overall.get('avgDeal');
// Get won deals
AggregateResult won = [SELECT COUNT(Id) wonCount
FROM Opportunity
WHERE StageName = 'Closed Won'
AND CloseDate = THIS_YEAR][0];
data.wonDeals = (Integer)won.get('wonCount');
data.winRate = (Decimal)data.wonDeals / data.totalDeals * 100;
// Revenue by stage
data.revenueByStage = new Map<String, Decimal>();
List<AggregateResult> byStage = [SELECT StageName, SUM(Amount) stageRevenue
FROM Opportunity
WHERE CloseDate = THIS_YEAR
GROUP BY StageName];
for (AggregateResult ar : byStage) {
data.revenueByStage.put((String)ar.get('StageName'),
(Decimal)ar.get('stageRevenue'));
}
// Deals by industry
data.dealsByIndustry = new Map<String, Integer>();
List<AggregateResult> byIndustry = [SELECT Account.Industry, COUNT(Id) dealCount
FROM Opportunity
WHERE CloseDate = THIS_YEAR
GROUP BY Account.Industry];
for (AggregateResult ar : byIndustry) {
data.dealsByIndustry.put((String)ar.get('Industry'),
(Integer)ar.get('dealCount'));
}
return data;
}
}
Performance Optimization Example:
public class OptimizedReporting {
// Efficient: Use aggregates instead of processing all records
public static Decimal getTotalRevenueEfficient() {
// One query with aggregate
AggregateResult result = [SELECT SUM(Amount) total
FROM Opportunity
WHERE StageName = 'Closed Won'];
return (Decimal)result.get('total');
}
// Inefficient: Don't do this
public static Decimal getTotalRevenueInefficient() {
// Retrieves all records, processes in memory
List<Opportunity> opps = [SELECT Amount
FROM Opportunity
WHERE StageName = 'Closed Won'];
Decimal total = 0;
for (Opportunity opp : opps) {
total += opp.Amount != null ? opp.Amount : 0;
}
return total;
}
}
Complete Example – Sales Performance Report:
public class SalesPerformanceReport {
public class PerformanceMetrics {
public String ownerName;
public Integer totalOpportunities;
public Integer wonOpportunities;
public Decimal winRate;
public Decimal totalRevenue;
public Decimal averageDealSize;
public Decimal largestDeal;
}
public static List<PerformanceMetrics> getTopPerformers(Integer topN) {
List<PerformanceMetrics> metrics = new List<PerformanceMetrics>();
List<AggregateResult> results = [
SELECT Owner.Name ownerName,
COUNT(Id) totalOpps,
COUNT_DISTINCT(CASE WHEN StageName = 'Closed Won' THEN Id ELSE null END) wonOpps,
SUM(CASE WHEN StageName = 'Closed Won' THEN Amount ELSE 0 END) revenue,
AVG(CASE WHEN StageName = 'Closed Won' THEN Amount ELSE null END) avgDeal,
MAX(CASE WHEN StageName = 'Closed Won' THEN Amount ELSE 0 END) maxDeal
FROM Opportunity
WHERE CloseDate = THIS_YEAR
GROUP BY Owner.Name
ORDER BY SUM(CASE WHEN StageName = 'Closed Won' THEN Amount ELSE 0 END) DESC
LIMIT :topN
];
for (AggregateResult ar : results) {
PerformanceMetrics pm = new PerformanceMetrics();
pm.ownerName = (String)ar.get('ownerName');
pm.totalOpportunities = (Integer)ar.get('totalOpps');
pm.wonOpportunities = (Integer)ar.get('wonOpps');
pm.totalRevenue = (Decimal)ar.get('revenue');
pm.averageDealSize = (Decimal)ar.get('avgDeal');
pm.largestDeal = (Decimal)ar.get('maxDeal');
pm.winRate = pm.totalOpportunities > 0 ?
(Decimal)pm.wonOpportunities / pm.totalOpportunities * 100 : 0;
metrics.add(pm);
}
return metrics;
}
}
Governor Limits:
Governor Limits in Integrations: When integrating Salesforce with external systems, you must be mindful of various governor limits, especially:
1. Use Asynchronous Processing
@future for Callouts:
public class AsyncIntegration {
// Trigger or synchronous method
public static void processRecords(List<Account> accounts) {
Set<Id> accountIds = new Set<Id>();
for (Account acc : accounts) {
accountIds.add(acc.Id);
}
// Call asynchronous method
syncAccountsAsync(accountIds);
}
@future(callout=true)
public static void syncAccountsAsync(Set<Id> accountIds) {
List<Account> accounts = [SELECT Id, Name, Industry, AnnualRevenue
FROM Account
WHERE Id IN :accountIds];
for (Account acc : accounts) {
syncSingleAccount(acc);
}
}
private static void syncSingleAccount(Account acc) {
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.external.com/accounts');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(acc));
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
System.debug('Account synced: ' + acc.Name);
}
}
}
Queueable for Complex Processing:
public class IntegrationQueueable implements Queueable, Database.AllowsCallouts {
private List<Account> accounts;
private Integer startIndex;
private static final Integer BATCH_SIZE = 10;
public IntegrationQueueable(List<Account> accounts, Integer startIndex) {
this.accounts = accounts;
this.startIndex = startIndex;
}
public void execute(QueueableContext context) {
Integer endIndex = Math.min(startIndex + BATCH_SIZE, accounts.size());
// Process batch
for (Integer i = startIndex; i < endIndex; i++) {
Account acc = accounts[i];
callExternalSystem(acc);
}
// Chain next batch if more records exist
if (endIndex < accounts.size()) {
System.enqueueJob(new IntegrationQueueable(accounts, endIndex));
}
}
private void callExternalSystem(Account acc) {
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.external.com/accounts/' + acc.Id);
req.setMethod('PUT');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(new Map<String, Object>{
'name' => acc.Name,
'industry' => acc.Industry
}));
try {
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
// Log success
System.debug('Synced: ' + acc.Name);
} else {
// Log error
System.debug('Error syncing: ' + acc.Name + ', Status: ' + res.getStatusCode());
}
} catch (Exception e) {
System.debug('Exception: ' + e.getMessage());
}
}
}
// Usage
public class AccountTriggerHandler {
public static void syncAccounts(List<Account> accounts) {
if (!System.isFuture() && !System.isBatch()) {
System.enqueueJob(new IntegrationQueueable(accounts, 0));
}
}
}
2. Batch Apex for Large Data Volumes
global class BulkSyncBatch implements Database.Batchable<sObject>, Database.AllowsCallouts, Database.Stateful {
global Integer successCount = 0;
global Integer errorCount = 0;
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, Name, Industry, Website FROM Account WHERE LastModifiedDate = TODAY'
);
}
global void execute(Database.BatchableContext bc, List<Account> scope) {
// Make one callout per batch (or a few)
String jsonData = JSON.serialize(scope);
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.external.com/accounts/bulk');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(jsonData);
req.setTimeout(120000); // 120 seconds
try {
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
successCount += scope.size();
// Parse response and update records if needed
Map<String, Object> responseData = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
// Process response...
} else {
errorCount += scope.size();
// Log errors
}
} catch (Exception e) {
errorCount += scope.size();
System.debug('Batch error: ' + e.getMessage());
}
}
global void finish(Database.BatchableContext bc) {
// Send summary email
System.debug('Sync complete. Success: ' + successCount + ', Errors: ' + errorCount);
}
}
3. Bulkify Callouts
Bad: Individual Callouts
// DON'T DO THIS - Hits callout limit quickly
public static void syncAccountsIndividually(List<Account> accounts) {
for (Account acc : accounts) {
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.external.com/accounts');
req.setMethod('POST');
req.setBody(JSON.serialize(acc));
Http http = new Http();
HttpResponse res = http.send(req); // One callout per account!
}
}
Good: Bulk Callout
public static void syncAccountsBulk(List<Account> accounts) {
// Single callout with all accounts
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.external.com/accounts/bulk');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(accounts)); // Send all at once
req.setTimeout(120000);
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
// Process bulk response
Map<String, Object> results = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
// Handle results...
}
}
4. Platform Events for Decoupling
// Publisher - doesn't wait for integration
public class AccountEventPublisher {
public static void publishAccountChanges(List<Account> accounts) {
List<Account_Event__e> events = new List<Account_Event__e>();
for (Account acc : accounts) {
events.add(new Account_Event__e(
Account_Id__c = acc.Id,
Account_Name__c = acc.Name,
Industry__c = acc.Industry,
Event_Type__c = 'UPDATE'
));
}
// Publish events (doesn't count against callout limits)
List<Database.SaveResult> results = EventBus.publish(events);
for (Database.SaveResult result : results) {
if (!result.isSuccess()) {
System.debug('Error publishing event');
}
}
}
}
// Subscriber - processes events asynchronously
trigger AccountEventTrigger on Account_Event__e (after insert) {
List<String> accountIds = new List<String>();
for (Account_Event__e event : Trigger.new) {
accountIds.add(event.Account_Id__c);
}
// Process in queueable for callouts
System.enqueueJob(new AccountSyncQueueable(accountIds));
}
5. Continuation for Long-Running Callouts
public class LongRunningCallout {
@future(callout=true)
public static void processLargeDataset(String datasetId) {
// Initiate long-running process
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.external.com/process');
req.setMethod('POST');
req.setBody(JSON.serialize(new Map<String, String>{
'datasetId' => datasetId,
'callbackUrl' => 'https://yourorg.my.salesforce.com/services/apexrest/callback'
}));
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 202) {
// Process accepted, will receive callback later
System.debug('Process initiated');
}
}
}
// Callback endpoint
@RestResource(urlMapping='/callback/*')
global class CallbackHandler {
@HttpPost
global static void handleCallback() {
RestRequest req = RestContext.request;
Map<String, Object> payload = (Map<String, Object>)JSON.deserializeUntyped(req.requestBody.toString());
String status = (String)payload.get('status');
String datasetId = (String)payload.get('datasetId');
if (status == 'completed') {
// Process results
System.debug('Processing complete for: ' + datasetId);
}
}
}
6. Caching to Reduce Queries
public class CachedIntegration {
private static Map<String, API_Config__mdt> configCache;
private static Map<Id, Account> accountCache;
static {
// Load configuration once
configCache = new Map<String, API_Config__mdt>();
for (API_Config__mdt config : [SELECT DeveloperName, Endpoint__c, API_Key__c
FROM API_Config__mdt]) {
configCache.put(config.DeveloperName, config);
}
}
public static API_Config__mdt getConfig(String configName) {
return configCache.get(configName);
}
public static void bulkSyncAccounts(Set<Id> accountIds) {
// Query once, cache results
accountCache = new Map<Id, Account>(
[SELECT Id, Name, Industry, Website
FROM Account
WHERE Id IN :accountIds]
);
// Use cached accounts for processing
for (Id accId : accountIds) {
Account acc = accountCache.get(accId);
if (acc != null) {
syncAccount(acc);
}
}
}
private static void syncAccount(Account acc) {
API_Config__mdt config = getConfig('Default');
HttpRequest req = new HttpRequest();
req.setEndpoint(config.Endpoint__c + '/accounts');
req.setMethod('POST');
req.setHeader('Authorization', 'Bearer ' + config.API_Key__c);
req.setBody(JSON.serialize(acc));
// Make callout...
}
}
7. Monitoring and Error Handling
public class IntegrationMonitor {
public class IntegrationMetrics {
public Integer calloutCount = 0;
public Integer soqlCount = 0;
public Integer dmlCount = 0;
public Long cpuTime = 0;
public Integer heapSize = 0;
}
public static IntegrationMetrics getMetrics() {
IntegrationMetrics metrics = new IntegrationMetrics();
metrics.calloutCount = Limits.getCallouts();
metrics.soqlCount = Limits.getQueries();
metrics.dmlCount = Limits.getDMLStatements();
metrics.cpuTime = Limits.getCpuTime();
metrics.heapSize = Limits.getHeapSize();
return metrics;
}
public static Boolean canMakeCallout() {
return Limits.getCallouts() < Limits.getLimitCallouts();
}
public static void logLimitWarning(String context) {
if (Limits.getCallouts() > Limits.getLimitCallouts() * 0.8) {
System.debug('WARNING: Approaching callout limit in ' + context);
}
if (Limits.getQueries() > Limits.getLimitQueries() * 0.8) {
System.debug('WARNING: Approaching SOQL limit in ' + context);
}
}
}
// Usage
public class SafeIntegration {
public static void syncAccounts(List<Account> accounts) {
if (!IntegrationMonitor.canMakeCallout()) {
System.debug('Cannot make callout - limit reached');
// Queue for later or use platform events
return;
}
// Make callout
HttpRequest req = new HttpRequest();
// ... setup request
Http http = new Http();
HttpResponse res = http.send(req);
IntegrationMonitor.logLimitWarning('Account Sync');
}
}
8. Composite API for Multiple Operations
public class CompositeAPIIntegration {
public static void performMultipleOperations() {
// Use Composite API to reduce callout count
HttpRequest req = new HttpRequest();
req.setEndpoint(URL.getSalesforceBaseUrl().toExternalForm() +
'/services/data/v59.0/composite');
req.setMethod('POST');
req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId());
req.setHeader('Content-Type', 'application/json');
// Multiple operations in one callout
Map<String, Object> compositeRequest = new Map<String, Object>{
'allOrNone' => false,
'compositeRequest' => new List<Object>{
new Map<String, Object>{
'method' => 'POST',
'url' => '/services/data/v59.0/sobjects/Account',
'referenceId' => 'NewAccount',
'body' => new Map<String, String>{'Name' => 'Test Account'}
},
new Map<String, Object>{
'method' => 'GET',
'url' => '/services/data/v59.0/sobjects/Account/001xx000003DGb0AAG',
'referenceId' => 'GetAccount'
}
}
};
req.setBody(JSON.serialize(compositeRequest));
Http http = new Http();
HttpResponse res = http.send(req);
System.debug('Composite response: ' + res.getBody());
}
}
Best Practices Summary: