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:
- 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.
- 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
| Scenario | Pattern |
|---|---|
| Callout result determines whether to save | Callout first, then DML |
| Need Salesforce record ID in the callout | @future(callout=true) or Queueable |
| Trigger-initiated callout | Always @future or Queueable (callouts are prohibited in trigger context) |
| Chaining multiple callout + DML pairs | Queueable 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)orQueueablewhen 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.