Unlocking Callouts: Rolling Back Transactions and Releasing Savepoints in Apex

Introduction

The recent Spring ’24 release of Salesforce introduced an important enhancement to the way callouts can be made after rolling back uncommitted transactions in Apex code. Previously, attempting to make a callout after creating a savepoint in a transaction would result in a CalloutException, even if the transaction was rolled back. This would prevent developers from being able to integrate callouts with complex transactions involving savepoints.

The new Database.releaseSavepoint() method now allows developers to explicitly release savepoints after rolling back changes, enabling callouts to be made subsequently within the same transaction. This unblocks several important use cases and improves the ability to integrate external services and APIs with Apex transactions.

In this article, we will cover the details of this enhancement, explain the issues it aims to solve, provide examples of how to make use of Database.releaseSavepoint() correctly, and discuss some best practices to keep in mind when working with savepoints and callouts in Apex.

Advertisements

The Problem

To understand the need for this enhancement, we must first review how savepoints work in Apex transactions. The Database class provides methods to set, release and roll back to savepoints within a transaction. For example:

Savepoint sp = Database.setSavepoint(); 

// ... DML operations 

Database.rollback(sp);

This allows changes made after the savepoint to be rolled back, while maintaining changes made earlier in the transaction. Savepoints can be nested, rolled back selectively, and so on.

The key issue arises when you attempt to make an external callout after creating one or more savepoints. For example:

Savepoint sp = Database.setSavepoint();

// ... DML operations 

Database.rollback(sp); 

// Callout attempt  
HttpResponse res = h.httpGet('http://example.com');

Previously, this would fail with a CalloutException, with the message:

“You have uncommitted work pending. Please commit or rollback before calling out.”

This exception would happen even after rolling back all uncommitted work to the savepoint. The platform did not have a way to determine if any savepoints remained “active” deeper in the stack after a rollback.

This posed problems for some use cases, like:

  • Wrapping external service calls in a transaction to handle errors
  • Making callouts after selective nested rollbacks
  • Combining external APIs with complex multi-statement transactions

The New Database.releaseSavepoint() Method

To enable callouts after savepoint rollbacks, the Spring ’24 release introduced a new Database.releaseSavepoint() method. This method allows developers to explicitly release a savepoint, indicating to the system that the savepoint is no longer needed.

For example, the previous transaction can now be written as:

Savepoint sp = Database.setSavepoint();

// ... DML operations

Database.rollback(sp);

Database.releaseSavepoint(sp);

// Callout now allowed
HttpResponse res = h.httpGet('http://example.com'); 

By releasing the savepoint after rolling back, you enable the platform to allow the subsequent callout by clearing the savepoint stack.

The releaseSavepoint() method also cascades and releases any savepoints created after the one passed to it. So if you have nested savepoints like:

Savepoint outer = Database.setSavepoint();
Savepoint inner = Database.setSavepoint();

// ... transactions 

Database.rollback(outer);
Database.releaseSavepoint(outer); 

This will release both “outer” and “inner” savepoints.

Advertisements

Examples and Use Cases of Database.releaseSavepoint()

Let’s go through some examples to demonstrate cases where the new releaseSavepoint() capability enables new transaction patterns involving callouts.

Wrapping a Callout in a Transaction

A common use case is wanting to wrap an external API call in a Apex transaction to encapsulate it:

Savepoint sp = Database.setSavepoint();

try {

  // Call external service
  HttpResponse res = h.httpGet('http://example.com');  

  // Process response and update DB
  insert data;

} catch(Exception e) {

  Database.rollback(sp);

  // Handle exception

}

Previously, its was not possible to make the callout after the setSavepoint without getting an exception. Now we can release the savepoint before making the callout after rolling back:

Savepoint sp = Database.setSavepoint();

try {

  // Call external service
  HttpResponse res = h.httpGet('http://example.com');  

  // Process response and update DB
  insert data;

} catch(Exception e) {

  Database.rollback(sp);
  Database.releaseSavepoint(sp);

  // Handle exception or make callout retry

}

Integrating Callouts with Nested Savepoints

More complex use cases may involve nested savepoints. For example, wanting to integrate a multi-step transaction involving external APIs:

response1 = // Call API 1 

Savepoint sp1 = Database.setSavepoint();

// Process response1
try {

  response2 = // Call API 2

  // Process response2  
  insert data;

} catch(Exception e) {

  Database.rollback(sp1);

  // Compensating logic

}

Savepoint sp2 = Database.setSavepoint(); 

// More business logic

Database.rollback(sp2);

// Callout to external system
notifySystem(); 

Without the ability to release savepoints, this would previously fail on the notifySystem() call. But now we can explicitly release the savepoints before making the cross-system call:

Database.releaseSavepoint(sp1); 
Database.releaseSavepoint(sp2);

// Cross-system callout
notifySystem();

This unblocks many similar cases of complex transaction handling across systems.

Integration Testing

Explicit savepoint releases also helps when writing integration tests that combine transactions with callouts.

Without releases, all savepoints created during a test method would continue to accumulate without being released, eventually leading to exceptions when attempting callouts after transaction controls in setup or teardown logic.

Now in test methods, developers can manually release savepoints at logical points to enable callouts later in the test:

@isTest
private static void myTest() {

  // Set up test data
  Savepoint sp1 = Database.setSavepoint();
  insert records;

  // Release at end of setup 
  Database.releaseSavepoint(sp1);

  // Testing logic 
  Savepoint sp2 = Database.setSavepoint();

  // ... transactions

  // Release before callouts
  Database.releaseSavepoint(sp2); 

  // Callouts
  HttpResponse res = h.httpGet(...);

  // Assertions

}

This better supports Tests that need to orchestrate a sequence with interleaved transactions and callouts.

Advertisements

Best Practices

While the new savepoint release capability enables new transaction control patterns, there are some best practices to keep in mind:

Release Early

Don’t hold on to savepoints for longer than required. Release them as soon as possible after rolling back unneeded transaction work. Lingering inactive savepoints can constrain future callout capabilities within a transaction or test method.

Keep Nested Usage Minimal

Complex nested savepoint usage can make transaction handling overly intricate and fragile. See if transaction logic can be kept simple through flat single-level savepoints. Release savepoints before additional nested ones if nesting is unavoidable.

Keep Release Logic Modular

Encapsulate savepoint rollback and release in separate handler methods. Don’t embed that logic throughout application code. Makes transaction rollback/release behavior easier to test and maintain.

Test Error Cases

Rigorously test transaction rollbacks and callout sequences to cover both success and failure paths. Use asserts to validate expected exception behavior for out-of-order callouts or unreleased savepoints.

Summary

The Spring ’24 Database.releaseSavepoint() enhancement marks an important improvement for transaction control flexibility in Apex. It enables the combining of external service callouts with complex transaction savepoint logic that was previously unavailable. Developers can now integrate such use cases without limits or exceptions.

However, with added power comes increased responsibility. Mindful savepoint management and sound transaction design is still imperative for robust application code. Employ the best practices outlined here when employing this new capability within your own solutions.

About the blog

SFDCLessons is a blog where you can find various Salesforce tutorials and tips that we have written to help beginners and experienced developers alike. we also share my experience and knowledge on Salesforce best practices, troubleshooting, and optimization. Don’t forget to follow us on:

Newsletter

Subscribe to our email newsletter to be notified when a new post is published.

Arun Kumar

Arun Kumar is a Salesforce Certified Platform Developer I with over 7+ years of experience working on the Salesforce platform. He specializes in developing custom applications, integrations, and reports to help customers streamline their business processes. Arun is passionate about helping businesses leverage the power of Salesforce to achieve their goals.

This Post Has One Comment

Leave a Reply