System.DmlException: MIXED_DML_OPERATION) happens when an Apex transaction attempts to perform Data Manipulation Language (DML) operations (such as insert, update, or delete) on both Setup Objects (like Users, Groups, or Permission Sets) and Non-Setup Objects (like Accounts, Contacts, or custom objects) in the same execution context.
When developing automated onboarding flows, automated user provisioning, or trigger logic in Salesforce, you may encounter an unexpected transaction rollback with the error: MIXED_DML_OPERATION, DML operation on setup object is not permitted after you have updated a non-setup object (or vice versa). Understanding the underlying security architecture and transaction boundaries is key to resolving this exception permanently.
1. Why Salesforce Enforces the Mixed DML Restriction
Salesforce separates database tables into two broad categories to protect security access controls and metadata integrity:
- Setup Objects: Govern user permissions, organizational hierarchy, and access metadata. Examples include
User,UserRole,Group,GroupMember,PermissionSetAssignment, andQueueSobject. - Non-Setup Objects: Represent standard business data and custom records. Examples include
Account,Contact,Opportunity,Case, and all custom objects (CustomObject__c).
Because modifying a Setup record immediately changes sharing calculations and object-level permissions, modifying regular data in the exact same database transaction can cause permission race conditions and data corruption. Salesforce blocks this by requiring separate transaction contexts.
- Error Signature:
System.DmlException: MIXED_DML_OPERATION - Core Trigger: Mixing DML on Setup (e.g., User, Group) & Non-Setup (e.g., Account) in 1 context.
- Production Fixes: Asynchronous execution via
@futureorQueueable Apex. - Unit Test Fix: Wrapping setup record creation in
System.runAs()blocks.
2. Proven Strategies to Resolve Mixed DML Errors
Move the Setup object modification into a separate asynchronous thread with its own transaction boundaries.
public with sharing class UserProvisioningService {
// Non-setup DML in the main synchronous transaction
public static void createCustomerAccount(String accountName, String userEmail) {
Account newAcc = new Account(Name = accountName);
insert as user newAcc;
// Call asynchronous future method to create/assign user without Mixed DML
assignPermissionSetAsync(userEmail, 'Standard_User_Access');
}
// Setup object DML isolated in its own transaction
@future
public static void assignPermissionSetAsync(String userEmail, String permSetName) {
User targetUser = [SELECT Id FROM User WHERE Email = :userEmail LIMIT 1];
PermissionSet ps = [SELECT Id FROM PermissionSet WHERE Name = :permSetName LIMIT 1];
PermissionSetAssignment psa = new PermissionSetAssignment(
AssigneeId = targetUser.Id,
PermissionSetId = ps.Id
);
insert as user psa;
}
}
Queueable Apex supports complex sObject parameter passing and job chaining, making it superior to
@future for multi-step processes.
public with sharing class AssignUserQueueable implements Queueable {
private Id userId;
private Id groupId;
public AssignUserQueueable(Id userId, Id groupId) {
this.userId = userId;
this.groupId = groupId;
}
public void execute(QueueableContext context) {
// Setup object DML executes in an isolated background thread
GroupMember gm = new GroupMember(
UserOrGroupId = this.userId,
GroupId = this.groupId
);
insert as user gm;
}
}
// In your main business logic:
// 1. Insert Non-Setup record (e.g., Contact)
insert as user contactRecord;
// 2. Enqueue the Setup DML into a distinct asynchronous queue
System.enqueueJob(new AssignUserQueueable(targetUserId, targetGroupId));
In test classes, you can enclose Setup record creation within a
System.runAs() block to create a distinct execution sub-context.
@isTest
private class UserCreationTest {
@isTest
static void testMixedDMLResolution() {
// 1. Non-Setup DML (Account)
Account acc = new Account(Name = 'Test Enterprise');
insert acc;
// Fetch an existing admin user to run setup changes
User adminUser = [SELECT Id FROM User WHERE Profile.Name = 'System Administrator' AND IsActive = true LIMIT 1];
// 2. Isolate Setup DML inside System.runAs block
System.runAs(adminUser) {
User standardUser = TestDataFactory.createTestUser();
insert standardUser; // Setup DML succeeds without Mixed DML exception
}
// Verify outcomes
Assert.areEqual(1, [SELECT COUNT() FROM Account WHERE Name = 'Test Enterprise']);
}
}
3. Common Traps & Platform Best Practices
Writing trigger logic on
Contact or Account that directly inserts a User or GroupMember record will always throw a Mixed DML error upon execution. Triggers cannot change their own execution context synchronously—you must delegate the Setup DML to a Queueable or @future method.
- Prefer Queueable Over @future: Use
Queueable Apexinstead of@futurebecause it allows passing complex sObjects, returns a Job ID for monitoring, and supports job chaining. - Enforce Security Standards: Always include
as userorWITH USER_MODEon database operations to respect current user permissions. - Be Mindful of Async Limits: Remember that Salesforce enforces daily governor limits on asynchronous Apex executions (such as 250,000 asynchronous calls or 200 times the number of user licenses).
Summary
The Mixed DML Operation error is a safeguard designed to maintain platform access control and metadata consistency. By cleanly separating Setup and Non-Setup DML into distinct transactions using Queueable Apex, @future methods, or System.runAs() in test contexts, developers can build scalable, error-free provisioning and automation workflows across Salesforce.