Mixed DML is the error Salesforce throws when a single transaction tries to perform DML on both a setup object and a non-setup object. The runtime blocks it with:
MIXED_DML_OPERATION: DML operation on setup object is not permitted after you have updated a non-setup object (or vice versa).
This isn’t an edge case — it’s a fundamental rule. Setup metadata and business data live in different commit phases, and the platform doesn’t allow them to share a transaction.
Setup vs non-setup objects
| Setup objects (cannot mix) | Non-setup objects (regular data) |
|---|---|
User | Account, Contact, Opportunity, Lead, Case |
Group | Custom objects (MyObject__c) |
GroupMember | Task, Event |
UserRole | Attachment, Note |
UserTerritory | Most other standard objects |
Territory | |
QueueSObject | |
PermissionSet / PermissionSetAssignment |
When in doubt, the Apex Developer Guide list is authoritative.
What triggers the error
// This will throw MIXED_DML_OPERATION
User newUser = new User(
Username = '[email protected]',
Alias = 'newuser',
Email = '[email protected]',
LastName = 'User',
TimeZoneSidKey = 'America/Los_Angeles',
LocaleSidKey = 'en_US',
EmailEncodingKey = 'UTF-8',
ProfileId = profileId,
LanguageLocaleKey = 'en_US'
);
insert newUser; // setup DML
Account a = new Account(Name = 'Acme');
insert a; // non-setup DML — boom
The trap is that the error doesn’t fire on whichever DML you think of as “first” — it fires on whichever runs second, in either direction.
Three legitimate fixes
1. Future method (async deferral)
The most common pattern: move one of the two DMLs to an @future method, which runs in its own transaction.
public class UserProvisioner {
public static void provision(Id contactId, Id profileId) {
// Non-setup DML now
Contact c = [SELECT Id, FirstName, LastName, Email FROM Contact WHERE Id = :contactId];
c.Provisioned__c = true;
update c;
// Setup DML later, in a separate transaction
createUser(c.Email, c.LastName, profileId);
}
@future
static void createUser(String email, String lastName, Id profileId) {
User u = new User(
Username = email + '.demo',
Email = email,
LastName = lastName,
Alias = email.substring(0, 5),
ProfileId = profileId,
TimeZoneSidKey = 'America/Los_Angeles',
LocaleSidKey = 'en_US',
EmailEncodingKey = 'UTF-8',
LanguageLocaleKey = 'en_US'
);
insert u;
}
}
Queueable works the same way for the same reason — each enqueued job runs as its own transaction.
2. System.runAs in tests
In test methods, System.runAs() resets the mixed-DML check. This is only valid in tests — it has no production analogue.
@isTest
static void testUserCreation() {
User runAsUser = TestDataFactory.createUser();
System.runAs(runAsUser) {
Account a = new Account(Name = 'Acme');
insert a; // setup + non-setup mixed safely under runAs
}
}
This is why every Salesforce test that creates a User and then non-setup data wraps the second DML in runAs.
3. Order matters in some edge cases
A few specific setup objects — notably PermissionSetAssignment — sometimes allow mixed DML if the non-setup DML comes first and is the only non-setup DML. Don’t rely on this; the safe pattern is async deferral.
Why Salesforce enforces this rule
Setup metadata changes flow through a different commit path than business data changes — they may trigger cache invalidation, profile recompilation, or permission recalculation across the org. Mixing them in one transaction would create ordering hazards the platform doesn’t want to reason about. The mixed-DML rule is the simplest enforcement of that boundary.
What interviewers are really looking for
This is an advanced trigger / async question. The signal isn’t “do you know what the error is” — it’s whether you can reach for @future, Queueable, or runAs reflexively when you see User/Group/PermissionSet DML mixed with anything else. Mention all three fixes and explain why the runtime enforces this, and you’ve nailed the question.
Verified against: Apex Developer Guide — sObjects That Cannot Be Used Together in DML Operations. Last reviewed 2026-05-17 for Spring ‘26 release.