Skip to main content

Salesforce Basics: Setup vs Non-Setup Objects (Mixed DML)

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 — raising the MIXED_DML_OPERATION exception. The standard fix is to move one side of the operation into a separate transaction context using an asynchronous job (Queueable / @future) or using System.runAs() within test methods.

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. 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 exclusively in test contexts.
  • Architectural Pattern: User and permission provisioning should exist as a dedicated asynchronous service rather than a synchronous side-effect inside 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 sObjects have specific exemptions or partial behaviors:

  • For example, you can perform DML on FeedItem alongside non-setup objects, and User inserts can sometimes be mixed if the insert does not modify fields affecting sharing or roles.
  • However, relying on these edge cases is fragile; architects should treat User as a strict setup object.
  • Inside Batch Apex, each execute() batch invocation is its own discrete transaction, but mixed DML is still prohibited within a single execute() batch. The solution remains identical: enqueue or chain a Queueable job from the batch method.
Q2: During a legacy re-platforming, user provisioning and data migration must happen together. How do you avoid mixed DML at cutover scale?

A2: Solve this through pipeline sequencing rather than code workarounds:

  • Phase 1: Provision Users, Roles, and Permission Set assignments first using the Bulk API / Data Loader as an isolated setup job.
  • Phase 2: Migrate business data in a subsequent load referencing the pre-existing User IDs for record ownership.
  • Runtime Provisioning: Any runtime user creation (e.g., auto-creating a user when an assessment record is submitted) should be dispatched to asynchronous Queueable jobs so business triggers never touch setup objects synchronously.

Recommended Reading: Profiles vs Permission Sets vs Permission Set Groups