In the world of enterprise CRM systems, data integrity is paramount. Salesforce’s robust transaction management system ensures that your critical business data remains consistent, reliable, and secure across all operations.
Transaction management in Salesforce refers to the platform’s ability to ensure that database operations are processed reliably and maintain data integrity.
A transaction is a sequence of operations performed as a single logical unit of work, which either completes entirely or fails completely – there’s no middle ground.
Salesforce follows the ACID principles (Atomicity, Consistency, Isolation, Durability) to guarantee reliable transaction processing across its multi-tenant architecture.
All operations within a transaction are treated as a single unit. If any operation fails, the entire transaction is rolled back, leaving the database in its original state.
Example:
try {
Account acc = new Account(Name = 'Tech Corp');
insert acc;
Contact con = new Contact(
LastName = 'Smith',
AccountId = acc.Id,
Email = 'invalid-email-format' // This will cause validation error
);
insert con;
// If contact insertion fails, account insertion is also rolled back
} catch (Exception e) {
System.debug('Transaction failed: ' + e.getMessage());
// At this point, no Account record exists in the database
}
Transactions ensure that the database remains in a valid state before and after the transaction, respecting all validation rules, triggers, and constraints.
Multiple transactions can occur simultaneously without interfering with each other, preventing data corruption from concurrent access.
Once a transaction is committed, the changes are permanently stored and will survive system failures.
Understanding transaction boundaries is crucial for effective Salesforce development. Here’s how different contexts define transaction boundaries:
public class TransactionExample {
public static void complexBusinessProcess() {
// Single transaction begins here
Savepoint sp = Database.setSavepoint();
try {
// Create parent records
List<Account> accounts = new List<Account>();
accounts.add(new Account(Name = 'Account 1', Industry = 'Technology'));
accounts.add(new Account(Name = 'Account 2', Industry = 'Finance'));
insert accounts;
// Create child records
List<Contact> contacts = new List<Contact>();
for (Account acc : accounts) {
contacts.add(new Contact(
LastName = 'Contact for ' + acc.Name,
AccountId = acc.Id
));
}
insert contacts;
// Create opportunities
List<Opportunity> opportunities = new List<Opportunity>();
for (Account acc : accounts) {
opportunities.add(new Opportunity(
Name = 'Opportunity for ' + acc.Name,
AccountId = acc.Id,
StageName = 'Prospecting',
CloseDate = Date.today().addDays(30)
));
}
insert opportunities;
// All operations succeed - transaction commits automatically
} catch (Exception e) {
// Roll back to savepoint if any operation fails
Database.rollback(sp);
throw new CustomException('Business process failed: ' + e.getMessage());
}
// Transaction ends here
}
}
trigger AccountTrigger on Account (before insert, after insert, before update, after update) {
// Entire trigger execution is within a single transaction
if (Trigger.isAfter && Trigger.isInsert) {
List<Contact> defaultContacts = new List<Contact>();
for (Account acc : Trigger.new) {
// Create default contact for each new account
defaultContacts.add(new Contact(
LastName = 'Default Contact',
AccountId = acc.Id,
Email = 'default@' + acc.Name.toLowerCase().replaceAll('[^a-zA-Z0-9]', '') + '.com'
));
}
if (!defaultContacts.isEmpty()) {
insert defaultContacts; // This is part of the same transaction
}
}
}
Savepoints allow you to create checkpoints within a transaction and roll back to specific points without affecting the entire transaction:
public class SavepointExample {
public static void processAccountsWithErrorHandling(List<Account> accounts) {
Savepoint mainSavepoint = Database.setSavepoint();
List<Account> successfulAccounts = new List<Account>();
List<String> errors = new List<String>();
for (Account acc : accounts) {
Savepoint individualSavepoint = Database.setSavepoint();
try {
// Validate account data
if (String.isBlank(acc.Name)) {
throw new ValidationException('Account name is required');
}
// Perform complex business logic
acc.Description = 'Processed on ' + DateTime.now().format();
acc.Rating = 'Warm';
// Simulate external API call validation
if (acc.Name.length() > 50) {
throw new CalloutException('Account name too long for external system');
}
successfulAccounts.add(acc);
} catch (Exception e) {
// Roll back only this account's changes
Database.rollback(individualSavepoint);
errors.add('Failed to process account ' + acc.Name + ': ' + e.getMessage());
}
}
try {
if (!successfulAccounts.isEmpty()) {
upsert successfulAccounts;
}
} catch (Exception e) {
// Roll back everything if final insert fails
Database.rollback(mainSavepoint);
throw new ProcessingException('Batch processing failed: ' + e.getMessage());
}
// Log errors for failed accounts
if (!errors.isEmpty()) {
System.debug('Processing errors: ' + String.join(errors, '; '));
}
}
}
For scenarios where you want some records to succeed even if others fail:
public class PartialSuccessExample {
public static void bulkInsertWithPartialSuccess(List<Contact> contacts) {
Database.SaveResult[] results = Database.insert(contacts, false); // Allow partial success
List<Contact> successfulContacts = new List<Contact>();
List<String> errors = new List<String>();
for (Integer i = 0; i < results.size(); i++) {
Database.SaveResult result = results[i];
if (result.isSuccess()) {
successfulContacts.add(contacts[i]);
System.debug('Successfully inserted contact: ' + contacts[i].LastName);
} else {
String errorMsg = 'Failed to insert contact ' + contacts[i].LastName + ': ';
for (Database.Error error : result.getErrors()) {
errorMsg += error.getMessage() + ' ';
}
errors.add(errorMsg);
}
}
// Process successful contacts further
if (!successfulContacts.isEmpty()) {
createFollowUpTasks(successfulContacts);
}
// Handle errors appropriately
if (!errors.isEmpty()) {
logErrorsToCustomObject(errors);
}
}
private static void createFollowUpTasks(List<Contact> contacts) {
List<Task> followUpTasks = new List<Task>();
for (Contact con : contacts) {
followUpTasks.add(new Task(
Subject = 'Follow up with ' + con.LastName,
WhoId = con.Id,
ActivityDate = Date.today().addDays(7),
Priority = 'Normal',
Status = 'Not Started'
));
}
insert followUpTasks;
}
}
Salesforce enforces governor limits per transaction to ensure platform stability. Understanding these limits is crucial for effective transaction design:
public class GovernorLimitOptimization {
public static void bulkProcessAccounts(List<Account> accounts) {
// Check limits before processing
System.debug('DML Statements used: ' + Limits.getDmlStatements() + '/' + Limits.getLimitDmlStatements());
System.debug('SOQL Queries used: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());
// Batch processing to respect governor limits
Integer batchSize = 200; // Safe batch size
List<List<Account>> batches = createBatches(accounts, batchSize);
for (List<Account> batch : batches) {
processBatch(batch);
// Check if we're approaching limits
if (Limits.getDmlStatements() > 140) { // Leave buffer
System.debug('Approaching DML limit, consider async processing');
break;
}
}
}
private static List<List<Account>> createBatches(List<Account> records, Integer batchSize) {
List<List<Account>> batches = new List<List<Account>>();
for (Integer i = 0; i < records.size(); i += batchSize) {
Integer endIndex = Math.min(i + batchSize, records.size());
batches.add(records.subList(i, endIndex));
}
return batches;
}
private static void processBatch(List<Account> batch) {
// Single DML operation for the entire batch
update batch;
// Update related records efficiently
List<Contact> contactsToUpdate = [
SELECT Id, AccountId, Email
FROM Contact
WHERE AccountId IN :batch
];
for (Contact con : contactsToUpdate) {
con.Description = 'Account updated on ' + Date.today().format();
}
if (!contactsToUpdate.isEmpty()) {
update contactsToUpdate;
}
}
}
Proper transaction management ensures that your business data remains consistent and accurate, preventing partial updates that could lead to data corruption.
Business Impact:
Structured transaction management allows for sophisticated error handling strategies:
public class RobustTransactionExample {
public static OpportunityResult createOpportunityWithTeam(OpportunityRequest request) {
Savepoint sp = Database.setSavepoint();
OpportunityResult result = new OpportunityResult();
try {
// Create opportunity
Opportunity opp = new Opportunity(
Name = request.opportunityName,
AccountId = request.accountId,
StageName = 'Qualification',
CloseDate = request.closeDate,
Amount = request.amount
);
insert opp;
result.opportunityId = opp.Id;
// Create opportunity team members
List<OpportunityTeamMember> teamMembers = new List<OpportunityTeamMember>();
for (String userId : request.teamMemberIds) {
teamMembers.add(new OpportunityTeamMember(
OpportunityId = opp.Id,
UserId = userId,
TeamMemberRole = 'Sales Team'
));
}
if (!teamMembers.isEmpty()) {
insert teamMembers;
}
// Create initial activity
Task initialTask = new Task(
Subject = 'Initial qualification call',
WhatId = opp.Id,
OwnerId = opp.OwnerId,
ActivityDate = Date.today().addDays(1),
Priority = 'High'
);
insert initialTask;
result.success = true;
result.message = 'Opportunity created successfully with team and initial task';
} catch (DmlException e) {
Database.rollback(sp);
result.success = false;
result.message = 'Database error: ' + e.getDmlMessage(0);
result.errorType = 'DML_ERROR';
} catch (Exception e) {
Database.rollback(sp);
result.success = false;
result.message = 'Unexpected error: ' + e.getMessage();
result.errorType = 'SYSTEM_ERROR';
}
return result;
}
}
public class OpportunityRequest {
public String opportunityName;
public Id accountId;
public Date closeDate;
public Decimal amount;
public List<String> teamMemberIds;
}
public class OpportunityResult {
public Boolean success;
public String message;
public Id opportunityId;
public String errorType;
}
Proper transaction management can significantly improve performance by:
Well-designed transactions scale better as your organization grows:
// Good: Focused transaction
public static void updateAccountRating(Id accountId, String newRating) {
Account acc = [SELECT Id, Rating FROM Account WHERE Id = :accountId];
acc.Rating = newRating;
update acc;
}
// Avoid: Long-running transactions with multiple unrelated operations
public static void massiveTransaction() {
// Multiple unrelated operations in single transaction
// This can cause lock contention and timeout issues
}
// Good: Bulk operations
public static void updateMultipleAccounts(Map<Id, String> accountRatings) {
List<Account> accountsToUpdate = new List<Account>();
for (Id accId : accountRatings.keySet()) {
accountsToUpdate.add(new Account(
Id = accId,
Rating = accountRatings.get(accId)
));
}
update accountsToUpdate; // Single DML operation
}
Always implement comprehensive error handling with appropriate rollback strategies and user-friendly error messages.
public class TransactionMonitoring {
public static void monitoredTransaction() {
Long startTime = System.currentTimeMillis();
Integer startQueries = Limits.getQueries();
Integer startDML = Limits.getDmlStatements();
try {
// Your transaction logic here
performBusinessLogic();
} finally {
Long endTime = System.currentTimeMillis();
System.debug('Transaction completed in: ' + (endTime - startTime) + 'ms');
System.debug('Queries used: ' + (Limits.getQueries() - startQueries));
System.debug('DML statements used: ' + (Limits.getDmlStatements() - startDML));
}
}
}
Avoid mixing setup objects (Users, Profiles) with standard objects in the same transaction.
// Incorrect: Mixed DML
User usr = new User(/* user details */);
insert usr;
Account acc = new Account(Name = 'Test', OwnerId = usr.Id);
insert acc; // This will fail due to mixed DML
// Correct: Separate transactions or use System.runAs()
System.runAs(new User(Id = UserInfo.getUserId())) {
User usr = new User(/* user details */);
insert usr;
}
Account acc = new Account(Name = 'Test');
insert acc;
Plan your transactions to stay well within governor limits, especially in bulk operations.
Always implement proper exception handling and provide meaningful error messages to users.
Mastering Salesforce transaction management is essential for building robust, scalable, and reliable applications on the platform. By understanding ACID properties, transaction boundaries, and best practices, you can ensure data integrity while optimizing performance and user experience.
[…] Mastering Salesforce Transaction Management […]