Skip to main content

HTTP Callouts and DML: The Transaction Order Trap

One of the more counterintuitive Salesforce governor limit errors is System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out. This error fires when you attempt an HTTP callout after performing a DML operation in the same transaction -- and it catches developers off guard because the code compiles without issue.

Why Salesforce Prevents This

Salesforce transactions are all-or-nothing. When you perform a DML operation (insert, update, delete), the changes are staged but not committed until the entire transaction completes successfully. If a callout were allowed after DML, two problems arise:

  1. Rollback inconsistency -- If the callout succeeds but a later operation fails and rolls back the transaction, the external system has already received data that Salesforce just undid.
  2. Lock contention -- DML operations hold row-level locks. Allowing a callout (which involves network latency) while holding those locks would degrade performance across the platform.

Salesforce enforces a simple rule: callouts and DML cannot coexist in the same transaction if DML happens first.

The Trap in Action

This code looks perfectly reasonable but will throw at runtime:

public class OrderSyncService {
public static void createAndSync(String orderName, String endpoint) {
// DML first -- this stages uncommitted work
Order__c ord = new Order__c(Name = orderName);
insert ord;

// Callout after DML -- FAILS at runtime
HttpRequest req = new HttpRequest();
req.setEndpoint(endpoint);
req.setMethod('POST');
req.setBody(JSON.serialize(ord));

Http http = new Http();
HttpResponse res = http.send(req); // throws CalloutException
}
}

Solution 1: Reorder Operations (Callout First)

The simplest fix is restructuring so the callout executes before any DML. The inverse scenario -- callout before DML -- is perfectly legal.

public class OrderSyncService {
public static void createAndSync(String orderName, String endpoint) {
// Callout FIRST -- no uncommitted work yet
HttpRequest req = new HttpRequest();
req.setEndpoint(endpoint);
req.setMethod('POST');
req.setBody('{"name":"' + orderName + '"}');

Http http = new Http();
HttpResponse res = http.send(req);

// DML AFTER callout -- transaction integrity maintained
if (res.getStatusCode() == 200) {
Order__c ord = new Order__c(
Name = orderName,
External_Id__c = res.getBody()
);
insert ord;
}
}
}

This works when the callout does not depend on a Salesforce record ID.

Solution 2: Separate Transactions with @future or Queueable

When you genuinely need the record ID before making the callout (e.g., sending the Salesforce ID to an external system), split the work into two transactions:

public class OrderSyncService {
public static void createAndSync(String orderName, String endpoint) {
Order__c ord = new Order__c(Name = orderName);
insert ord;

// Offload callout to a separate transaction
syncToExternalSystem(ord.Id, endpoint);
}

@future(callout=true)
public static void syncToExternalSystem(Id orderId, String endpoint) {
Order__c ord = [SELECT Id, Name FROM Order__c WHERE Id = :orderId];

HttpRequest req = new HttpRequest();
req.setEndpoint(endpoint);
req.setMethod('POST');
req.setBody(JSON.serialize(ord));

Http http = new Http();
HttpResponse res = http.send(req);
}
}

For more complex scenarios, a Queueable class with Database.AllowsCallouts offers better chaining and state management than @future.

When to Use Each Pattern

ScenarioPattern
Callout result determines whether to saveCallout first, then DML
Need Salesforce record ID in the callout@future(callout=true) or Queueable
Trigger-initiated calloutAlways @future or Queueable (callouts are prohibited in trigger context)
Chaining multiple callout + DML pairsQueueable with Database.AllowsCallouts

Key Takeaways

  • DML before callout in the same transaction always fails at runtime.
  • Callout before DML in the same transaction is allowed.
  • Use @future(callout=true) or Queueable when the callout depends on a committed record.
  • This is a frequently tested concept on the Platform Developer I exam -- know the rule and the workarounds.