Skip to main content

Setup vs Non-Setup Objects (Mixed DML)

Core Platform & Fundamentals

Module map

MODULE 1 root: 'Who can do what, on which objects/fields?'

├─ Setup vs non-setup (transaction rules)

├─ Profile + Perm Sets + PSG (access = union)

├─ Licensing (the ceiling)

├─ CRUD/FLS (objects & fields)

└─ Reports/Dashboards (who sees what data)

1.1 Setup vs Non-Setup Objects (Mixed DML)

💬 In plain words:  Salesforce keeps 'admin' records (like User, Group) and 'business' records (like Account, Case) in two separate rooms. One transaction cannot write to both rooms at once — that error is Mixed DML. The fix: do the second write in a separate async step.

Concept

Salesforce partitions sObjects into setup objects (User, Group, GroupMember, PermissionSet, PermissionSetAssignment, QueueSObject, UserRole, etc.) that configure the org itself, and non-setup objects (Account, Case, custom objects) that hold business data. Because setup DML can change who is allowed to see what mid-transaction, the platform forbids mixing the two kinds of DML in a single transaction — the MIXED_DML_OPERATION error. The standard fix is to move one side of the operation into a separate transaction context: an async job (Queueable/@future — see Module 6) or System.runAs() inside test code. This topic connects directly to Module 3 (sharing recalculation is why the restriction exists) and Module 6 (async as the fix).

Objects

├─ Setup (User, Group, PermSet, Role, Queue…)  → configure the ORG

└─ Non-setup (Account, Case, custom…)          → business DATA

        Mixing DML on both in 1 txn → MIXED_DML_OPERATION

        Fix: async job  |  runAs() (test only)  |  phased load (cutover)

🧠 Mixed DML fix:  "Setup + non-setup can't share a transaction." Split them → async (Queueable/@future) in prod, System.runAs() in tests only.

Core Q&A

Q: You insert an Account and then assign a Permission Set to a User in the same trigger context, and it fails. Why, and how do you fix it in production code?

🎯 Say this first:  It's a Mixed DML error — setup and business objects can't share one transaction; move the permission-set insert into a Queueable.

A: That is a mixed DML violation: PermissionSetAssignment is a setup object and Account is a non-setup object, and both cannot be committed in one transaction because setup changes can alter security evaluation of the same transaction's data. In production code the fix is to isolate the setup DML into its own transaction by enqueueing a Queueable (or @future) job that performs the PermissionSetAssignment insert after the business-data transaction commits. System.runAs() is NOT a production option — it is available only in test context. A cleaner architectural answer: user/permission provisioning should live in its own service (async by design) rather than being a side-effect of business-record triggers.

public class AssignPermSetJob implements Queueable {

    private Id userId; private Id permSetId;

    public AssignPermSetJob(Id u, Id p) { userId = u; permSetId = p; }

    public void execute(QueueableContext ctx) {

        insert new PermissionSetAssignment(

            AssigneeId = userId, PermissionSetId = permSetId);

    }

}

// From the trigger/service handling Account DML:

System.enqueueJob(new AssignPermSetJob(userId, permSetId));

Follow-ups (scenario-based)

Q1: Are there setup objects that are exempt, and what happens with mixed DML inside a Batch Apex execute()?

A1: A few objects that look like setup objects are exempt or partially exempt. For example, you can DML FeedItem, and User inserts CAN be mixed if the insert does not touch fields that affect sharing/roles. But relying on that is fragile, and interviewers expect you to treat User as setup. Inside Batch Apex each execute () is its own transaction, but mixed DML is still blocked within that single execute(); the pattern is the same — chain a Queueable from the batch for the setup-object half.

Q2: During a legacy re-platforming, user provisioning and data migration must happen together. How do you avoid mixed DML at cutover scale?

A2: Frame it as sequencing, not code tricks. At migration/cutover you separate phases: provision Users, Roles, and Permission Set assignments first via the setup pipeline (Data Loader/Bulk API run as its own job). Then load business data in a second phase referencing the now-existing user IDs for ownership. Inside the org, any runtime provisioning (e.g., auto-creating a user record when an assessment record demands it) was pushed to Queueable jobs so business-data triggers never touched setup objects synchronously. This shows the interviewer you solve mixed DML architecturally (phased pipelines) at LDV scale, not just with an @future patch.