Mixed DML Exception in Salesforce Apex: Complete Guide with Examples & Solutions

If you’ve ever inserted a User and then tried to update an Account in the same transaction, you already know the pain: System.MixedDmlException: DML operation on setup object is not permitted after you have updated a non-setup object. It’s one of the more confusing errors a Salesforce developer runs into, mostly because it doesn’t happen the first ten times you write similar code — only when the object combination happens to trip Salesforce’s internal rules.

This guide walks through what causes the mixed DML error, shows you real code that triggers it, and gives you six ways to fix it depending on your situation — trigger, Flow, batch job, or test class.

Table of Contents


What is the Mixed DML Exception in Salesforce?

The mixed DML exception in Salesforce Apex happens when you try to insert, update, or delete a setup object (like User or Group) and a non-setup object (like Account or Contact) in the same transaction. Salesforce throws this as a runtime error, not a compile-time one, so your code will look completely fine until it actually executes.

The exact message reads:

System.MixedDmlException: DML operation on setup object is not permitted 
after you have updated a non-setup object (or vice versa). Please 
perform DML on setup object in a separate transaction from non-setup 
objects.

Salesforce blocks this combination because setup objects can trigger a recalculation of sharing rules and field-level access across the org. If that recalculation ran in the middle of a transaction that’s also touching regular business records, the platform couldn’t guarantee data consistency — so it simply refuses to let the two mix.

This is also a favorite Salesforce interview question, usually phrased as: “What happens if you insert a User and update an Account in the same method?” If you’ve read this far, you can already answer it.

What are Setup and Non-Setup Objects in Salesforce?

Before you can fix the error, you need to know which side of the fence your objects sit on. Salesforce splits sObjects into two buckets, and the split isn’t always intuitive.

What are Setup Objects?

Setup objects generally control who has access to what in your org. The most common ones you’ll bump into are:

  • User — the actual user account record
  • UserRole — role hierarchy assignments
  • PermissionSetAssignment — grants a permission set to a user
  • Group — public groups and queues
  • QueueSObject — assigns objects to a queue
  • Territory and UserTerritory — territory management assignments

There are more (GroupMember, ObjectPermissions, FieldPermissions among them), but these six cover the vast majority of real-world mixed DML errors.

What are Non-Setup Objects?

Non-setup objects are your everyday business records — the ones your sales and service teams actually work in:

  • Account
  • Contact
  • Opportunity
  • Case
  • Any custom object you’ve built

Setup vs Non-Setup Objects Comparison Table

CategoryExamplesTriggers Sharing Recalculation?Common in Mixed DML Errors?
Setup ObjectsUser, UserRole, PermissionSetAssignment, Group, QueueSObject, TerritoryYesYes — very common
Non-Setup ObjectsAccount, Contact, Opportunity, Case, Custom ObjectsNoYes — as the “other half”

Here’s a detail that trips people up: this isn’t about whether an object is “standard” or “custom.” A custom object is non-setup by default. The classification is about what the object does inside the security model, not where it came from.

Why Does the Mixed DML Error Occur?

Under the hood, this comes down to how Salesforce protects transaction integrity.

Access recalculation. Any DML on a setup object can change who can see what. Adding a user to a role, for instance, might ripple through the entire sharing hierarchy above and below that role.

Sharing recalculation. Salesforce recalculates implicit and explicit sharing whenever role, group, or permission assignments change. That’s a heavier operation than a normal record save, and it needs to run in isolation.

Transaction security. If Salesforce let both operation types run in one transaction, a rollback partway through could leave the org in an inconsistent security state — records visible to people who shouldn’t see them, or the reverse. The platform would rather throw an exception than risk that.

In short: this isn’t a bug you’re running into. It’s a deliberate guardrail. Salesforce’s multi-tenant architecture depends on strict separation between “who can see records” and “what’s in the records,” and mixed DML is where that separation becomes visible to you as a developer.

Mixed DML Error in Salesforce Example

Let’s look at the code that actually breaks.

Apex example that throws the exception

Apex Code

public class UserAccountCreator {
    public static void createUserAndAccount() {
        Account acc = new Account(Name = 'Acme Corp');
        insert acc; // non-setup object

        Profile p = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1];
        User u = new User(
            LastName = 'Smith',
            Email = 'smith@example.com',
            Username = 'smith@example.com.test',
            Alias = 'smithj',
            ProfileId = p.Id,
            TimeZoneSidKey = 'America/Los_Angeles',
            LocaleSidKey = 'en_US',
            EmailEncodingKey = 'UTF-8',
            LanguageLocaleKey = 'en_US'
        );
        insert u; // setup object — this line throws MixedDmlException
    }
}

The Account insert happens first, then the User insert fails because both objects were touched in the same transaction.

Trigger example

Apex Code

trigger UserTrigger on User (after insert) {
    List<Account> accountsToUpdate = new List<Account>();
    for (User u : Trigger.new) {
        accountsToUpdate.add(new Account(Id = someAccountId, Description = 'Updated by trigger'));
    }
    update accountsToUpdate; // throws MixedDmlException
}

Because the User insert that fired the trigger and the Account update inside it belong to the same transaction, this fails the same way.

Flow example

A Screen Flow that creates a new User record and, in the same interview, updates a related Account or Contact field will hit the identical wall. Flow doesn’t get a pass here — it’s still executing Apex-equivalent DML behind the scenes, so the same rule applies.

Test class example

Apex Code

@isTest
public class UserAccountTest {
    @isTest
    static void testCreation() {
        Account acc = new Account(Name = 'Test Corp');
        insert acc;

        User u = new User(/* required fields */);
        insert u; // fails here too, same as production code
    }
}

Test classes aren’t exempt. If anything, this is where the error shows up most often, because developers write test setup data quickly and don’t think about object order.

Exception Message Explained

The error code you’ll see in debug logs is MIXED_DML_OPERATION. It shows up in a handful of recurring scenarios:

  • Creating a User right after inserting or updating an Account or Contact
  • Assigning a PermissionSetAssignment after modifying a custom object
  • Adding a user to a Group in the same method that updates a Case
  • Running System.runAs() incorrectly inside a test method

If you see MIXED_DML_OPERATION in a log, search for any setup-object DML statement and non-setup-object DML statement within the same method call stack — that’s almost always where the conflict lives.

How to Resolve Mixed DML Error in Salesforce

There isn’t one universal fix. The right solution depends on whether you’re in a trigger, a Flow, a batch job, or a test class. Here are six approaches, in order of how often you’ll actually reach for them.

Solution 1: Future Method (@future)

The most common fix. Move the setup-object DML into an asynchronous method, which runs in its own transaction.

Apex Code

public class UserAccountCreator {
    public static void createAccount() {
        Account acc = new Account(Name = 'Acme Corp');
        insert acc;
        createUserAsync();
    }

    @future
    public static void createUserAsync() {
        User u = new User(/* required fields */);
        insert u;
    }
}

Pros: Simple to implement, works in most contexts, doesn’t need a queue. Cons: No return value, can’t call another @future method from inside it, and you’re limited to 50 future calls per transaction.

Solution 2: Queueable Apex

Queueable Apex is the modern replacement for @future in most new development, and for good reason — it gives you chaining, complex parameter types, and better job monitoring.

Apex Code

public class UserCreatorQueueable implements Queueable {
    public void execute(QueueableContext context) {
        User u = new User(/* required fields */);
        insert u;
    }
}

// Calling it:
System.enqueueJob(new UserCreatorQueueable());

Prefer Queueable over @future when you need to pass non-primitive objects (like an sObject or a custom class) into the async method, or when you want to chain a second job after the first completes.

Solution 3: Platform Events

For enterprise-scale architecture, Platform Events decouple the setup-object operation entirely. Publish an event after your non-setup DML completes, then handle the User or PermissionSetAssignment creation in a separate trigger listening for that event. This adds architectural overhead, but it scales well when multiple systems need to react to the same user-provisioning event.

Solution 4: Scheduled Apex

If the setup-object operation doesn’t need to happen immediately — say, a nightly batch that syncs permission sets — Scheduled Apex sidesteps the mixed DML problem entirely by running as its own transaction on a schedule.

Solution 5: Separate Transactions Using Flow’s Scheduled or Asynchronous Path

Flow Builder gives you an easy option here: put the setup-object action on a Scheduled Path or an Asynchronous Path rather than the immediate run path. Since the scheduled or async path executes as a new transaction, it naturally avoids the conflict without a single line of Apex.

Solution 6: Using System.runAs in Test Classes

This one’s specific to tests, and it’s covered in detail in the next section — but the short version is that System.runAs() starts a new transaction context, which lets you separate setup and non-setup DML inside a single test method.

SolutionBest ForComplexity
@futureQuick fixes, simple syncLow
QueueableComplex parameters, chainingMedium
Platform EventsMulti-system architectureHigh
Scheduled ApexNon-urgent batch operationsLow
Flow Async PathNo-code/low-code teamsLow
System.runAsTest classes onlyLow

Mixed DML Error in Salesforce Test Class

Test classes fail on mixed DML more often than production code, mostly because test setup data gets written fast and rarely gets the same scrutiny as business logic.

Why test classes commonly fail

A typical test class inserts a Profile query, a test User, and a handful of Account or Contact records — all in @testSetup or right at the top of the test method. If the User insert and the Account insert land in the same block without System.runAs(), you’ll hit the same wall as production code.

Correct use of System.runAs

Apex Code

@isTest
static void testMixedDml() {
    Profile p = [SELECT Id FROM Profile WHERE Name='Standard User' LIMIT 1];
    User testUser = new User(
        ProfileId = p.Id,
        LastName = 'Test',
        Email = 'test@example.com',
        Username = 'test@example.com.uniquetest',
        Alias = 'tuser',
        TimeZoneSidKey = 'America/Los_Angeles',
        LocaleSidKey = 'en_US',
        EmailEncodingKey = 'UTF-8',
        LanguageLocaleKey = 'en_US'
    );
    insert testUser;

    System.runAs(testUser) {
        Account acc = new Account(Name = 'Test Account');
        insert acc; // runs in a fresh transaction context
    }
}

System.runAs() doesn’t just switch the running user for permission testing — it also opens a new transaction, which is exactly what separates the setup-object DML from the non-setup-object DML.

Test data factory best practices

If your org has more than a couple of test classes, build a shared TestDataFactory class that creates users through System.runAs() once, then hands back the user record for reuse. It saves you from copy-pasting the same 12-field User constructor into every test method, and it keeps the mixed DML fix consistent across your codebase.

Complete working example

Apex Code

@isTest
public class TestDataFactory {
    public static User createTestUser() {
        Profile p = [SELECT Id FROM Profile WHERE Name='Standard User' LIMIT 1];
        User u = new User(
            ProfileId = p.Id,
            LastName = 'Factory',
            Email = 'factory@example.com',
            Username = 'factory@example.com.' + DateTime.now().getTime(),
            Alias = 'fact',
            TimeZoneSidKey = 'America/Los_Angeles',
            LocaleSidKey = 'en_US',
            EmailEncodingKey = 'UTF-8',
            LanguageLocaleKey = 'en_US'
        );
        insert u;
        return u;
    }
}

@isTest
public class AccountServiceTest {
    @isTest
    static void testAccountCreation() {
        User u = TestDataFactory.createTestUser();
        System.runAs(u) {
            Account acc = new Account(Name = 'Runas Test');
            insert acc;
            System.assertNotEquals(null, acc.Id);
        }
    }
}

Real-World Scenarios

Creating a User and Account together. This is the classic case: a Partner Community setup where a new partner contact needs both an Account record and a portal User created at the same time. Split it — Account first in the synchronous transaction, User creation queued asynchronously.

User onboarding automation. HR systems trigger a User creation, and a downstream process wants to update a related Employee custom object at the same time. Queueable Apex is usually the cleanest fix here.

Permission Set Assignment after user creation. You create a User, then immediately try to assign a PermissionSet — both setup objects, so this one’s actually fine on its own. The error shows up when a non-setup object update gets mixed in nearby.

Community/Experience Cloud user creation. Enabling a Contact as an Experience Cloud user creates a User record behind the scenes. If your Contact trigger updates other non-setup fields in the same save, you’ll hit mixed DML without even calling insert on a User yourself.

Flow plus Apex mixed DML scenarios. A Flow calls an invocable Apex method that updates an Account, and the Flow itself also updates a related User field on the before-save path. Because Flow and invocable Apex share a transaction, the same rule applies as if you’d written it all in one class.

Best Practices to Avoid Mixed DML Exceptions

  • Separate setup and business transactions by design, not as an afterthought. Plan which DML belongs where before you write the trigger.
  • Prefer Queueable Apex for new development — it’s more flexible than @future and easier to test.
  • Avoid creating Users inside core business logic. Push user provisioning into its own service class that runs asynchronously.
  • Consider event-driven architecture for anything touching multiple downstream systems, not just a single Apex trigger.
  • Log and monitor async job failures. A @future or Queueable method that silently fails is worse than a mixed DML error you catch in testing, since nobody sees it happen.

Common Mistakes Developers Make

  1. Creating a User and Account in the same transaction without checking which object type each one is.
  2. Assigning a PermissionSetAssignment right after updating an Account in the same method, assuming both are “just records.”
  3. Writing tests without System.runAs() and being surprised when a passing dev-org test fails in CI.
  4. Building Flow after-save automation that mixes setup-object updates with non-setup-object updates on the same record-triggered flow.

Frequently Asked Questions

What is Mixed DML Exception? It’s a Salesforce runtime error that occurs when you perform DML on a setup object (User, Group, PermissionSetAssignment) and a non-setup object (Account, Contact, custom object) within the same transaction.

Why does Salesforce prevent mixed DML? Because setup-object changes can trigger sharing and access recalculation, and running that alongside regular record DML risks leaving the org’s security model inconsistent if a rollback occurs mid-transaction.

Which objects are setup objects? User, UserRole, PermissionSetAssignment, Group, QueueSObject, Territory, and several permission-related objects like GroupMember and ObjectPermissions.

Can Queueable solve Mixed DML? Yes. Running the setup-object DML inside a Queueable Apex class puts it in its own transaction, separate from the calling code’s non-setup DML.

Does Future Method always solve it? In most cases, yes — but it comes with limits: no return values, a 50-call cap per transaction, and you can’t call another @future method from within one.

How do I fix Mixed DML in Flow? Move the setup-object action to a Scheduled Path or Asynchronous Path in Flow Builder, which runs as a separate transaction from the main Flow logic.

How do I fix Mixed DML in Test Classes? Wrap the non-setup-object DML inside System.runAs(testUser) { } after inserting the test user outside that block.

Does Database.insert avoid Mixed DML? No. Using Database.insert() instead of insert only changes error-handling behavior (partial success vs. all-or-nothing) — it doesn’t bypass the setup/non-setup object rule.

Does Mixed DML affect governor limits? Not directly. It’s a separate validation from governor limits, though the async methods you’ll use to fix it (like @future or Queueable) do count against their own separate limits, such as the 50 future calls per transaction.


Key Takeaways

Mixed DML isn’t a bug — it’s Salesforce protecting its sharing model from inconsistent mid-transaction states. Once you know which objects count as “setup” (User, Group, PermissionSetAssignment, and a handful of others), fixing it is mostly a matter of separating transactions: @future, Queueable Apex, Platform Events, Scheduled Apex, or a Flow async path in production, and System.runAs() in your test classes.

Decision matrix — which fix should you reach for?

  • Need it done right now, simple case → @future
  • Passing complex objects or need job chaining → Queueable Apex
  • Multiple systems need to react → Platform Events
  • Can wait until off-hours → Scheduled Apex
  • Building in Flow, no Apex → Scheduled/Async Path
  • Writing a test → System.runAs()

Want a printable version of this decision matrix for your team’s Apex style guide? Grab the cheat sheet below and keep it next to your IDE.

Next step: If mixed DML errors are a recurring headache in your org, it’s worth auditing your triggers and Flows for setup/non-setup object combinations before they hit production. Check out our other Apex architecture guides for a deeper look at async patterns, governor limits, and order of execution.

Add a Comment

Your email address will not be published. Required fields are marked *

Discover more from SFDCRocks247

Subscribe now to keep reading and get access to the full archive.

Continue reading