Transaction Finalizers in Salesforce

Modern Salesforce implementations regularly use asynchronous processing for large volume data, external integrations, and long-running business logic. For this developers move heavy work off the main transaction to Queueable Apex, but traditional Queueables have one persistent limitation: once the async job finishes or fails, there is no clean in-built hook to run guaranteed post processing logic in the same execution lifecycle. That is exactly the problem Transaction Finalizers solve.

Transaction Finalizers provide a structured mechanism to execute logic after a Queueable job completes whether it succeeds, fails, or is aborted. They allow developers to concentrate cleanup, retry handling, logging, notifications, and chained processing in a far more reliable way than older workarounds.

What Problem Do Transaction Finalizers Solve?

Dealing post queueable was quite difficult before Transaction Finalizers. For example if a Queueable performs an external API callout and if the callout fails because of various reasons like timeout, hitting the limits, or any other unhandled exception then you need to 

  1. Log the error into a custom object .
  2. Notify admins about the error.
  3. Retry the job if the failure is temporary.
  4. Then trigger downstream processing only if the original job succeeds.

Developers need to distribute the logic into different processes like try/catch blocks or custom monitoring frameworks, etc without the finalizers. But this approach has several issues. For example if the Queueable fails even before reaching the catch block then cleanup logic may never execute. And als0 handling success and failure processes may cause duplicates across many queueables. Third, retry mechanisms become inconsistent and hard to maintain.

Transaction Finalizers solve this by giving you one guaranteed execution point after the Queueable finishes, regardless of outcome.

In Salesforce, Transaction Finalizers are implemented by creating a class that implements the System.Finalizer interface. That implementation can be attached to a Queueable job and is invoked automatically after the Queueable transaction completes, whether the job succeeds or fails. Conceptually, this provides a platform-managed equivalent of a finally block for asynchronous Queueable execution.

When Should You Use Transaction Finalizers?

Transaction Finalizers  can be used when retrying failed integration jobs, logging async failures to a custom monitoring object, sending an email or notification when an async process fails, for cleaning up temporary records and also can be used for chaining additional queueables after Queueable for post processing.

How Transaction Finalizers Work

The lifecycle is straightforward,  Queueable job starts execution and inside the Queueable, you attach a Finalizer using System.attachFinalizer().And when the Queueable completes, fails, or aborts Salesforce invokes the Finalizer automatically, the Finalizer receives execution context containing job result information.This allows your finalizer to inspect whether the Queueable succeeded or failed and respond appropriately.

Syntax and Working Example Using Standard Salesforce Objects

Let’s build a practical example using the standard Account object.

Assume your business updates Account ratings asynchronously and wants retry/error logging if processing fails.

public class AccountRatingProcessor implements Queueable, Finalizer {

    private List<Id> accountIds;

    public AccountRatingProcessor(List<Id> accountIds) {
        this.accountIds = accountIds;
    }

    // Queueable Execution Logic
    public void execute(QueueableContext qc) {

        // Attach this same class as finalizer
        System.attachFinalizer(this);

        List<Account> accounts = [
            SELECT Id, Rating
            FROM Account
            WHERE Id IN :accountIds
        ];

        for (Account acc : accounts) {
            acc.Rating = 'Hot';
        }

        update accounts;
    }

    // Finalizer Execution Logic
    public void execute(FinalizerContext fc) {

        if (fc.getResult() == ParentJobResult.SUCCESS) {
            System.debug('Account rating update completed successfully.');
        }
        else if (fc.getResult() == ParentJobResult.UNHANDLED_EXCEPTION) {
            System.debug('Queueable failed. Retrying job...');

            System.enqueueJob(
                new AccountRatingProcessor(accountIds)
            );
        }
    }
}

How This Example Works

The above queueable code updates the Account rating field. Even before performing the update logic it attaches to the Transaction finalizer using System.attachFinalizer(this). Since we are implementing both queueable and finalizer in same class it invokes the appropriate execute() method based on context. If the update process runs successfully, finalizer logs a success message after the transaction completes and in the same way if the queueable process fails with an unhandled exception, finalizer logs a failure message and retries the job by enqueueing a new instance of the same class. In this way creates a resilient retry mechanism while keeping both the processing and post-processing logic encapsulated in one implementation.

Why This Is Better Than Try/Catch Alone

Using  try/ catch blocks to handle exceptions and finally for post clean logic works only within the same transaction. And it is also limited to the limits and life cycle of that particular transaction and won’t handle the final outcome of an asynchronous job. But Transaction Finalizers has the capability to provide a guaranteed post-execution hook that runs in a separate transaction after the Queueable completes, irrespective of whether it fails or succeeds, which developers a reliable way to implement retry logic, logging, cleanup, and notifications with fresh governor limits and full visibility into the job result.

Limitations

Transaction Finalizers are powerful, but they have some limitations like :

  1. They work only with Queueable Apex and cannot be attached directly to other async processes like Batch, Future, Schedule.
  2. We are able to attach just one Finalizer to a Queueable.
  3. Finalizer wont be running in the same transaction as Queueable it runs in its own separate transaction and own limits, Even when both queueable and finalizers were implemented in the same class they separate execute method, making clear separation.
  4. A Finalizer cannot attach another Finalizer during its execution.

Because of these limits, Finalizers are best suited for orchestration, monitoring, and reliability logic rather than heavy business processing.

Best Practices 

  1. Try to keep finalizer logic simple and light weight and limit its use to post processing primarily for handling retries, clean up processes and sending notifications.
  2. For better usage of finalizer log all finalizer outcomes into an custom object like Async_Job_Log__c  for troubleshooting and operational visibility.
  3. While dealing with retry logic for failed logic use fc.getException() to inspect failures intelligently.
  4. Also make sure to implement count protection on retries while reenqueueing failed jobs to avoid infinite recursion problem
  5. Avoid retrying blindly, as data issues or validation rule failures will continue failing and unnecessarily consume async resources.

Conclusion

Transaction Finalizers are one of  Salesforce’s most valuable features for Queueable asynchronous process which solves the long standing limitation of guaranteed post execution after the queueable irrespective of whether the job succeed or fails, which helps developers implement retry logic, monitoring, data cleanup process and also enable them to build more robust async solutions. By leveraging Transaction Finalizers, Salesforce teams can move beyond basic async execution and design systems that are production-ready, fault-tolerant, and operationally observable.

Satyam parasa
Satyam parasa

Satyam Parasa is a Salesforce and Mobile application developer. Passionate about learning new technologies, he is the founder of Flutterant.com, where he shares his knowledge and insights.

Articles: 82

Leave a Reply

Your email address will not be published. Required fields are marked *